/* * Copyright 2017 Mauricio Colli * BackgroundPlayer.java is part of NewPipe * * License: GPL-3.0+ * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.schabi.newpipe.player; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.os.Build; import android.os.IBinder; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.RemoteViews; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.playlist.PlayQueueItem; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ThemeHelper; import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString; /** * Base players joining the common properties * * @author mauriciocolli */ public final class BackgroundPlayer extends Service { private static final String TAG = "BackgroundPlayer"; private static final boolean DEBUG = BasePlayer.DEBUG; public static final String ACTION_CLOSE = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE"; public static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE"; public static final String ACTION_OPEN_CONTROLS = "org.schabi.newpipe.player.BackgroundPlayer.OPEN_CONTROLS"; public static final String ACTION_REPEAT = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT"; public static final String ACTION_PLAY_NEXT = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT"; public static final String ACTION_PLAY_PREVIOUS = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS"; private BasePlayerImpl basePlayerImpl; private LockManager lockManager; /*////////////////////////////////////////////////////////////////////////// // Service-Activity Binder //////////////////////////////////////////////////////////////////////////*/ private PlayerEventListener activityListener; private IBinder mBinder; /*////////////////////////////////////////////////////////////////////////// // Notification //////////////////////////////////////////////////////////////////////////*/ private static final int NOTIFICATION_ID = 123789; private NotificationManager notificationManager; private NotificationCompat.Builder notBuilder; private RemoteViews notRemoteView; private RemoteViews bigNotRemoteView; private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha"; private boolean shouldUpdateOnProgress; /*////////////////////////////////////////////////////////////////////////// // Service's LifeCycle //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreate() { if (DEBUG) Log.d(TAG, "onCreate() called"); notificationManager = ((NotificationManager) getSystemService(NOTIFICATION_SERVICE)); lockManager = new LockManager(this); ThemeHelper.setTheme(this); basePlayerImpl = new BasePlayerImpl(this); basePlayerImpl.setup(); mBinder = new PlayerServiceBinder(basePlayerImpl); shouldUpdateOnProgress = true; } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); basePlayerImpl.handleIntent(intent); return START_NOT_STICKY; } @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "destroy() called"); onClose(); } @Override public IBinder onBind(Intent intent) { return mBinder; } /*////////////////////////////////////////////////////////////////////////// // Actions //////////////////////////////////////////////////////////////////////////*/ public void openControl(final Context context) { final Intent intent = new Intent(context, BackgroundPlayerActivity.class); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } context.startActivity(intent); context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); } private void onClose() { if (DEBUG) Log.d(TAG, "onClose() called"); if (lockManager != null) { lockManager.releaseWifiAndCpu(); } if (basePlayerImpl != null) { basePlayerImpl.stopActivityBinding(); basePlayerImpl.destroy(); } if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID); mBinder = null; basePlayerImpl = null; lockManager = null; stopForeground(true); stopSelf(); } private void onScreenOnOff(boolean on) { if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]"); shouldUpdateOnProgress = on; basePlayerImpl.triggerProgressUpdate(); } /*////////////////////////////////////////////////////////////////////////// // Notification //////////////////////////////////////////////////////////////////////////*/ private void resetNotification() { notBuilder = createNotification(); } private NotificationCompat.Builder createNotification() { notRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification); bigNotRemoteView = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded); setupNotification(notRemoteView); setupNotification(bigNotRemoteView); NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) .setOngoing(true) .setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setCustomContentView(notRemoteView) .setCustomBigContentView(bigNotRemoteView); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(NotificationCompat.PRIORITY_MAX); return builder; } private void setupNotification(RemoteViews remoteViews) { remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle()); remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName()); remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationStop, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_CLOSE), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationContent, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_OPEN_CONTROLS), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationRepeat, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_REPEAT), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationFRewind, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PREVIOUS), PendingIntent.FLAG_UPDATE_CURRENT)); remoteViews.setOnClickPendingIntent(R.id.notificationFForward, PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_NEXT), PendingIntent.FLAG_UPDATE_CURRENT)); setRepeatModeIcon(remoteViews, basePlayerImpl.getRepeatMode()); } /** * Updates the notification, and the play/pause button in it. * Used for changes on the remoteView * * @param drawableId if != -1, sets the drawable with that id on the play/pause button */ private synchronized void updateNotification(int drawableId) { //if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]"); if (notBuilder == null) return; if (drawableId != -1) { if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId); } notificationManager.notify(NOTIFICATION_ID, notBuilder.build()); } private void setControlsOpacity(@IntRange(from = 0, to = 255) int opacity) { if (notRemoteView != null) notRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity); if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity); if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity); } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ private void setRepeatModeIcon(final RemoteViews remoteViews, final int repeatMode) { final String methodName = "setImageResource"; switch (repeatMode) { case Player.REPEAT_MODE_OFF: remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_off); break; case Player.REPEAT_MODE_ONE: remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_one); break; case Player.REPEAT_MODE_ALL: remoteViews.setInt(R.id.notificationRepeat, methodName, R.drawable.exo_controls_repeat_all); break; } } ////////////////////////////////////////////////////////////////////////// protected class BasePlayerImpl extends BasePlayer { BasePlayerImpl(Context context) { super(context); } @Override public void handleIntent(final Intent intent) { super.handleIntent(intent); resetNotification(); if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false); startForeground(NOTIFICATION_ID, notBuilder.build()); } @Override public void initThumbnail(final String url) { resetNotification(); if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail); updateNotification(-1); super.initThumbnail(url); } @Override public void onThumbnailReceived(Bitmap thumbnail) { super.onThumbnailReceived(thumbnail); if (thumbnail != null) { // rebuild notification here since remote view does not release bitmaps, causing memory leaks resetNotification(); if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail); updateNotification(-1); } } @Override public void onPrepared(boolean playWhenReady) { super.onPrepared(playWhenReady); simpleExoPlayer.setVolume(1f); } @Override public void onShuffleClicked() { super.onShuffleClicked(); updatePlayback(); } @Override public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) { updateProgress(currentProgress, duration, bufferPercent); if (!shouldUpdateOnProgress) return; resetNotification(); if (bigNotRemoteView != null) { bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration)); } if (notRemoteView != null) { notRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false); } updateNotification(-1); } @Override public void onPlayPrevious() { super.onPlayPrevious(); triggerProgressUpdate(); } @Override public void onPlayNext() { super.onPlayNext(); triggerProgressUpdate(); } @Override public void destroy() { super.destroy(); if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, null); if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, null); } /*////////////////////////////////////////////////////////////////////////// // ExoPlayer Listener //////////////////////////////////////////////////////////////////////////*/ @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { super.onPlaybackParametersChanged(playbackParameters); updatePlayback(); } @Override public void onLoadingChanged(boolean isLoading) { // Disable default behavior } @Override public void onRepeatModeChanged(int i) { resetNotification(); updateNotification(-1); updatePlayback(); } /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ @Override public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) { super.sync(item, info); resetNotification(); updateNotification(-1); updateMetadata(); } @Override public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) { final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams); if (index < 0) return null; final AudioStream audio = info.audio_streams.get(index); return buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format)); } @Override public void shutdown() { super.shutdown(); onClose(); } /*////////////////////////////////////////////////////////////////////////// // Activity Event Listener //////////////////////////////////////////////////////////////////////////*/ /*package-private*/ void setActivityListener(PlayerEventListener listener) { activityListener = listener; updateMetadata(); updatePlayback(); triggerProgressUpdate(); } /*package-private*/ void removeActivityListener(PlayerEventListener listener) { if (activityListener == listener) { activityListener = null; } } private void updateMetadata() { if (activityListener != null && currentInfo != null) { activityListener.onMetadataUpdate(currentInfo); } } private void updatePlayback() { if (activityListener != null && simpleExoPlayer != null && playQueue != null) { activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), getPlaybackParameters()); } } private void updateProgress(int currentProgress, int duration, int bufferPercent) { if (activityListener != null) { activityListener.onProgressUpdate(currentProgress, duration, bufferPercent); } } private void stopActivityBinding() { if (activityListener != null) { activityListener.onServiceStopped(); activityListener = null; } } /*////////////////////////////////////////////////////////////////////////// // Broadcast Receiver //////////////////////////////////////////////////////////////////////////*/ @Override protected void setupBroadcastReceiver(IntentFilter intentFilter) { super.setupBroadcastReceiver(intentFilter); intentFilter.addAction(ACTION_CLOSE); intentFilter.addAction(ACTION_PLAY_PAUSE); intentFilter.addAction(ACTION_OPEN_CONTROLS); intentFilter.addAction(ACTION_REPEAT); intentFilter.addAction(ACTION_PLAY_PREVIOUS); intentFilter.addAction(ACTION_PLAY_NEXT); intentFilter.addAction(Intent.ACTION_SCREEN_ON); intentFilter.addAction(Intent.ACTION_SCREEN_OFF); intentFilter.addAction(Intent.ACTION_HEADSET_PLUG); } @Override public void onBroadcastReceived(Intent intent) { super.onBroadcastReceived(intent); if (intent == null || intent.getAction() == null) return; if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]"); switch (intent.getAction()) { case ACTION_CLOSE: onClose(); break; case ACTION_PLAY_PAUSE: onVideoPlayPause(); break; case ACTION_OPEN_CONTROLS: openControl(getApplicationContext()); break; case ACTION_REPEAT: onRepeatClicked(); break; case ACTION_PLAY_NEXT: onPlayNext(); break; case ACTION_PLAY_PREVIOUS: onPlayPrevious(); break; case Intent.ACTION_SCREEN_ON: onScreenOnOff(true); break; case Intent.ACTION_SCREEN_OFF: onScreenOnOff(false); break; } } /*////////////////////////////////////////////////////////////////////////// // States //////////////////////////////////////////////////////////////////////////*/ @Override public void changeState(int state) { super.changeState(state); updatePlayback(); } @Override public void onBlocked() { super.onBlocked(); setControlsOpacity(77); updateNotification(-1); } @Override public void onPlaying() { super.onPlaying(); setControlsOpacity(255); updateNotification(R.drawable.ic_pause_white); lockManager.acquireWifiAndCpu(); } @Override public void onPaused() { super.onPaused(); updateNotification(R.drawable.ic_play_arrow_white); if (isProgressLoopRunning()) stopProgressLoop(); lockManager.releaseWifiAndCpu(); } @Override public void onCompleted() { super.onCompleted(); setControlsOpacity(255); resetNotification(); if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false); updateNotification(R.drawable.ic_replay_white); lockManager.releaseWifiAndCpu(); } } }