com.smedic.tubtub.BackgroundAudioService.java Source code

Java tutorial

Introduction

Here is the source code for com.smedic.tubtub.BackgroundAudioService.java

Source

/*
 * Copyright (C) 2016 SMedic
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.smedic.tubtub;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.v4.media.RatingCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v7.app.NotificationCompat;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.util.SparseArray;
import android.widget.Toast;

import com.smedic.tubtub.model.ItemType;
import com.smedic.tubtub.model.YouTubeVideo;
import com.smedic.tubtub.utils.Config;
import com.smedic.tubtub.utils.NetworkConf;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;

import java.io.IOException;
import java.util.ArrayList;
import java.util.ListIterator;

import at.huber.youtubeExtractor.VideoMeta;
import at.huber.youtubeExtractor.YouTubeExtractor;
import at.huber.youtubeExtractor.YtFile;

/**
 * Service class for background youtube playback
 * Created by Stevan Medic on 9.3.16..
 */
public class BackgroundAudioService extends Service
        implements MediaPlayer.OnCompletionListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener {

    private static final String TAG = "SMEDIC service";

    public static final String ACTION_PLAY = "action_play";
    public static final String ACTION_PAUSE = "action_pause";
    public static final String ACTION_NEXT = "action_next";
    public static final String ACTION_PREVIOUS = "action_previous";
    public static final String ACTION_STOP = "action_stop";

    private WifiManager.WifiLock mWifiLock;
    private MediaPlayer mMediaPlayer;
    private MediaSessionCompat mSession;
    private MediaControllerCompat mController;

    private NetworkConf network;

    private ItemType mediaType = ItemType.YOUTUBE_MEDIA_NONE;

    private YouTubeVideo videoItem;

    private ArrayList<YouTubeVideo> youTubeVideos;
    private int videoIndex = 0;

    private NotificationCompat.Builder builder = null;

    private boolean isPrepared = false;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        videoItem = new YouTubeVideo();

        network = new NetworkConf(getApplicationContext());

        mWifiLock = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE))
                .createWifiLock(WifiManager.WIFI_MODE_FULL, "tub_wifi_lock");

        mMediaPlayer = new MediaPlayer();
        mMediaPlayer.setOnCompletionListener(this);
        mMediaPlayer.setOnPreparedListener(this);
        mMediaPlayer.setOnErrorListener(this);

        initMediaSessions();
        initPhoneCallListener();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        handleIntent(intent);
        return super.onStartCommand(intent, flags, startId);
    }

    private void initPhoneCallListener() {
        PhoneStateListener phoneStateListener = new PhoneStateListener() {
            @Override
            public void onCallStateChanged(int state, String incomingNumber) {
                switch (state) {
                case TelephonyManager.CALL_STATE_RINGING:
                    pauseVideo();
                    break;
                case TelephonyManager.CALL_STATE_IDLE:
                    resumeVideo();
                    break;
                case TelephonyManager.CALL_STATE_OFFHOOK:
                default:
                    break;
                }

                super.onCallStateChanged(state, incomingNumber);
            }
        };

        TelephonyManager mgr = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        if (mgr != null) {
            mgr.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
        }
    }

    /**
     * Handles intent (player options play/pause/stop...)
     *
     * @param intent
     */
    private void handleIntent(Intent intent) {
        if (intent == null || intent.getAction() == null)
            return;
        String action = intent.getAction();
        if (action.equalsIgnoreCase(ACTION_PLAY)) {
            handleMedia(intent);
            mController.getTransportControls().play();
        } else if (action.equalsIgnoreCase(ACTION_PAUSE)) {
            mController.getTransportControls().pause();
        } else if (action.equalsIgnoreCase(ACTION_PREVIOUS)) {
            mController.getTransportControls().skipToPrevious();
        } else if (action.equalsIgnoreCase(ACTION_NEXT)) {
            mController.getTransportControls().skipToNext();
        } else if (action.equalsIgnoreCase(ACTION_STOP)) {
            mController.getTransportControls().stop();
        }
    }

    /**
     * Handles media - playlists and videos sent from fragments
     *
     * @param intent
     */
    private void handleMedia(Intent intent) {
        ItemType intentMediaType = ItemType.YOUTUBE_MEDIA_NONE;
        if (intent.getSerializableExtra(Config.YOUTUBE_TYPE) != null) {
            intentMediaType = (ItemType) intent.getSerializableExtra(Config.YOUTUBE_TYPE);
        }
        switch (intentMediaType) {
        case YOUTUBE_MEDIA_NONE: //video is paused,so no new playback requests should be processed
            playVideo();
            break;
        case YOUTUBE_MEDIA_TYPE_VIDEO:
            mediaType = ItemType.YOUTUBE_MEDIA_TYPE_VIDEO;
            videoItem = (YouTubeVideo) intent.getSerializableExtra(Config.YOUTUBE_TYPE_VIDEO);
            if (videoItem.getId() != null) {
                playVideo();
            }
            break;
        case YOUTUBE_MEDIA_TYPE_PLAYLIST: //new playlist playback request
            mediaType = ItemType.YOUTUBE_MEDIA_TYPE_PLAYLIST;
            youTubeVideos = (ArrayList<YouTubeVideo>) intent.getSerializableExtra(Config.YOUTUBE_TYPE_PLAYLIST);
            int startPosition = intent.getIntExtra(Config.YOUTUBE_TYPE_PLAYLIST_VIDEO_POS, 0);

            videoItem = youTubeVideos.get(startPosition);
            videoIndex = startPosition;
            playVideo();
            break;
        default:
            Log.d(TAG, "Unknown command");
            break;
        }
    }

    /**
     * Initializes media sessions and receives media events
     */
    private void initMediaSessions() {
        PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(getApplicationContext(), 0,
                new Intent(Intent.ACTION_MEDIA_BUTTON), PendingIntent.FLAG_UPDATE_CURRENT);

        mSession = new MediaSessionCompat(getApplicationContext(), "simple player session", null,
                buttonReceiverIntent);

        try {
            mController = new MediaControllerCompat(getApplicationContext(), mSession.getSessionToken());

            mSession.setCallback(new MediaSessionCompat.Callback() {
                @Override
                public void onPlay() {
                    super.onPlay();
                    buildNotification(generateAction(android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE));
                }

                @Override
                public void onPause() {

                    super.onPause();
                    pauseVideo();
                    buildNotification(generateAction(android.R.drawable.ic_media_play, "Play", ACTION_PLAY));
                }

                @Override
                public void onSkipToNext() {
                    super.onSkipToNext();
                    playNext();
                    buildNotification(generateAction(android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE));
                }

                @Override
                public void onSkipToPrevious() {
                    super.onSkipToPrevious();
                    playPrevious();
                    buildNotification(generateAction(android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE));
                }

                @Override
                public void onStop() {
                    super.onStop();
                    stopPlayer();
                    //remove notification and stop service
                    NotificationManager notificationManager = (NotificationManager) getApplicationContext()
                            .getSystemService(Context.NOTIFICATION_SERVICE);
                    notificationManager.cancel(1);
                    Intent intent = new Intent(getApplicationContext(), BackgroundAudioService.class);
                    stopService(intent);
                }

                @Override
                public void onSetRating(RatingCompat rating) {
                    super.onSetRating(rating);
                }
            });
        } catch (RemoteException re) {
            re.printStackTrace();
        }
    }

    /**
     * Builds notification panel with buttons and info on it
     *
     * @param action Action to be applied
     */

    private void buildNotification(NotificationCompat.Action action) {

        final NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle();

        Intent intent = new Intent(getApplicationContext(), BackgroundAudioService.class);
        intent.setAction(ACTION_STOP);
        PendingIntent stopPendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);

        Intent clickIntent = new Intent(this, MainActivity.class);
        clickIntent.setAction(Intent.ACTION_MAIN);
        clickIntent.addCategory(Intent.CATEGORY_LAUNCHER);
        PendingIntent clickPendingIntent = PendingIntent.getActivity(this, 0, clickIntent, 0);

        builder = new NotificationCompat.Builder(this);
        builder.setSmallIcon(R.mipmap.ic_launcher);
        builder.setContentTitle(videoItem.getTitle());
        builder.setContentInfo(videoItem.getDuration());
        builder.setShowWhen(false);
        builder.setContentIntent(clickPendingIntent);
        builder.setDeleteIntent(stopPendingIntent);
        builder.setOngoing(false);
        builder.setSubText(videoItem.getViewCount());
        builder.setStyle(style);

        //load bitmap for largeScreen
        if (videoItem.getThumbnailURL() != null && !videoItem.getThumbnailURL().isEmpty()) {
            Picasso.with(this).load(videoItem.getThumbnailURL()).into(target);
        }

        builder.addAction(generateAction(android.R.drawable.ic_media_previous, "Previous", ACTION_PREVIOUS));
        builder.addAction(action);
        builder.addAction(generateAction(android.R.drawable.ic_media_next, "Next", ACTION_NEXT));
        style.setShowActionsInCompactView(0, 1, 2);

        NotificationManager notificationManager = (NotificationManager) getSystemService(
                Context.NOTIFICATION_SERVICE);
        notificationManager.notify(1, builder.build());

    }

    /**
     * Field which handles image loading
     */
    private Target target = new Target() {

        @Override
        public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) {
            updateNotificationLargeIcon(bitmap);
        }

        @Override
        public void onBitmapFailed(Drawable drawable) {
            Log.d(TAG, "Load bitmap... failed");
        }

        @Override
        public void onPrepareLoad(Drawable drawable) {
        }
    };

    /**
     * Updates only large icon in notification panel when bitmap is decoded
     *
     * @param bitmap
     */
    private void updateNotificationLargeIcon(Bitmap bitmap) {
        builder.setLargeIcon(bitmap);
        NotificationManager notificationManager = (NotificationManager) getSystemService(
                Context.NOTIFICATION_SERVICE);
        notificationManager.notify(1, builder.build());
    }

    /**
     * Generates specific action with parameters below
     *
     * @param icon
     * @param title
     * @param intentAction
     * @return
     */
    private NotificationCompat.Action generateAction(int icon, String title, String intentAction) {
        Intent intent = new Intent(getApplicationContext(), BackgroundAudioService.class);
        intent.setAction(intentAction);
        PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
        return new NotificationCompat.Action.Builder(icon, title, pendingIntent).build();
    }

    /**
     * Plays next video in playlist
     */
    private void playNext() {
        //if media type is video not playlist, just loop it
        if (mediaType == ItemType.YOUTUBE_MEDIA_TYPE_VIDEO) {
            restartVideo();
            return;
        }

        if (videoIndex < youTubeVideos.size() - 1) {
            videoIndex++;
        } else {
            videoIndex = 0;
        }

        videoItem = youTubeVideos.get(videoIndex);
        playVideo();
    }

    /**
     * Plays previous video in playlist
     */
    private void playPrevious() {
        //if media type is video not playlist, just loop it
        if (mediaType == ItemType.YOUTUBE_MEDIA_TYPE_VIDEO) {
            restartVideo();
            return;
        }

        if (videoIndex > 0) {
            videoIndex--;
        } else {
            videoIndex = youTubeVideos.size() - 1;
        }

        videoItem = youTubeVideos.get(videoIndex);
        playVideo();
    }

    /**
     * Plays video
     */
    private void playVideo() {
        playURL();
    }

    /**
     * Pauses video
     */
    private void pauseVideo() {
        if (mMediaPlayer != null && mMediaPlayer.isPlaying() && isPrepared) {
            mMediaPlayer.pause();
        }
    }

    /**
     * Resumes video
     */
    private void resumeVideo() {
        if (mMediaPlayer != null && !mMediaPlayer.isPlaying() && isPrepared) {
            mMediaPlayer.start();
        }
    }

    /**
     * Restarts video
     */
    private void restartVideo() {
        if (mMediaPlayer != null) {
            mMediaPlayer.seekTo(0);
            mMediaPlayer.prepareAsync();
        }
    }

    /**
     * Stops video
     */
    private void stopPlayer() {
        isPrepared = false;

        mMediaPlayer.stop();
        mMediaPlayer.release();
        mMediaPlayer = null;

        if (mWifiLock.isHeld())
            mWifiLock.release();
    }

    /**
     * Get the best available audio stream given connection type.
     * If the user is on wifi, then provide the best quality audio from video sources, if available.
     * Otherwise, use DASH audio streams to save on mobile data.
     *
     * @TODO make source configurable by the user.
     *
     * @param ytFiles Array of available streams
     * @return Audio stream with highest bitrate
     */
    @Nullable
    private YtFile findBestStream(SparseArray<YtFile> ytFiles) {
        final long start = System.nanoTime();

        final int[] mobileAllowed = { 251, 140, 171, 18, 250, 249, 36, 17 };
        final int[] wifiAllowed = { 22, 251, 140, 171, 43, 18, 250, 249, 36, 17 };

        final int[] allowedItags = network.networkType() == ConnectivityManager.TYPE_WIFI ? wifiAllowed
                : mobileAllowed;

        for (int itag : allowedItags) {
            final YtFile ytFile = ytFiles.get(itag);
            if (ytFile != null) {
                final long end = System.nanoTime();
                final long cost = end - start;
                Log.i(TAG, "Found best stream with itag " + ytFile.getFormat().getItag() + " in " + cost / 1000000.0
                        + "ms.");

                return ytFile;
            }
        }

        Log.e(TAG, "Unable to find audio stream from available candidates.");
        return null;
    }

    /**
     * Extracts link from youtube video ID, so mediaPlayer can play it
     */
    private void playURL() {
        final String youtubeLink = Config.YOUTUBE_BASE_URL + videoItem.getId();

        new YouTubeExtractor(this) {
            @Override
            protected void onExtractionComplete(SparseArray<YtFile> ytFiles, VideoMeta videoMeta) {
                onVideoStart(ytFiles);
            }
        }.execute(youtubeLink);
    }

    private void onVideoStart(SparseArray<YtFile> ytFiles) {
        YtFile ytFile;
        if (ytFiles == null || (ytFile = findBestStream(ytFiles)) == null) {
            // Something went wrong we got no urls. Always check this.
            Toast.makeText(YTApplication.getAppContext(), R.string.failed_playback, Toast.LENGTH_SHORT).show();

            playNext();
            buildNotification(generateAction(android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE));
            return;
        }

        final String id = videoItem.getId();
        final String title = videoItem.getTitle();
        final int bitrate = ytFile.getFormat().getAudioBitrate();
        final String message = "Starting: " + title + "(" + id + ")" + ", quality: " + bitrate + "kbps.";
        Log.d(TAG, message);

        try {
            if (mMediaPlayer != null) {
                mMediaPlayer.reset();
                isPrepared = false;

                // Make sure the media player will acquire a wake-lock while playing. If we don't do
                // that, the CPU might go to sleep while the song is playing, causing playback to stop.
                //
                // Remember that to use this, we have to declare the android.permission.WAKE_LOCK
                // permission in AndroidManifest.xml.
                mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
                mMediaPlayer.setDataSource(ytFile.getUrl());
                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
                mMediaPlayer.prepareAsync();

                if (!mWifiLock.isHeld())
                    mWifiLock.acquire();

                Toast.makeText(YTApplication.getAppContext(), message, Toast.LENGTH_SHORT).show();
            }
        } catch (IOException io) {
            io.printStackTrace();
        }
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        NotificationManager mNotificationManager = (NotificationManager) getSystemService(
                Context.NOTIFICATION_SERVICE);
        mNotificationManager.cancel(1);
    }

    @Override
    public void onCompletion(MediaPlayer _mediaPlayer) {
        if (mediaType == ItemType.YOUTUBE_MEDIA_TYPE_PLAYLIST) {
            playNext();
            buildNotification(generateAction(android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE));
        } else {
            restartVideo();
        }
    }

    @Override
    public void onPrepared(MediaPlayer mp) {
        isPrepared = true;
        mp.start();
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        isPrepared = false;
        playNext();

        Log.e(TAG, "Mediaplayer failed with code " + what + ", extra info code: " + extra + ".");
        return true;
    }
}