AntennaPod/playback/service/src/main/java/de/danoeh/antennapod/playback/service/PlaybackController.java

481 lines
16 KiB
Java

package de.danoeh.antennapod.playback.service;
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.os.IBinder;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.playback.SpeedChangedEvent;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.ui.episodes.PlaybackSpeedUtils;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.Collections;
import java.util.List;
/**
* 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 final Activity activity;
private PlaybackService playbackService;
private Playable media;
private PlayerStatus status = PlayerStatus.STOPPED;
private boolean mediaInfoLoaded = false;
private boolean released = false;
private boolean initialized = false;
private boolean eventsRegistered = false;
private long loadedFeedMedia = -1;
public PlaybackController(@NonNull Activity activity) {
this.activity = activity;
}
/**
* Creates a new connection to the playbackService.
*/
public synchronized void init() {
if (!eventsRegistered) {
EventBus.getDefault().register(this);
eventsRegistered = true;
}
if (PlaybackService.isRunning) {
initServiceRunning();
} else {
updatePlayButtonShowsPlay(true);
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(PlaybackServiceEvent event) {
if (event.action == PlaybackServiceEvent.Action.SERVICE_STARTED) {
init();
}
}
private synchronized void initServiceRunning() {
if (initialized) {
return;
}
initialized = true;
ContextCompat.registerReceiver(activity, statusUpdate, new IntentFilter(
PlaybackService.ACTION_PLAYER_STATUS_CHANGED), ContextCompat.RECEIVER_NOT_EXPORTED);
ContextCompat.registerReceiver(activity, notificationReceiver, new IntentFilter(
PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION), ContextCompat.RECEIVER_NOT_EXPORTED);
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
}
unbind();
media = null;
released = true;
if (eventsRegistered) {
EventBus.getDefault().unregister(this);
eventsRegistered = false;
}
}
private void unbind() {
try {
activity.unbindService(mConnection);
} catch (IllegalArgumentException e) {
// ignore
}
initialized = false;
}
/**
* 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 (!PlaybackService.isRunning) {
throw new IllegalStateException("Trying to bind but service is not running");
}
boolean bound = activity.bindService(new Intent(activity, PlaybackService.class), mConnection, 0);
Log.d(TAG, "Result for service binding: " + bound);
}
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;
initialized = false;
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 (playbackService != null) {
PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo();
status = info.getPlayerStatus();
media = info.getPlayable();
handleStatus();
} else {
Log.w(TAG, "Couldn't receive status update: playbackService was null");
if (PlaybackService.isRunning) {
bindToService();
} else {
status = PlayerStatus.STOPPED;
handleStatus();
}
}
}
};
private final BroadcastReceiver notificationReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int type = intent.getIntExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_TYPE, -1);
int code = intent.getIntExtra(PlaybackServiceInterface.EXTRA_NOTIFICATION_CODE, -1);
if (code == -1 || type == -1) {
Log.d(TAG, "Bad arguments. Won't handle intent");
return;
}
switch (type) {
case PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD:
if (playbackService == null && PlaybackService.isRunning) {
bindToService();
return;
}
mediaInfoLoaded = false;
queryService();
break;
case PlaybackServiceInterface.NOTIFICATION_TYPE_PLAYBACK_END:
onPlaybackEnd();
break;
}
}
};
public void onPlaybackEnd() {
}
/**
* 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() {
Log.d(TAG, "status: " + status.toString());
checkMediaInfoLoaded();
switch (status) {
case PLAYING:
updatePlayButtonShowsPlay(false);
break;
case PREPARING:
if (playbackService != null) {
updatePlayButtonShowsPlay(!playbackService.isStartWhenPrepared());
}
break;
case PAUSED:
case PREPARED: // Fall-through
case STOPPED: // Fall-through
case INITIALIZED: // Fall-through
updatePlayButtonShowsPlay(true);
break;
default:
break;
}
}
private void checkMediaInfoLoaded() {
if (!mediaInfoLoaded || loadedFeedMedia != PlaybackPreferences.getCurrentlyPlayingFeedMediaId()) {
loadedFeedMedia = PlaybackPreferences.getCurrentlyPlayingFeedMediaId();
loadMediaInfo();
}
mediaInfoLoaded = true;
}
protected void updatePlayButtonShowsPlay(boolean showPlay) {
}
public abstract void loadMediaInfo();
/**
* 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.getPlayerStatus();
media = info.getPlayable();
// make sure that new media is loaded if it's available
mediaInfoLoaded = false;
handleStatus();
} else {
Log.e(TAG,
"queryService() was called without an existing connection to playbackservice");
}
}
public void playPause() {
if (playbackService == null) {
new PlaybackServiceStarter(activity, media).start();
Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!");
return;
}
switch (status) {
case PLAYING:
playbackService.pause(true, false);
break;
case PAUSED:
case PREPARED:
playbackService.resume();
break;
case PREPARING:
playbackService.setStartWhenPrepared(!playbackService.isStartWhenPrepared());
break;
case INITIALIZED:
playbackService.setStartWhenPrepared(true);
playbackService.prepare();
break;
default:
new PlaybackServiceStarter(activity, media)
.callEvenIfRunning(true)
.start();
Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown");
break;
}
}
public int getPosition() {
if (playbackService != null) {
return playbackService.getCurrentPosition();
} else if (getMedia() != null) {
return getMedia().getPosition();
} else {
return Playable.INVALID_TIME;
}
}
public int getDuration() {
if (playbackService != null) {
return playbackService.getDuration();
} else if (getMedia() != null) {
return getMedia().getDuration();
} else {
return Playable.INVALID_TIME;
}
}
public Playable getMedia() {
if (media == null) {
media = DBReader.getFeedMedia(PlaybackPreferences.getCurrentlyPlayingFeedMediaId());
}
return media;
}
public boolean sleepTimerActive() {
return playbackService != null && playbackService.sleepTimerActive();
}
public void disableSleepTimer() {
if (playbackService != null) {
playbackService.disableSleepTimer();
}
}
public long getSleepTimerTimeLeft() {
if (playbackService != null) {
return playbackService.getSleepTimerTimeLeft();
} else {
return Playable.INVALID_TIME;
}
}
public void extendSleepTimer(long extendTime) {
long timeLeft = getSleepTimerTimeLeft();
if (playbackService != null && timeLeft != Playable.INVALID_TIME) {
setSleepTimer(timeLeft + extendTime);
}
}
public void setSleepTimer(long time) {
if (playbackService != null) {
playbackService.setSleepTimer(time);
}
}
public void seekTo(int time) {
if (playbackService != null) {
playbackService.seekTo(time);
} else if (getMedia() instanceof FeedMedia) {
FeedMedia media = (FeedMedia) getMedia();
media.setPosition(time);
DBWriter.setFeedItem(media.getItem());
EventBus.getDefault().post(new PlaybackPositionEvent(time, getMedia().getDuration()));
}
}
public void setVideoSurface(SurfaceHolder holder) {
if (playbackService != null) {
playbackService.setVideoSurface(holder);
}
}
public PlayerStatus getStatus() {
return status;
}
public void setPlaybackSpeed(float speed) {
if (playbackService != null) {
playbackService.setSpeed(speed);
} else {
EventBus.getDefault().post(new SpeedChangedEvent(speed));
}
}
public void setSkipSilence(boolean skipSilence) {
if (playbackService != null) {
playbackService.setSkipSilence(skipSilence);
}
}
public float getCurrentPlaybackSpeedMultiplier() {
if (playbackService != null) {
return playbackService.getCurrentPlaybackSpeed();
} else {
return PlaybackSpeedUtils.getCurrentPlaybackSpeed(getMedia());
}
}
public boolean getCurrentPlaybackSkipSilence() {
if (playbackService != null) {
return playbackService.getCurrentSkipSilence();
} else {
return PlaybackSpeedUtils.getCurrentSkipSilencePreference(getMedia())
== FeedPreferences.SkipSilence.AGGRESSIVE;
}
}
public List<String> getAudioTracks() {
if (playbackService == null) {
return Collections.emptyList();
}
return playbackService.getAudioTracks();
}
public int getSelectedAudioTrack() {
if (playbackService == null) {
return -1;
}
return playbackService.getSelectedAudioTrack();
}
public void setAudioTrack(int track) {
if (playbackService != null) {
playbackService.setAudioTrack(track);
}
}
public boolean isPlayingVideoLocally() {
if (PlaybackService.isCasting()) {
return false;
} else if (playbackService != null) {
return PlaybackService.getCurrentMediaType() == MediaType.VIDEO;
} else {
return getMedia() != null && getMedia().getMediaType() == MediaType.VIDEO;
}
}
public Pair<Integer, Integer> getVideoSize() {
if (playbackService != null) {
return playbackService.getVideoSize();
} else {
return null;
}
}
public void notifyVideoSurfaceAbandoned() {
if (playbackService != null) {
playbackService.notifyVideoSurfaceAbandoned();
}
}
public boolean isStreaming() {
return playbackService != null && playbackService.isStreaming();
}
}