/* * Copyright 2017 Mauricio Colli * 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.Service; import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import android.util.DisplayMetrics; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import org.schabi.newpipe.R; import org.schabi.newpipe.util.ThemeHelper; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; /** * One service for all players. * * @author mauriciocolli */ public final class MainPlayer extends Service { private static final String TAG = "MainPlayer"; private static final boolean DEBUG = BasePlayer.DEBUG; private VideoPlayerImpl playerImpl; private WindowManager windowManager; private final IBinder mBinder = new MainPlayer.LocalBinder(); public enum PlayerType { VIDEO, AUDIO, POPUP } /*////////////////////////////////////////////////////////////////////////// // Notification //////////////////////////////////////////////////////////////////////////*/ static final String ACTION_CLOSE = "org.schabi.newpipe.player.MainPlayer.CLOSE"; static final String ACTION_PLAY_PAUSE = "org.schabi.newpipe.player.MainPlayer.PLAY_PAUSE"; static final String ACTION_OPEN_CONTROLS = "org.schabi.newpipe.player.MainPlayer.OPEN_CONTROLS"; static final String ACTION_REPEAT = "org.schabi.newpipe.player.MainPlayer.REPEAT"; static final String ACTION_PLAY_NEXT = "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT"; static final String ACTION_PLAY_PREVIOUS = "org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS"; static final String ACTION_FAST_REWIND = "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND"; static final String ACTION_FAST_FORWARD = "org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD"; static final String ACTION_BUFFERING = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_BUFFERING"; static final String ACTION_SHUFFLE = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_SHUFFLE"; static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource"; /*////////////////////////////////////////////////////////////////////////// // Service's LifeCycle //////////////////////////////////////////////////////////////////////////*/ @Override public void onCreate() { if (DEBUG) { Log.d(TAG, "onCreate() called"); } assureCorrectAppLanguage(this); windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); ThemeHelper.setTheme(this); createView(); } private void createView() { final View layout = View.inflate(this, R.layout.player, null); playerImpl = new VideoPlayerImpl(this); playerImpl.setup(layout); playerImpl.shouldUpdateOnProgress = true; } @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { if (DEBUG) { Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]"); } if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) && playerImpl.playQueue == null) { // Player is not working, no need to process media button's action return START_NOT_STICKY; } if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) || intent.getStringExtra(VideoPlayer.PLAY_QUEUE_KEY) != null) { showNotificationAndStartForeground(); } playerImpl.handleIntent(intent); if (playerImpl.mediaSessionManager != null) { playerImpl.mediaSessionManager.handleMediaButtonIntent(intent); } return START_NOT_STICKY; } public void stop(final boolean autoplayEnabled) { if (DEBUG) { Log.d(TAG, "stop() called"); } if (playerImpl.getPlayer() != null) { playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady(); // Releases wifi & cpu, disables keepScreenOn, etc. if (!autoplayEnabled) { playerImpl.onPause(); } // We can't just pause the player here because it will make transition // from one stream to a new stream not smooth playerImpl.getPlayer().stop(false); playerImpl.setRecovery(); // Android TV will handle back button in case controls will be visible // (one more additional unneeded click while the player is hidden) playerImpl.hideControls(0, 0); // Notification shows information about old stream but if a user selects // a stream from backStack it's not actual anymore // So we should hide the notification at all. // When autoplay enabled such notification flashing is annoying so skip this case if (!autoplayEnabled) { stopForeground(true); } } } @Override public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); onDestroy(); // Unload from memory completely Runtime.getRuntime().halt(0); } @Override public void onDestroy() { if (DEBUG) { Log.d(TAG, "destroy() called"); } onClose(); } @Override protected void attachBaseContext(final Context base) { super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); } @Override public IBinder onBind(final Intent intent) { return mBinder; } /*////////////////////////////////////////////////////////////////////////// // Actions //////////////////////////////////////////////////////////////////////////*/ private void onClose() { if (DEBUG) { Log.d(TAG, "onClose() called"); } if (playerImpl != null) { removeViewFromParent(); playerImpl.setRecovery(); playerImpl.savePlaybackState(); playerImpl.stopActivityBinding(); playerImpl.removePopupFromView(); playerImpl.destroy(); } NotificationUtil.getInstance().cancelNotification(); stopForeground(true); stopSelf(); } /*////////////////////////////////////////////////////////////////////////// // Utils //////////////////////////////////////////////////////////////////////////*/ boolean isLandscape() { // DisplayMetrics from activity context knows about MultiWindow feature // while DisplayMetrics from app context doesn't final DisplayMetrics metrics = (playerImpl != null && playerImpl.getParentActivity() != null) ? playerImpl.getParentActivity().getResources().getDisplayMetrics() : getResources().getDisplayMetrics(); return metrics.heightPixels < metrics.widthPixels; } public View getView() { if (playerImpl == null) { return null; } return playerImpl.getRootView(); } public void removeViewFromParent() { if (getView().getParent() != null) { if (playerImpl.getParentActivity() != null) { // This means view was added to fragment final ViewGroup parent = (ViewGroup) getView().getParent(); parent.removeView(getView()); } else { // This means view was added by windowManager for popup player windowManager.removeViewImmediate(getView()); } } } private void showNotificationAndStartForeground() { NotificationUtil.getInstance().createNotificationIfNeeded(playerImpl, true); NotificationUtil.getInstance().startForegroundServiceWithNotification(this); } public class LocalBinder extends Binder { public MainPlayer getService() { return MainPlayer.this; } public VideoPlayerImpl getPlayer() { return MainPlayer.this.playerImpl; } } }