package de.danoeh.antennapod.core.util.playback; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.media.MediaPlayer; import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.TextView; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.core.service.playback.PlayerStatus; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; import rx.Observable; import rx.Subscription; import rx.android.schedulers.AndroidSchedulers; import rx.schedulers.Schedulers; /** * Communicates with the playback service. GUI classes should use this class to * control playback instead of communicating with the PlaybackService directly. */ public abstract class PlaybackController { private static final String TAG = "PlaybackController"; private static final int INVALID_TIME = -1; private final Activity activity; private PlaybackService playbackService; private Playable media; private PlayerStatus status; private final ScheduledThreadPoolExecutor schedExecutor; private static final int SCHED_EX_POOLSIZE = 1; private MediaPositionObserver positionObserver; private ScheduledFuture positionObserverFuture; private boolean mediaInfoLoaded = false; private boolean released = false; private Subscription serviceBinder; /** * True if controller should reinit playback service if 'pause' button is * pressed. */ private final boolean reinitOnPause; public PlaybackController(@NonNull Activity activity, boolean reinitOnPause) { this.activity = activity; this.reinitOnPause = reinitOnPause; schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, r -> { Thread t = new Thread(r); t.setPriority(Thread.MIN_PRIORITY); return t; }, new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { Log.w(TAG, "Rejected execution of runnable in schedExecutor"); } } ); } /** * Creates a new connection to the playbackService. Should be called in the * activity's onResume() method. */ public void init() { activity.registerReceiver(statusUpdate, new IntentFilter( PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); activity.registerReceiver(notificationReceiver, new IntentFilter( PlaybackService.ACTION_PLAYER_NOTIFICATION)); activity.registerReceiver(shutdownReceiver, new IntentFilter( PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); if (!released) { bindToService(); } else { throw new IllegalStateException("Can't call init() after release() has been called"); } checkMediaInfoLoaded(); } /** * Should be called if the PlaybackController is no longer needed, for * example in the activity's onStop() method. */ public void release() { Log.d(TAG, "Releasing PlaybackController"); try { activity.unregisterReceiver(statusUpdate); } catch (IllegalArgumentException e) { // ignore } try { activity.unregisterReceiver(notificationReceiver); } catch (IllegalArgumentException e) { // ignore } if(serviceBinder != null) { serviceBinder.unsubscribe(); } try { activity.unbindService(mConnection); } catch (IllegalArgumentException e) { // ignore } try { activity.unregisterReceiver(shutdownReceiver); } catch (IllegalArgumentException e) { // ignore } cancelPositionObserver(); schedExecutor.shutdownNow(); media = null; released = true; } /** * Should be called in the activity's onPause() method. */ public void pause() { mediaInfoLoaded = false; } /** * Tries to establish a connection to the PlaybackService. If it isn't * running, the PlaybackService will be started with the last played media * as the arguments of the launch intent. */ private void bindToService() { Log.d(TAG, "Trying to connect to service"); if(serviceBinder != null) { serviceBinder.unsubscribe(); } serviceBinder = Observable.fromCallable(this::getPlayLastPlayedMediaIntent) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(intent -> { boolean bound = false; if (!PlaybackService.started) { if (intent != null) { Log.d(TAG, "Calling start service"); activity.startService(intent); bound = activity.bindService(intent, mConnection, 0); } else { status = PlayerStatus.STOPPED; setupGUI(); handleStatus(); } } else { Log.d(TAG, "PlaybackService is running, trying to connect without start command."); bound = activity.bindService(new Intent(activity, PlaybackService.class), mConnection, 0); } Log.d(TAG, "Result for service binding: " + bound); }, error -> { Log.e(TAG, Log.getStackTraceString(error)); }); } /** * Returns an intent that starts the PlaybackService and plays the last * played media or null if no last played media could be found. */ private Intent getPlayLastPlayedMediaIntent() { Log.d(TAG, "Trying to restore last played media"); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( activity.getApplicationContext()); long currentlyPlayingMedia = PlaybackPreferences.getCurrentlyPlayingMedia(); if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { Playable media = PlayableUtils.createInstanceFromPreferences(activity, (int) currentlyPlayingMedia, prefs); if (media != null) { Intent serviceIntent = new Intent(activity, PlaybackService.class); serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); serviceIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, false); serviceIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false); boolean fileExists = media.localFileAvailable(); boolean lastIsStream = PlaybackPreferences.getCurrentEpisodeIsStream(); if (!fileExists && !lastIsStream && media instanceof FeedMedia) { DBTasks.notifyMissingFeedMediaFile(activity, (FeedMedia) media); } serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, lastIsStream || !fileExists); return serviceIntent; } } Log.d(TAG, "No last played media found"); return null; } private void setupPositionObserver() { if (positionObserverFuture == null || positionObserverFuture.isCancelled() || positionObserverFuture.isDone()) { Log.d(TAG, "Setting up position observer"); positionObserver = new MediaPositionObserver(); positionObserverFuture = schedExecutor.scheduleWithFixedDelay( positionObserver, MediaPositionObserver.WAITING_INTERVALL, MediaPositionObserver.WAITING_INTERVALL, TimeUnit.MILLISECONDS); } } private void cancelPositionObserver() { if (positionObserverFuture != null) { boolean result = positionObserverFuture.cancel(true); Log.d(TAG, "PositionObserver cancelled. Result: " + result); } } private final ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { if(service instanceof PlaybackService.LocalBinder) { playbackService = ((PlaybackService.LocalBinder) service).getService(); if (!released) { queryService(); Log.d(TAG, "Connection to Service established"); } else { Log.i(TAG, "Connection to playback service has been established, " + "but controller has already been released"); } } } @Override public void onServiceDisconnected(ComponentName name) { playbackService = null; Log.d(TAG, "Disconnected from Service"); } }; private final BroadcastReceiver statusUpdate = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received statusUpdate Intent."); if (isConnectedToPlaybackService()) { PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); status = info.playerStatus; media = info.playable; handleStatus(); } else { Log.w(TAG, "Couldn't receive status update: playbackService was null"); bindToService(); } } }; private final BroadcastReceiver notificationReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (!isConnectedToPlaybackService()) { bindToService(); return; } int type = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_TYPE, -1); int code = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_CODE, -1); if(code == -1 || type == -1) { Log.d(TAG, "Bad arguments. Won't handle intent"); return; } switch (type) { case PlaybackService.NOTIFICATION_TYPE_ERROR: handleError(code); break; case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE: float progress = ((float) code) / 100; onBufferUpdate(progress); break; case PlaybackService.NOTIFICATION_TYPE_RELOAD: cancelPositionObserver(); mediaInfoLoaded = false; queryService(); onReloadNotification(intent.getIntExtra( PlaybackService.EXTRA_NOTIFICATION_CODE, -1)); break; case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE: onSleepTimerUpdate(); break; case PlaybackService.NOTIFICATION_TYPE_BUFFER_START: onBufferStart(); break; case PlaybackService.NOTIFICATION_TYPE_BUFFER_END: onBufferEnd(); break; case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END: onPlaybackEnd(); break; case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE: onPlaybackSpeedChange(); break; case PlaybackService.NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED: onSetSpeedAbilityChanged(); break; case PlaybackService.NOTIFICATION_TYPE_SHOW_TOAST: postStatusMsg(code, true); } } }; private final BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (isConnectedToPlaybackService()) { if (TextUtils.equals(intent.getAction(), PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { release(); onShutdownNotification(); } } } }; public void setupGUI() {} public void onPositionObserverUpdate() {} public void onPlaybackSpeedChange() {} public void onSetSpeedAbilityChanged() {} public void onShutdownNotification() {} /** * Called when the currently displayed information should be refreshed. */ public void onReloadNotification(int code) {} public void onBufferStart() {} public void onBufferEnd() {} public void onBufferUpdate(float progress) {} public void onSleepTimerUpdate() {} public void handleError(int code) {} public void onPlaybackEnd() {} public void repeatHandleStatus() { if (status != null && playbackService != null) { handleStatus(); } } /** * Is called whenever the PlaybackService changes its status. This method * should be used to update the GUI or start/cancel background threads. */ private void handleStatus() { final int playResource; final int pauseResource; final CharSequence playText = activity.getString(R.string.play_label); final CharSequence pauseText = activity.getString(R.string.pause_label); if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO || PlaybackService.isCasting()) { TypedArray res = activity.obtainStyledAttributes(new int[]{ R.attr.av_play_big, R.attr.av_pause_big}); playResource = res.getResourceId(0, R.drawable.ic_play_arrow_grey600_36dp); pauseResource = res.getResourceId(1, R.drawable.ic_pause_grey600_36dp); res.recycle(); } else { playResource = R.drawable.ic_av_play_circle_outline_80dp; pauseResource = R.drawable.ic_av_pause_circle_outline_80dp; } Log.d(TAG, "status: " + status.toString()); switch (status) { case ERROR: postStatusMsg(R.string.player_error_msg, false); handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); break; case PAUSED: clearStatusMsg(); checkMediaInfoLoaded(); cancelPositionObserver(); onPositionObserverUpdate(); updatePlayButtonAppearance(playResource, playText); if (!PlaybackService.isCasting() && PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { setScreenOn(false); } break; case PLAYING: clearStatusMsg(); checkMediaInfoLoaded(); if (!PlaybackService.isCasting() && PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { onAwaitingVideoSurface(); setScreenOn(true); } setupPositionObserver(); updatePlayButtonAppearance(pauseResource, pauseText); break; case PREPARING: postStatusMsg(R.string.player_preparing_msg, false); checkMediaInfoLoaded(); if (playbackService != null) { if (playbackService.isStartWhenPrepared()) { updatePlayButtonAppearance(pauseResource, pauseText); } else { updatePlayButtonAppearance(playResource, playText); } } break; case STOPPED: postStatusMsg(R.string.player_stopped_msg, false); break; case PREPARED: checkMediaInfoLoaded(); postStatusMsg(R.string.player_ready_msg, false); updatePlayButtonAppearance(playResource, playText); break; case SEEKING: onPositionObserverUpdate(); postStatusMsg(R.string.player_seeking_msg, false); break; case INITIALIZED: checkMediaInfoLoaded(); clearStatusMsg(); updatePlayButtonAppearance(playResource, playText); break; } } private void checkMediaInfoLoaded() { mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo()); } private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) { ImageButton butPlay = getPlayButton(); if(butPlay != null) { butPlay.setImageResource(resource); butPlay.setContentDescription(contentDescription); } } public ImageButton getPlayButton() { return null; } public void postStatusMsg(int msg, boolean showToast) {} public void clearStatusMsg() {} public boolean loadMediaInfo() { return false; } public void onAwaitingVideoSurface() {} /** * Called when connection to playback service has been established or * information has to be refreshed */ private void queryService() { Log.d(TAG, "Querying service info"); if (playbackService != null) { PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); status = info.playerStatus; media = info.playable; /* if (media == null) { Log.w(TAG, "PlaybackService has no media object. Trying to restore last played media."); Intent serviceIntent = getPlayLastPlayedMediaIntent(); if (serviceIntent != null) { activity.startService(serviceIntent); } } */ onServiceQueried(); setupGUI(); handleStatus(); // make sure that new media is loaded if it's available mediaInfoLoaded = false; } else { Log.e(TAG, "queryService() was called without an existing connection to playbackservice"); } } public void onServiceQueried() {} /** * Should be used by classes which implement the OnSeekBarChanged interface. */ public float onSeekBarProgressChanged(SeekBar seekBar, int progress, boolean fromUser, TextView txtvPosition) { if (fromUser && playbackService != null && media != null) { float prog = progress / ((float) seekBar.getMax()); int duration = media.getDuration(); txtvPosition.setText(Converter .getDurationStringLong((int) (prog * duration))); return prog; } return 0; } /** * Should be used by classes which implement the OnSeekBarChanged interface. */ public void onSeekBarStartTrackingTouch(SeekBar seekBar) { // interrupt position Observer, restart later cancelPositionObserver(); } /** * Should be used by classes which implement the OnSeekBarChanged interface. */ public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { if (playbackService != null && media != null) { playbackService.seekTo((int) (prog * media.getDuration())); setupPositionObserver(); } } /** * Should be implemented by classes that show a video. The default implementation * does nothing * * @param enable True if the screen should be kept on, false otherwise */ protected void setScreenOn(boolean enable) { } public void playPause() { if (playbackService == null) { Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!"); return; } switch (status) { case PLAYING: playbackService.pause(true, reinitOnPause); break; case PAUSED: case PREPARED: playbackService.resume(); break; case PREPARING: playbackService.setStartWhenPrepared(!playbackService .isStartWhenPrepared()); if (reinitOnPause && !playbackService.isStartWhenPrepared()) { playbackService.reinit(); } break; case INITIALIZED: playbackService.setStartWhenPrepared(true); playbackService.prepare(); break; } } public boolean serviceAvailable() { return playbackService != null; } public int getPosition() { if (playbackService != null) { return playbackService.getCurrentPosition(); } else { return PlaybackService.INVALID_TIME; } } public int getDuration() { if (playbackService != null) { return playbackService.getDuration(); } else { return PlaybackService.INVALID_TIME; } } public Playable getMedia() { return media; } public boolean sleepTimerActive() { return playbackService != null && playbackService.sleepTimerActive(); } public boolean sleepTimerNotActive() { return playbackService != null && !playbackService.sleepTimerActive(); } public void disableSleepTimer() { if (playbackService != null) { playbackService.disableSleepTimer(); } } public long getSleepTimerTimeLeft() { if (playbackService != null) { return playbackService.getSleepTimerTimeLeft(); } else { return INVALID_TIME; } } public void setSleepTimer(long time, boolean shakeToReset, boolean vibrate) { if (playbackService != null) { playbackService.setSleepTimer(time, shakeToReset, vibrate); } } public void seekToChapter(Chapter chapter) { if (playbackService != null) { playbackService.seekToChapter(chapter); } } public void seekTo(int time) { if (playbackService != null) { playbackService.seekTo(time); } } public void setVideoSurface(SurfaceHolder holder) { if (playbackService != null) { playbackService.setVideoSurface(holder); } } public PlayerStatus getStatus() { return status; } public boolean canSetPlaybackSpeed() { return org.antennapod.audio.MediaPlayer.isPrestoLibraryInstalled(activity.getApplicationContext()) || UserPreferences.useSonic() || Build.VERSION.SDK_INT >= 23 || playbackService != null && playbackService.canSetSpeed(); } public void setPlaybackSpeed(float speed) { if (playbackService != null) { playbackService.setSpeed(speed); } } public void setVolume(float leftVolume, float rightVolume) { if (playbackService != null) { playbackService.setVolume(leftVolume, rightVolume); } } public float getCurrentPlaybackSpeedMultiplier() { if (canSetPlaybackSpeed()) { return playbackService.getCurrentPlaybackSpeed(); } else { return -1; } } public boolean canDownmix() { return playbackService != null && playbackService.canDownmix(); } public void setDownmix(boolean enable) { if(playbackService != null) { playbackService.setDownmix(enable); } } public boolean isPlayingVideoLocally() { return playbackService != null && PlaybackService.getCurrentMediaType() == MediaType.VIDEO && !PlaybackService.isCasting(); } public Pair<Integer, Integer> getVideoSize() { if (playbackService != null) { return playbackService.getVideoSize(); } else { return null; } } /** * Returns true if PlaybackController can communicate with the playback * service. */ private boolean isConnectedToPlaybackService() { return playbackService != null; } public void notifyVideoSurfaceAbandoned() { if (playbackService != null) { playbackService.notifyVideoSurfaceAbandoned(); } } /** * Move service into INITIALIZED state if it's paused to save bandwidth */ public void reinitServiceIfPaused() { if (playbackService != null && playbackService.isStreaming() && !PlaybackService.isCasting() && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService.getStatus() == PlayerStatus.PREPARING && !playbackService.isStartWhenPrepared()))) { playbackService.reinit(); } } /** * Refreshes the current position of the media file that is playing. */ public class MediaPositionObserver implements Runnable { public static final int WAITING_INTERVALL = 1000; @Override public void run() { if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) { activity.runOnUiThread(PlaybackController.this::onPositionObserverUpdate); } } } }