Java tutorial
/* * 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; } }