From 3c033cc0fbaa4b1418cda330ac0287d735c621f2 Mon Sep 17 00:00:00 2001 From: Martin Fietz Date: Sat, 4 Jun 2016 01:36:25 +0200 Subject: Create one flavor with Google Cast support and one (free) without --- .../de/danoeh/antennapod/core/ClientConfig.java | 49 - .../danoeh/antennapod/core/cast/CastConsumer.java | 11 - .../danoeh/antennapod/core/cast/CastManager.java | 1766 ------------------- .../de/danoeh/antennapod/core/cast/CastUtils.java | 317 ---- .../antennapod/core/cast/DefaultCastConsumer.java | 10 - .../danoeh/antennapod/core/cast/RemoteMedia.java | 357 ---- .../cast/SwitchableMediaRouteActionProvider.java | 106 -- .../de/danoeh/antennapod/core/feed/FeedMedia.java | 568 ------- .../core/service/playback/PlaybackService.java | 1780 -------------------- .../core/service/playback/RemotePSMP.java | 592 ------- .../antennapod/core/util/playback/Playable.java | 246 --- 11 files changed, 5802 deletions(-) delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java (limited to 'core/src/main') diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java deleted file mode 100644 index 9bbccbb82..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.content.Context; - -import de.danoeh.antennapod.core.cast.CastManager; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.danoeh.antennapod.core.util.NetworkUtils; - -/** - * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables. - * Apps using the core module of AntennaPod should register implementations of all interfaces here. - */ -public class ClientConfig { - - /** - * Should be used when setting User-Agent header for HTTP-requests. - */ - public static String USER_AGENT; - - public static ApplicationCallbacks applicationCallbacks; - - public static DownloadServiceCallbacks downloadServiceCallbacks; - - public static PlaybackServiceCallbacks playbackServiceCallbacks; - - public static GpodnetCallbacks gpodnetCallbacks; - - public static FlattrCallbacks flattrCallbacks; - - public static DBTasksCallbacks dbTasksCallbacks; - - private static boolean initialized = false; - - public static synchronized void initialize(Context context) { - if(initialized) { - return; - } - PodDBAdapter.init(context); - UserPreferences.init(context); - UpdateManager.init(context); - PlaybackPreferences.init(context); - NetworkUtils.init(context); - CastManager.init(context); - initialized = true; - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java b/core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java deleted file mode 100644 index 213dd1875..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/cast/CastConsumer.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumer; - -public interface CastConsumer extends VideoCastConsumer{ - - /** - * Called when the stream's volume is changed. - */ - void onStreamVolumeChanged(double value, boolean isMute); -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java deleted file mode 100644 index 5b1fdab61..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/cast/CastManager.java +++ /dev/null @@ -1,1766 +0,0 @@ -/* - * Copyright (C) 2015 Google Inc. All Rights Reserved. - * - * 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. - * - * ------------------------------------------------------------------------ - * - * Changes made by Domingos Lopes - * - * original can be found at http://www.github.com/googlecast/CastCompanionLibrary-android - */ - -package de.danoeh.antennapod.core.cast; - -import android.content.Context; -import android.os.Build; -import android.support.v4.view.MenuItemCompat; -import android.support.v7.media.MediaRouter; -import android.util.Log; -import android.view.KeyEvent; -import android.view.MenuItem; - -import com.google.android.gms.cast.ApplicationMetadata; -import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.CastMediaControlIntent; -import com.google.android.gms.cast.CastStatusCodes; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.cast.MediaQueueItem; -import com.google.android.gms.cast.MediaStatus; -import com.google.android.gms.cast.RemoteMediaPlayer; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GoogleApiAvailability; -import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager; -import com.google.android.libraries.cast.companionlibrary.cast.CastConfiguration; -import com.google.android.libraries.cast.companionlibrary.cast.MediaQueue; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.OnFailedListener; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException; - -import org.json.JSONObject; - -import java.io.IOException; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.core.R; - -import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_PLAY; -import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_UNCHANGED; - -/** - * A subclass of {@link BaseCastManager} that is suitable for casting video contents (it - * also provides a single custom data channel/namespace if an out-of-band communication is - * needed). - *

- * Clients need to initialize this class by calling - * {@link #init(android.content.Context)} in the Application's - * {@code onCreate()} method. To access the (singleton) instance of this class, clients - * need to call {@link #getInstance()}. - *

This - * class manages various states of the remote cast device. Client applications, however, can - * complement the default behavior of this class by hooking into various callbacks that it provides - * (see {@link CastConsumer}). - * Since the number of these callbacks is usually much larger than what a single application might - * be interested in, there is a no-op implementation of this interface (see - * {@link DefaultCastConsumer}) that applications can subclass to override only those methods that - * they are interested in. Since this library depends on the cast functionalities provided by the - * Google Play services, the library checks to ensure that the right version of that service is - * installed. It also provides a simple static method {@code checkGooglePlayServices()} that clients - * can call at an early stage of their applications to provide a dialog for users if they need to - * update/activate their Google Play Services library. - * - * @see CastConfiguration - */ -public class CastManager extends BaseCastManager implements OnFailedListener { - public static final String TAG = "CastManager"; - - public static final String CAST_APP_ID = CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID; - - public static final double DEFAULT_VOLUME_STEP = 0.05; - public static final long DEFAULT_LIVE_STREAM_DURATION_MS = TimeUnit.HOURS.toMillis(2); - private double volumeStep = DEFAULT_VOLUME_STEP; - private MediaQueue mediaQueue; - private MediaStatus mediaStatus; - - private static CastManager INSTANCE; - private RemoteMediaPlayer remoteMediaPlayer; - private int state = MediaStatus.PLAYER_STATE_IDLE; - private int idleReason; - private final Set castConsumers = new CopyOnWriteArraySet<>(); - private long liveStreamDuration = DEFAULT_LIVE_STREAM_DURATION_MS; - private MediaQueueItem preLoadingItem; - - public static final int QUEUE_OPERATION_LOAD = 1; - public static final int QUEUE_OPERATION_INSERT_ITEMS = 2; - public static final int QUEUE_OPERATION_UPDATE_ITEMS = 3; - public static final int QUEUE_OPERATION_JUMP = 4; - public static final int QUEUE_OPERATION_REMOVE_ITEM = 5; - public static final int QUEUE_OPERATION_REMOVE_ITEMS = 6; - public static final int QUEUE_OPERATION_REORDER = 7; - public static final int QUEUE_OPERATION_MOVE = 8; - public static final int QUEUE_OPERATION_APPEND = 9; - public static final int QUEUE_OPERATION_NEXT = 10; - public static final int QUEUE_OPERATION_PREV = 11; - public static final int QUEUE_OPERATION_SET_REPEAT = 12; - - private CastManager(Context context, CastConfiguration castConfiguration) { - super(context, castConfiguration); - Log.d(TAG, "CastManager is instantiated"); - } - - public static synchronized CastManager init(Context context) { - if (INSTANCE == null) { - //TODO also setup dialog factory if necessary - CastConfiguration castConfiguration = new CastConfiguration.Builder(CAST_APP_ID) - .enableDebug() - .enableAutoReconnect() - .enableWifiReconnection() - .setLaunchOptions(true, Locale.getDefault()) - .build(); - Log.d(TAG, "New instance of CastManager is created"); - if (ConnectionResult.SUCCESS != GoogleApiAvailability.getInstance() - .isGooglePlayServicesAvailable(context)) { - Log.e(TAG, "Couldn't find the appropriate version of Google Play Services"); - //TODO check whether creating an instance without google play services installed actually gives an exception - } - INSTANCE = new CastManager(context, castConfiguration); - } - return INSTANCE; - } - - /** - * Returns a (singleton) instance of this class. Clients should call this method in order to - * get a hold of this singleton instance, only after it is initialized. If it is not initialized - * yet, an {@link IllegalStateException} will be thrown. - * - */ - public static CastManager getInstance() { - if (INSTANCE == null) { - String msg = "No CastManager instance was found, did you forget to initialize it?"; - Log.e(TAG, msg); - throw new IllegalStateException(msg); - } - return INSTANCE; - } - - /** - * Returns the active {@link RemoteMediaPlayer} instance. Since there are a number of media - * control APIs that this library do not provide a wrapper for, client applications can call - * those methods directly after obtaining an instance of the active {@link RemoteMediaPlayer}. - */ - public final RemoteMediaPlayer getRemoteMediaPlayer() { - return remoteMediaPlayer; - } - - /** - * Determines if the media that is loaded remotely is a live stream or not. - * - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public final boolean isRemoteStreamLive() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - MediaInfo info = getRemoteMediaInformation(); - return (info != null) && (info.getStreamType() == MediaInfo.STREAM_TYPE_LIVE); - } - - /* - * A simple check to make sure remoteMediaPlayer is not null - */ - private void checkRemoteMediaPlayerAvailable() throws NoConnectionException { - if (remoteMediaPlayer == null) { - throw new NoConnectionException(); - } - } - - /** - * Returns the url for the media that is currently playing on the remote device. If there is no - * connection, this will return null. - * - * @throws NoConnectionException If no connectivity to the device exists - * @throws TransientNetworkDisconnectionException If framework is still trying to recover from - * a possibly transient loss of network - */ - public String getRemoteMediaUrl() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - if (remoteMediaPlayer != null && remoteMediaPlayer.getMediaInfo() != null) { - MediaInfo info = remoteMediaPlayer.getMediaInfo(); - remoteMediaPlayer.getMediaStatus().getPlayerState(); - return info.getContentId(); - } - throw new NoConnectionException(); - } - - /** - * Indicates if the remote media is currently playing (or buffering). - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public boolean isRemoteMediaPlaying() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - return state == MediaStatus.PLAYER_STATE_BUFFERING - || state == MediaStatus.PLAYER_STATE_PLAYING; - } - - /** - * Returns true if the remote connected device is playing a movie. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public boolean isRemoteMediaPaused() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - return state == MediaStatus.PLAYER_STATE_PAUSED; - } - - /** - * Returns true only if there is a media on the remote being played, paused or - * buffered. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public boolean isRemoteMediaLoaded() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - return isRemoteMediaPaused() || isRemoteMediaPlaying(); - } - - /** - * Returns the {@link MediaInfo} for the current media - * - * @throws NoConnectionException If no connectivity to the device exists - * @throws TransientNetworkDisconnectionException If framework is still trying to recover from - * a possibly transient loss of network - */ - public MediaInfo getRemoteMediaInformation() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - return remoteMediaPlayer.getMediaInfo(); - } - - /** - * Gets the remote's system volume. It internally detects what type of volume is used. - * - * @throws NoConnectionException If no connectivity to the device exists - * @throws TransientNetworkDisconnectionException If framework is still trying to recover from - * a possibly transient loss of network - */ - public double getStreamVolume() throws TransientNetworkDisconnectionException, NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - return remoteMediaPlayer.getMediaStatus().getStreamVolume(); - } - - /** - * Sets the stream volume. - * - * @param volume Should be a value between 0 and 1, inclusive. - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - * @throws CastException If setting system volume fails - */ - public void setStreamVolume(double volume) throws CastException, - TransientNetworkDisconnectionException, NoConnectionException { - checkConnectivity(); - if (volume > 1.0) { - volume = 1.0; - } else if (volume < 0) { - volume = 0.0; - } - - RemoteMediaPlayer mediaPlayer = getRemoteMediaPlayer(); - if (mediaPlayer == null) { - throw new NoConnectionException(); - } - mediaPlayer.setStreamVolume(mApiClient, volume).setResultCallback( - (result) -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_setting_volume, - result.getStatus().getStatusCode()); - } else { - CastManager.this.onStreamVolumeChanged(); - } - }); - } - - /** - * Returns true if remote Stream is muted. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public boolean isStreamMute() throws TransientNetworkDisconnectionException, NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - return remoteMediaPlayer.getMediaStatus().isMute(); - } - - /** - * Returns true if remote device is muted. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public boolean isMute() throws TransientNetworkDisconnectionException, NoConnectionException { - return isStreamMute() || isDeviceMute(); - } - - /** - * Mutes or un-mutes the stream volume. - * - * @throws CastException - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void setStreamMute(boolean mute) throws CastException, TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - remoteMediaPlayer.setStreamMute(mApiClient, mute); - } - - /** - * Returns the duration of the media that is loaded, in milliseconds. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public long getMediaDuration() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - return remoteMediaPlayer.getStreamDuration(); - } - - /** - * Returns the time left (in milliseconds) of the current media. If there is no - * {@code RemoteMediaPlayer}, it returns -1. - * - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public long getMediaTimeRemaining() - throws TransientNetworkDisconnectionException, NoConnectionException { - checkConnectivity(); - if (remoteMediaPlayer == null) { - return -1; - } - return isRemoteStreamLive() ? liveStreamDuration : remoteMediaPlayer.getStreamDuration() - - remoteMediaPlayer.getApproximateStreamPosition(); - } - - /** - * Returns the current (approximate) position of the current media, in milliseconds. - * - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public long getCurrentMediaPosition() throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - checkRemoteMediaPlayerAvailable(); - return remoteMediaPlayer.getApproximateStreamPosition(); - } - - public int getApplicationStandbyState() throws IllegalStateException { - Log.d(TAG, "getApplicationStandbyState()"); - return Cast.CastApi.getStandbyState(mApiClient); - } - - private void onApplicationDisconnected(int errorCode) { - Log.d(TAG, "onApplicationDisconnected() reached with error code: " + errorCode); - mApplicationErrorCode = errorCode; - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationDisconnected(errorCode); - } - if (mMediaRouter != null) { - Log.d(TAG, "onApplicationDisconnected(): Cached RouteInfo: " + getRouteInfo()); - Log.d(TAG, "onApplicationDisconnected(): Selected RouteInfo: " - + mMediaRouter.getSelectedRoute()); - if (getRouteInfo() == null || mMediaRouter.getSelectedRoute().equals(getRouteInfo())) { - Log.d(TAG, "onApplicationDisconnected(): Setting route to default"); - mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute()); - } - } - onDeviceSelected(null /* CastDevice */, null /* RouteInfo */); - } - - private void onApplicationStatusChanged() { - if (!isConnected()) { - return; - } - try { - String appStatus = Cast.CastApi.getApplicationStatus(mApiClient); - Log.d(TAG, "onApplicationStatusChanged() reached: " + appStatus); - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationStatusChanged(appStatus); - } - } catch (IllegalStateException e) { - Log.e(TAG, "onApplicationStatusChanged()", e); - } - } - - private void onDeviceVolumeChanged() { - Log.d(TAG, "onDeviceVolumeChanged() reached"); - double volume; - try { - volume = getDeviceVolume(); - boolean isMute = isDeviceMute(); - for (CastConsumer consumer : castConsumers) { - consumer.onVolumeChanged(volume, isMute); - } - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Failed to get volume", e); - } - - } - - private void onStreamVolumeChanged() { - Log.d(TAG, "onStreamVolumeChanged() reached"); - double volume; - try { - volume = getStreamVolume(); - boolean isMute = isStreamMute(); - for (CastConsumer consumer : castConsumers) { - consumer.onStreamVolumeChanged(volume, isMute); - } - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Failed to get volume", e); - } - } - - @Override - protected void onApplicationConnected(ApplicationMetadata appMetadata, - String applicationStatus, String sessionId, boolean wasLaunched) { - Log.d(TAG, "onApplicationConnected() reached with sessionId: " + sessionId - + ", and mReconnectionStatus=" + mReconnectionStatus); - mApplicationErrorCode = NO_APPLICATION_ERROR; - if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) { - // we have tried to reconnect and successfully launched the app, so - // it is time to select the route and make the cast icon happy :-) - List routes = mMediaRouter.getRoutes(); - if (routes != null) { - String routeId = mPreferenceAccessor.getStringFromPreference(PREFS_KEY_ROUTE_ID); - for (MediaRouter.RouteInfo routeInfo : routes) { - if (routeId.equals(routeInfo.getId())) { - // found the right route - Log.d(TAG, "Found the correct route during reconnection attempt"); - mReconnectionStatus = RECONNECTION_STATUS_FINALIZED; - mMediaRouter.selectRoute(routeInfo); - break; - } - } - } - } - try { - //attachDataChannel(); - attachMediaChannel(); - mSessionId = sessionId; - // saving device for future retrieval; we only save the last session info - mPreferenceAccessor.saveStringToPreference(PREFS_KEY_SESSION_ID, mSessionId); - remoteMediaPlayer.requestStatus(mApiClient).setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_status_request, - result.getStatus().getStatusCode()); - } - }); - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationConnected(appMetadata, mSessionId, wasLaunched); - } - } catch (TransientNetworkDisconnectionException e) { - Log.e(TAG, "Failed to attach media/data channel due to network issues", e); - onFailed(R.string.cast_failed_no_connection_trans, NO_STATUS_CODE); - } catch (NoConnectionException e) { - Log.e(TAG, "Failed to attach media/data channel due to network issues", e); - onFailed(R.string.cast_failed_no_connection, NO_STATUS_CODE); - } - } - - /* - * (non-Javadoc) - * @see com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager - * #onConnectivityRecovered() - */ - @Override - public void onConnectivityRecovered() { - reattachMediaChannel(); - //reattachDataChannel(); - super.onConnectivityRecovered(); - } - - /* - * (non-Javadoc) - * @see com.google.android.gms.cast.CastClient.Listener#onApplicationStopFailed (int) - */ - @Override - public void onApplicationStopFailed(int errorCode) { - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationStopFailed(errorCode); - } - } - - @Override - public void onApplicationConnectionFailed(int errorCode) { - Log.d(TAG, "onApplicationConnectionFailed() reached with errorCode: " + errorCode); - mApplicationErrorCode = errorCode; - if (mReconnectionStatus == RECONNECTION_STATUS_IN_PROGRESS) { - if (errorCode == CastStatusCodes.APPLICATION_NOT_RUNNING) { - // while trying to re-establish session, we found out that the app is not running - // so we need to disconnect - mReconnectionStatus = RECONNECTION_STATUS_INACTIVE; - onDeviceSelected(null /* CastDevice */, null /* RouteInfo */); - } - } else { - for (CastConsumer consumer : castConsumers) { - consumer.onApplicationConnectionFailed(errorCode); - } - onDeviceSelected(null /* CastDevice */, null /* RouteInfo */); - if (mMediaRouter != null) { - Log.d(TAG, "onApplicationConnectionFailed(): Setting route to default"); - mMediaRouter.selectRoute(mMediaRouter.getDefaultRoute()); - } - } - } - - /** - * Loads a media. For this to succeed, you need to have successfully launched the application. - * - * @param media The media to be loaded - * @param autoPlay If true, playback starts after load - * @param position Where to start the playback (only used if autoPlay is true. - * Units is milliseconds. - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void loadMedia(MediaInfo media, boolean autoPlay, int position) - throws TransientNetworkDisconnectionException, NoConnectionException { - loadMedia(media, autoPlay, position, null); - } - - /** - * Loads a media. For this to succeed, you need to have successfully launched the application. - * - * @param media The media to be loaded - * @param autoPlay If true, playback starts after load - * @param position Where to start the playback (only used if autoPlay is true). - * Units is milliseconds. - * @param customData Optional {@link JSONObject} data to be passed to the cast device - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void loadMedia(MediaInfo media, boolean autoPlay, int position, JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - loadMedia(media, null, autoPlay, position, customData); - } - - /** - * Loads a media. For this to succeed, you need to have successfully launched the application. - * - * @param media The media to be loaded - * @param activeTracks An array containing the list of track IDs to be set active for this - * media upon a successful load - * @param autoPlay If true, playback starts after load - * @param position Where to start the playback (only used if autoPlay is true). - * Units is milliseconds. - * @param customData Optional {@link JSONObject} data to be passed to the cast device - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void loadMedia(MediaInfo media, final long[] activeTracks, boolean autoPlay, - int position, JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "loadMedia"); - checkConnectivity(); - if (media == null) { - return; - } - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to load a video with no active media session"); - throw new NoConnectionException(); - } - - Log.d(TAG, "remoteMediaPlayer.load() with media=" + media.getMetadata().getString(MediaMetadata.KEY_TITLE) - + ", position=" + position + ", autoplay=" + autoPlay); - remoteMediaPlayer.load(mApiClient, media, autoPlay, position, activeTracks, customData) - .setResultCallback(result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaLoadResult(result.getStatus().getStatusCode()); - } - }); - } - - /** - * Loads and optionally starts playback of a new queue of media items. - * - * @param items Array of items to load, in the order that they should be played. Must not be - * {@code null} or empty. - * @param startIndex The array index of the item in the {@code items} array that should be - * played first (i.e., it will become the currentItem).If {@code repeatMode} - * is {@link MediaStatus#REPEAT_MODE_REPEAT_OFF} playback will end when the - * last item in the array is played. - *

- * This may be useful for continuation scenarios where the user was already - * using the sender application and in the middle decides to cast. This lets - * the sender application avoid mapping between the local and remote queue - * positions and/or avoid issuing an extra request to update the queue. - *

- * This value must be less than the length of {@code items}. - * @param repeatMode The repeat playback mode for the queue. One of - * {@link MediaStatus#REPEAT_MODE_REPEAT_OFF}, - * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL}, - * {@link MediaStatus#REPEAT_MODE_REPEAT_SINGLE} and - * {@link MediaStatus#REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE}. - * @param customData Custom application-specific data to pass along with the request, may be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void queueLoad(final MediaQueueItem[] items, final int startIndex, final int repeatMode, - final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "queueLoad"); - checkConnectivity(); - if (items == null || items.length == 0) { - return; - } - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to queue one or more videos with no active media session"); - throw new NoConnectionException(); - } - Log.d(TAG, "remoteMediaPlayer.queueLoad() with " + items.length + "items, starting at " - + startIndex); - remoteMediaPlayer - .queueLoad(mApiClient, items, startIndex, repeatMode, customData) - .setResultCallback(result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_LOAD, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Inserts a list of new media items into the queue. - * - * @param itemsToInsert List of items to insert into the queue, in the order that they should be - * played. The itemId field of the items should be unassigned or the - * request will fail with an INVALID_PARAMS error. Must not be {@code null} - * or empty. - * @param insertBeforeItemId ID of the item that will be located immediately after the inserted - * list. If the value is {@link MediaQueueItem#INVALID_ITEM_ID} or - * invalid, the inserted list will be appended to the end of the - * queue. - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - * @throws IllegalArgumentException - */ - public void queueInsertItems(final MediaQueueItem[] itemsToInsert, final int insertBeforeItemId, - final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "queueInsertItems"); - checkConnectivity(); - if (itemsToInsert == null || itemsToInsert.length == 0) { - throw new IllegalArgumentException("items cannot be empty or null"); - } - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to insert into queue with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueInsertItems(mApiClient, itemsToInsert, insertBeforeItemId, customData) - .setResultCallback( - result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult( - QUEUE_OPERATION_INSERT_ITEMS, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Updates properties of a subset of the existing items in the media queue. - * - * @param itemsToUpdate List of queue items to be updated. The items will retain the existing - * order and will be fully replaced with the ones provided, including the - * media information. Any other items currently in the queue will remain - * unchanged. The tracks information can not change once the item is loaded - * (if the item is the currentItem). If any of the items does not exist it - * will be ignored. - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void queueUpdateItems(final MediaQueueItem[] itemsToUpdate, final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to update the queue with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueUpdateItems(mApiClient, itemsToUpdate, customData).setResultCallback( - result -> { - Log.d(TAG, "queueUpdateItems() " + result.getStatus() + result.getStatus() - .isSuccess()); - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_UPDATE_ITEMS, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Plays the item with {@code itemId} in the queue. - *

- * If {@code itemId} is not found in the queue, this method will report success without sending - * a request to the receiver. - * - * @param itemId The ID of the item to which to jump. - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - * @throws IllegalArgumentException - */ - public void queueJumpToItem(int itemId, final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException, - IllegalArgumentException { - checkConnectivity(); - if (itemId == MediaQueueItem.INVALID_ITEM_ID) { - throw new IllegalArgumentException("itemId is not valid"); - } - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to jump in a queue with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueJumpToItem(mApiClient, itemId, customData).setResultCallback( - result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_JUMP, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Removes a list of items from the queue. If the remaining queue is empty, the media session - * will be terminated. - * - * @param itemIdsToRemove The list of media item IDs to remove. Must not be {@code null} or - * empty. - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - * @throws IllegalArgumentException - */ - public void queueRemoveItems(final int[] itemIdsToRemove, final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException, - IllegalArgumentException { - Log.d(TAG, "queueRemoveItems"); - checkConnectivity(); - if (itemIdsToRemove == null || itemIdsToRemove.length == 0) { - throw new IllegalArgumentException("itemIds cannot be empty or null"); - } - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to remove items from queue with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueRemoveItems(mApiClient, itemIdsToRemove, customData).setResultCallback( - result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_REMOVE_ITEMS, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Removes the item with {@code itemId} from the queue. - *

- * If {@code itemId} is not found in the queue, this method will silently return without sending - * a request to the receiver. A {@code itemId} may not be in the queue because it wasn't - * originally in the queue, or it was removed by another sender. - * - * @param itemId The ID of the item to be removed. - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - * @throws IllegalArgumentException - */ - public void queueRemoveItem(final int itemId, final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException, - IllegalArgumentException { - Log.d(TAG, "queueRemoveItem"); - checkConnectivity(); - if (itemId == MediaQueueItem.INVALID_ITEM_ID) { - throw new IllegalArgumentException("itemId is invalid"); - } - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to remove an item from queue with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueRemoveItem(mApiClient, itemId, customData).setResultCallback( - result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_REMOVE_ITEM, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Reorder a list of media items in the queue. - * - * @param itemIdsToReorder The list of media item IDs to reorder, in the new order. Any other - * items currently in the queue will maintain their existing order. The - * list will be inserted just before the item specified by - * {@code insertBeforeItemId}, or at the end of the queue if - * {@code insertBeforeItemId} is {@link MediaQueueItem#INVALID_ITEM_ID}. - *

- * For example: - *

- * If insertBeforeItemId is not specified
- * Existing queue: "A","D","G","H","B","E"
- * itemIds: "D","H","B"
- * New Order: "A","G","E","D","H","B"
- *

- * If insertBeforeItemId is "A"
- * Existing queue: "A","D","G","H","B"
- * itemIds: "D","H","B"
- * New Order: "D","H","B","A","G","E"
- *

- * If insertBeforeItemId is "G"
- * Existing queue: "A","D","G","H","B"
- * itemIds: "D","H","B"
- * New Order: "A","D","H","B","G","E"
- *

- * If any of the items does not exist it will be ignored. - * Must not be {@code null} or empty. - * @param insertBeforeItemId ID of the item that will be located immediately after the reordered - * list. If set to {@link MediaQueueItem#INVALID_ITEM_ID}, the - * reordered list will be appended at the end of the queue. - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void queueReorderItems(final int[] itemIdsToReorder, final int insertBeforeItemId, - final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException, - IllegalArgumentException { - Log.d(TAG, "queueReorderItems"); - checkConnectivity(); - if (itemIdsToReorder == null || itemIdsToReorder.length == 0) { - throw new IllegalArgumentException("itemIdsToReorder cannot be empty or null"); - } - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to reorder items in a queue with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueReorderItems(mApiClient, itemIdsToReorder, insertBeforeItemId, customData) - .setResultCallback( - result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_REORDER, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Moves the item with {@code itemId} to a new position in the queue. - *

- * If {@code itemId} is not found in the queue, either because it wasn't there originally or it - * was removed by another sender before calling this function, this function will silently - * return without sending a request to the receiver. - * - * @param itemId The ID of the item to be moved. - * @param newIndex The new index of the item. If the value is negative, an error will be - * returned. If the value is out of bounds, or becomes out of bounds because the - * queue was shortened by another sender while this request is in progress, the - * item will be moved to the end of the queue. - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void queueMoveItemToNewIndex(int itemId, int newIndex, final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "queueMoveItemToNewIndex"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to mote item to new index with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueMoveItemToNewIndex(mApiClient, itemId, newIndex, customData) - .setResultCallback( - result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_MOVE, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Appends a new media item to the end of the queue. - * - * @param item The item to append. Must not be {@code null}. - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void queueAppendItem(MediaQueueItem item, final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "queueAppendItem"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to append item with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueAppendItem(mApiClient, item, customData) - .setResultCallback( - result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_APPEND, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Jumps to the next item in the queue. - * - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void queueNext(final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "queueNext"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to update the queue with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueNext(mApiClient, customData).setResultCallback( - result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_NEXT, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Jumps to the previous item in the queue. - * - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void queuePrev(final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "queuePrev"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to update the queue with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queuePrev(mApiClient, customData).setResultCallback( - result -> { - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_PREV, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Inserts an item in the queue and starts the playback of that newly inserted item. It is - * assumed that we are inserting before the "current item" - * - * @param item The item to be inserted - * @param insertBeforeItemId ID of the item that will be located immediately after the inserted - * and is assumed to be the "current item" - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - * @throws IllegalArgumentException - */ - public void queueInsertBeforeCurrentAndPlay(MediaQueueItem item, int insertBeforeItemId, - final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "queueInsertBeforeCurrentAndPlay"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to insert into queue with no active media session"); - throw new NoConnectionException(); - } - if (item == null || insertBeforeItemId == MediaQueueItem.INVALID_ITEM_ID) { - throw new IllegalArgumentException( - "item cannot be empty or insertBeforeItemId cannot be invalid"); - } - remoteMediaPlayer.queueInsertItems(mApiClient, new MediaQueueItem[]{item}, - insertBeforeItemId, customData).setResultCallback( - result -> { - if (result.getStatus().isSuccess()) { - - try { - queuePrev(customData); - } catch (TransientNetworkDisconnectionException | - NoConnectionException e) { - Log.e(TAG, "queuePrev() Failed to skip to previous", e); - } - } - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_INSERT_ITEMS, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Sets the repeat mode of the queue. - * - * @param repeatMode The repeat playback mode for the queue. - * @param customData Custom application-specific data to pass along with the request. May be - * {@code null}. - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void queueSetRepeatMode(final int repeatMode, final JSONObject customData) - throws TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "queueSetRepeatMode"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to update the queue with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer - .queueSetRepeatMode(mApiClient, repeatMode, customData).setResultCallback( - result -> { - if (!result.getStatus().isSuccess()) { - Log.d(TAG, "Failed with status: " + result.getStatus()); - } - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueOperationResult(QUEUE_OPERATION_SET_REPEAT, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Plays the loaded media. - * - * @param position Where to start the playback. Units is milliseconds. - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void play(int position) throws TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - Log.d(TAG, "attempting to play media at position " + position + " seconds"); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to play a video with no active media session"); - throw new NoConnectionException(); - } - seekAndPlay(position); - } - - /** - * Resumes the playback from where it was left (can be the beginning). - * - * @param customData Optional {@link JSONObject} data to be passed to the cast device - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void play(JSONObject customData) throws - TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "play(customData)"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to play a video with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer.play(mApiClient, customData) - .setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_to_play, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Resumes the playback from where it was left (can be the beginning). - * - * @throws CastException - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void play() throws CastException, TransientNetworkDisconnectionException, - NoConnectionException { - play(null); - } - - /** - * Stops the playback of media/stream - * - * @param customData Optional {@link JSONObject} - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void stop(JSONObject customData) throws - TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "stop()"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to stop a stream with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer.stop(mApiClient, customData).setResultCallback( - result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_to_stop, - result.getStatus().getStatusCode()); - } - } - ); - } - - /** - * Stops the playback of media/stream - * - * @throws CastException - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void stop() throws CastException, - TransientNetworkDisconnectionException, NoConnectionException { - stop(null); - } - - /** - * Pauses the playback. - * - * @throws CastException - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void pause() throws CastException, TransientNetworkDisconnectionException, - NoConnectionException { - pause(null); - } - - /** - * Pauses the playback. - * - * @param customData Optional {@link JSONObject} data to be passed to the cast device - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void pause(JSONObject customData) throws - TransientNetworkDisconnectionException, NoConnectionException { - Log.d(TAG, "attempting to pause media"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to pause a video with no active media session"); - throw new NoConnectionException(); - } - remoteMediaPlayer.pause(mApiClient, customData) - .setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_to_pause, - result.getStatus().getStatusCode()); - } - }); - } - - /** - * Seeks to the given point without changing the state of the player, i.e. after seek is - * completed, it resumes what it was doing before the start of seek. - * - * @param position in milliseconds - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void seek(int position) throws TransientNetworkDisconnectionException, - NoConnectionException { - Log.d(TAG, "attempting to seek media"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to seek a video with no active media session"); - throw new NoConnectionException(); - } - Log.d(TAG, "remoteMediaPlayer.seek() to position " + position); - remoteMediaPlayer.seek(mApiClient, - position, - RESUME_STATE_UNCHANGED). - setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode()); - } - }); - } - - /** - * Fast forwards the media by the given amount. If {@code lengthInMillis} is negative, it - * rewinds the media. - * - * @param lengthInMillis The amount to fast forward the media, given in milliseconds - * @throws TransientNetworkDisconnectionException - * @throws NoConnectionException - */ - public void forward(int lengthInMillis) throws TransientNetworkDisconnectionException, - NoConnectionException { - Log.d(TAG, "forward(): attempting to forward media by " + lengthInMillis); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to seek a video with no active media session"); - throw new NoConnectionException(); - } - long position = remoteMediaPlayer.getApproximateStreamPosition() + lengthInMillis; - seek((int) position); - } - - /** - * Seeks to the given point and starts playback regardless of the starting state. - * - * @param position in milliseconds - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void seekAndPlay(int position) throws TransientNetworkDisconnectionException, - NoConnectionException { - Log.d(TAG, "attempting to seek media"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - Log.e(TAG, "Trying to seekAndPlay a video with no active media session"); - throw new NoConnectionException(); - } - Log.d(TAG, "remoteMediaPlayer.seek() to position " + position + "and play"); - remoteMediaPlayer.seek(mApiClient, position, RESUME_STATE_PLAY) - .setResultCallback(result -> { - if (!result.getStatus().isSuccess()) { - onFailed(R.string.cast_failed_seek, result.getStatus().getStatusCode()); - } - }); - } - - /** - * Toggles the playback of the media. - * - * @throws CastException - * @throws NoConnectionException - * @throws TransientNetworkDisconnectionException - */ - public void togglePlayback() throws CastException, TransientNetworkDisconnectionException, - NoConnectionException { - checkConnectivity(); - boolean isPlaying = isRemoteMediaPlaying(); - if (isPlaying) { - pause(); - } else { - if (state == MediaStatus.PLAYER_STATE_IDLE - && idleReason == MediaStatus.IDLE_REASON_FINISHED) { - loadMedia(getRemoteMediaInformation(), true, 0); - } else { - play(); - } - } - } - - private void attachMediaChannel() throws TransientNetworkDisconnectionException, - NoConnectionException { - Log.d(TAG, "attachMediaChannel()"); - checkConnectivity(); - if (remoteMediaPlayer == null) { - remoteMediaPlayer = new RemoteMediaPlayer(); - - remoteMediaPlayer.setOnStatusUpdatedListener( - () -> { - Log.d(TAG, "RemoteMediaPlayer::onStatusUpdated() is reached"); - CastManager.this.onRemoteMediaPlayerStatusUpdated(); - } - ); - - remoteMediaPlayer.setOnPreloadStatusUpdatedListener( - () -> { - Log.d(TAG, "RemoteMediaPlayer::onPreloadStatusUpdated() is reached"); - CastManager.this.onRemoteMediaPreloadStatusUpdated(); - }); - - - remoteMediaPlayer.setOnMetadataUpdatedListener( - () -> { - Log.d(TAG, "RemoteMediaPlayer::onMetadataUpdated() is reached"); - CastManager.this.onRemoteMediaPlayerMetadataUpdated(); - } - ); - - remoteMediaPlayer.setOnQueueStatusUpdatedListener( - () -> { - Log.d(TAG, "RemoteMediaPlayer::onQueueStatusUpdated() is reached"); - mediaStatus = remoteMediaPlayer.getMediaStatus(); - if (mediaStatus != null - && mediaStatus.getQueueItems() != null) { - List queueItems = mediaStatus - .getQueueItems(); - int itemId = mediaStatus.getCurrentItemId(); - MediaQueueItem item = mediaStatus - .getQueueItemById(itemId); - int repeatMode = mediaStatus.getQueueRepeatMode(); - onQueueUpdated(queueItems, item, repeatMode, false); - } else { - onQueueUpdated(null, null, - MediaStatus.REPEAT_MODE_REPEAT_OFF, false); - } - }); - - } - try { - Log.d(TAG, "Registering MediaChannel namespace"); - Cast.CastApi.setMessageReceivedCallbacks(mApiClient, remoteMediaPlayer.getNamespace(), - remoteMediaPlayer); - } catch (IOException | IllegalStateException e) { - Log.e(TAG, "attachMediaChannel()", e); - } - } - - private void reattachMediaChannel() { - if (remoteMediaPlayer != null && mApiClient != null) { - try { - Log.d(TAG, "Registering MediaChannel namespace"); - Cast.CastApi.setMessageReceivedCallbacks(mApiClient, - remoteMediaPlayer.getNamespace(), remoteMediaPlayer); - } catch (IOException | IllegalStateException e) { - Log.e(TAG, "reattachMediaChannel()", e); - } - } - } - - private void detachMediaChannel() { - Log.d(TAG, "trying to detach media channel"); - if (remoteMediaPlayer != null) { - try { - Cast.CastApi.removeMessageReceivedCallbacks(mApiClient, - remoteMediaPlayer.getNamespace()); - } catch (IOException | IllegalStateException e) { - Log.e(TAG, "detachMediaChannel()", e); - } - remoteMediaPlayer = null; - } - } - - /** - * Returns the playback status of the remote device. - * - * @return Returns one of the values - *

- */ - public int getPlaybackStatus() { - return state; - } - - /** - * Returns the latest retrieved value for the {@link MediaStatus}. This value is updated - * whenever the onStatusUpdated callback is called. - */ - public final MediaStatus getMediaStatus() { - return mediaStatus; - } - - /** - * Returns the Idle reason, defined in MediaStatus.IDLE_*. Note that the returned - * value is only meaningful if the status is truly MediaStatus.PLAYER_STATE_IDLE - * - * - *

Possible values are: - *

- */ - public int getIdleReason() { - return idleReason; - } - - private void onMessageSendFailed(int errorCode) { - for (CastConsumer consumer : castConsumers) { - consumer.onDataMessageSendFailed(errorCode); - } - } - - /* - * This is called by onStatusUpdated() of the RemoteMediaPlayer - */ - private void onRemoteMediaPlayerStatusUpdated() { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated() reached"); - if (mApiClient == null || remoteMediaPlayer == null) { - Log.d(TAG, "mApiClient or remoteMediaPlayer is null, so will not proceed"); - return; - } - mediaStatus = remoteMediaPlayer.getMediaStatus(); - if (mediaStatus == null) { - Log.d(TAG, "MediaStatus is null, so will not proceed"); - return; - } else { - List queueItems = mediaStatus.getQueueItems(); - if (queueItems != null) { - int itemId = mediaStatus.getCurrentItemId(); - MediaQueueItem item = mediaStatus.getQueueItemById(itemId); - int repeatMode = mediaStatus.getQueueRepeatMode(); - onQueueUpdated(queueItems, item, repeatMode, false); - } else { - onQueueUpdated(null, null, MediaStatus.REPEAT_MODE_REPEAT_OFF, false); - } - state = mediaStatus.getPlayerState(); - idleReason = mediaStatus.getIdleReason(); - - if (state == MediaStatus.PLAYER_STATE_PLAYING) { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = playing"); - } else if (state == MediaStatus.PLAYER_STATE_PAUSED) { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = paused"); - } else if (state == MediaStatus.PLAYER_STATE_IDLE) { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = IDLE with reason: " - + idleReason); - if (idleReason == MediaStatus.IDLE_REASON_ERROR) { - // something bad happened on the cast device - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): IDLE reason = ERROR"); - onFailed(R.string.cast_failed_receiver_player_error, NO_STATUS_CODE); - } - } else if (state == MediaStatus.PLAYER_STATE_BUFFERING) { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = buffering"); - } else { - Log.d(TAG, "onRemoteMediaPlayerStatusUpdated(): Player status = unknown"); - } - } - for (CastConsumer consumer : castConsumers) { - consumer.onRemoteMediaPlayerStatusUpdated(); - } - if (mediaStatus != null) { - double volume = mediaStatus.getStreamVolume(); - boolean isMute = mediaStatus.isMute(); - for (CastConsumer consumer : castConsumers) { - consumer.onStreamVolumeChanged(volume, isMute); - } - } - } - - private void onRemoteMediaPreloadStatusUpdated() { - MediaQueueItem item = null; - mediaStatus = remoteMediaPlayer.getMediaStatus(); - if (mediaStatus != null) { - item = mediaStatus.getQueueItemById(mediaStatus.getPreloadedItemId()); - } - preLoadingItem = item; - Log.d(TAG, "onRemoteMediaPreloadStatusUpdated() " + item); - for (CastConsumer consumer : castConsumers) { - consumer.onRemoteMediaPreloadStatusUpdated(item); - } - } - - public MediaQueueItem getPreLoadingItem() { - return preLoadingItem; - } - - /* - * This is called by onQueueStatusUpdated() of RemoteMediaPlayer - */ - private void onQueueUpdated(List queueItems, MediaQueueItem item, - int repeatMode, boolean shuffle) { - Log.d(TAG, "onQueueUpdated() reached"); - Log.d(TAG, String.format("Queue Items size: %d, Item: %s, Repeat Mode: %d, Shuffle: %s", - queueItems == null ? 0 : queueItems.size(), item, repeatMode, shuffle)); - if (queueItems != null) { - mediaQueue = new MediaQueue(new CopyOnWriteArrayList<>(queueItems), item, shuffle, - repeatMode); - } else { - mediaQueue = new MediaQueue(new CopyOnWriteArrayList<>(), null, false, - MediaStatus.REPEAT_MODE_REPEAT_OFF); - } - for (CastConsumer consumer : castConsumers) { - consumer.onMediaQueueUpdated(queueItems, item, repeatMode, shuffle); - } - } - - /* - * This is called by onMetadataUpdated() of RemoteMediaPlayer - */ - public void onRemoteMediaPlayerMetadataUpdated() { - Log.d(TAG, "onRemoteMediaPlayerMetadataUpdated() reached"); - for (CastConsumer consumer : castConsumers) { - consumer.onRemoteMediaPlayerMetadataUpdated(); - } - } - - /** - * Registers a {@link CastConsumer} interface with this class. - * Registered listeners will be notified of changes to a variety of - * lifecycle and media status changes through the callbacks that the interface provides. - * - * @see DefaultCastConsumer - */ - public synchronized void addCastConsumer(CastConsumer listener) { - if (listener != null) { - addBaseCastConsumer(listener); - castConsumers.add(listener); - Log.d(TAG, "Successfully added the new CastConsumer listener " + listener); - } - } - - /** - * Unregisters a {@link CastConsumer}. - */ - public synchronized void removeCastConsumer(CastConsumer listener) { - if (listener != null) { - removeBaseCastConsumer(listener); - castConsumers.remove(listener); - } - } - - @Override - protected void onDeviceUnselected() { - detachMediaChannel(); - //removeDataChannel(); - state = MediaStatus.PLAYER_STATE_IDLE; - mediaStatus = null; - } - - @Override - protected Cast.CastOptions.Builder getCastOptionBuilder(CastDevice device) { - Cast.CastOptions.Builder builder = new Cast.CastOptions.Builder(mSelectedCastDevice, new CastListener()); - if (isFeatureEnabled(CastConfiguration.FEATURE_DEBUGGING)) { - builder.setVerboseLoggingEnabled(true); - } - return builder; - } - - @Override - public void onConnectionFailed(ConnectionResult result) { - super.onConnectionFailed(result); - state = MediaStatus.PLAYER_STATE_IDLE; - mediaStatus = null; - } - - @Override - public void onDisconnected(boolean stopAppOnExit, boolean clearPersistedConnectionData, - boolean setDefaultRoute) { - super.onDisconnected(stopAppOnExit, clearPersistedConnectionData, setDefaultRoute); - state = MediaStatus.PLAYER_STATE_IDLE; - mediaStatus = null; - mediaQueue = null; - } - - class CastListener extends Cast.Listener { - - /* - * (non-Javadoc) - * @see com.google.android.gms.cast.Cast.Listener#onApplicationDisconnected (int) - */ - @Override - public void onApplicationDisconnected(int statusCode) { - CastManager.this.onApplicationDisconnected(statusCode); - } - - /* - * (non-Javadoc) - * @see com.google.android.gms.cast.Cast.Listener#onApplicationStatusChanged () - */ - @Override - public void onApplicationStatusChanged() { - CastManager.this.onApplicationStatusChanged(); - } - - @Override - public void onVolumeChanged() { - CastManager.this.onDeviceVolumeChanged(); - } - } - - @Override - public void onFailed(int resourceId, int statusCode) { - Log.d(TAG, "onFailed: " + mContext.getString(resourceId) + ", code: " + statusCode); - super.onFailed(resourceId, statusCode); - } - - /** - * Clients can call this method to delegate handling of the volume. Clients should override - * {@code dispatchEvent} and call this method: - *
-     public boolean dispatchKeyEvent(KeyEvent event) {
-     if (mCastManager.onDispatchVolumeKeyEvent(event, VOLUME_DELTA)) {
-     return true;
-     }
-     return super.dispatchKeyEvent(event);
-     }
-     * 
- * @param event The dispatched event. - * @param volumeDelta The amount by which volume should be increased or decreased in each step - * @return true if volume is handled by the library, false otherwise. - */ - public boolean onDispatchVolumeKeyEvent(KeyEvent event, double volumeDelta) { - if (isConnected()) { - boolean isKeyDown = event.getAction() == KeyEvent.ACTION_DOWN; - switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_VOLUME_UP: - return changeVolume(volumeDelta, isKeyDown); - case KeyEvent.KEYCODE_VOLUME_DOWN: - return changeVolume(-volumeDelta, isKeyDown); - } - } - return false; - } - - private boolean changeVolume(double volumeIncrement, boolean isKeyDown) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - && getPlaybackStatus() == MediaStatus.PLAYER_STATE_PLAYING - && isFeatureEnabled(CastConfiguration.FEATURE_LOCKSCREEN)) { - return false; - } - - if (isKeyDown) { - try { - adjustDeviceVolume(volumeIncrement); - } catch (CastException | TransientNetworkDisconnectionException | - NoConnectionException e) { - Log.e(TAG, "Failed to change volume", e); - } - } - return true; - } - - /** - * Sets the volume step, i.e. the fraction by which volume will increase or decrease each time - * user presses the hard volume buttons on the device. - * - * @param volumeStep Should be a double between 0 and 1, inclusive. - */ - public CastManager setVolumeStep(double volumeStep) { - if ((volumeStep > 1) || (volumeStep < 0)) { - throw new IllegalArgumentException("Volume Step should be between 0 and 1, inclusive"); - } - this.volumeStep = volumeStep; - return this; - } - - /** - * Returns the volume step. The default value is {@code DEFAULT_VOLUME_STEP}. - */ - public double getVolumeStep() { - return volumeStep; - } - - public final MediaQueue getMediaQueue() { - return mediaQueue; - } - - /** - * Checks whether the selected Cast Device has the specified audio or video capabilities. - * - * @param capability capability from: - *
    - *
  • {@link CastDevice#CAPABILITY_AUDIO_IN}
  • - *
  • {@link CastDevice#CAPABILITY_AUDIO_OUT}
  • - *
  • {@link CastDevice#CAPABILITY_VIDEO_IN}
  • - *
  • {@link CastDevice#CAPABILITY_VIDEO_OUT}
  • - *
- * @param defaultVal value to return whenever there's no device selected. - * @return {@code true} if the selected device has the specified capability, - * {@code false} otherwise. - */ - public boolean hasCapability(final int capability, final boolean defaultVal) { - if (mSelectedCastDevice != null) { - return mSelectedCastDevice.hasCapability(capability); - } else { - return defaultVal; - } - } - - /** - * Adds and wires up the Switchable Media Router cast button. It returns a reference to the - * {@link SwitchableMediaRouteActionProvider} associated with the button if the caller needs - * such reference. It is assumed that the enclosing - * {@link android.app.Activity} inherits (directly or indirectly) from - * {@link android.support.v7.app.AppCompatActivity}. - * - * @param menuItem MenuItem of the Media Router cast button. - */ - public final SwitchableMediaRouteActionProvider addMediaRouterButton(MenuItem menuItem) { - SwitchableMediaRouteActionProvider mediaRouteActionProvider = (SwitchableMediaRouteActionProvider) - MenuItemCompat.getActionProvider(menuItem); - mediaRouteActionProvider.setRouteSelector(mMediaRouteSelector); - if (mCastConfiguration.getMediaRouteDialogFactory() != null) { - mediaRouteActionProvider.setDialogFactory(mCastConfiguration.getMediaRouteDialogFactory()); - } - return mediaRouteActionProvider; - } - - /* (non-Javadoc) - * These methods startReconnectionService and stopReconnectionService simply override the ones - * from BaseCastManager with empty implementations because we handle the service ourselves, but - * need to allow BaseCastManager to save current network information. - */ - @Override - protected void startReconnectionService(long mediaDurationLeft) { - // Do nothing - } - - @Override - protected void stopReconnectionService() { - // Do nothing - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java b/core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java deleted file mode 100644 index f0a7214c9..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/cast/CastUtils.java +++ /dev/null @@ -1,317 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.common.images.WebImage; - -import java.util.Calendar; -import java.util.List; - -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedImage; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.playback.ExternalMedia; -import de.danoeh.antennapod.core.util.playback.Playable; - -/** - * Helper functions for Cast support. - */ -public class CastUtils { - private static final String TAG = "CastUtils"; - - public static final String KEY_MEDIA_ID = "de.danoeh.antennapod.core.cast.MediaId"; - - public static final String KEY_EPISODE_IDENTIFIER = "de.danoeh.antennapod.core.cast.EpisodeId"; - public static final String KEY_EPISODE_LINK = "de.danoeh.antennapod.core.cast.EpisodeLink"; - public static final String KEY_FEED_URL = "de.danoeh.antennapod.core.cast.FeedUrl"; - public static final String KEY_FEED_WEBSITE = "de.danoeh.antennapod.core.cast.FeedWebsite"; - public static final String KEY_EPISODE_NOTES = "de.danoeh.antennapod.core.cast.EpisodeNotes"; - public static final int EPISODE_NOTES_MAX_LENGTH = Integer.MAX_VALUE; - - /** - * The field AntennaPod.FormatVersion specifies which version of MediaMetaData - * fields we're using. Future implementations should try to be backwards compatible with earlier - * versions, and earlier versions should be forward compatible until the version indicated by - * MAX_VERSION_FORWARD_COMPATIBILITY. If an update makes the format unreadable for - * an earlier version, then its version number should be greater than the - * MAX_VERSION_FORWARD_COMPATIBILITY value set on the earlier one, so that it - * doesn't try to parse the object. - */ - public static final String KEY_FORMAT_VERSION = "de.danoeh.antennapod.core.cast.FormatVersion"; - public static final int FORMAT_VERSION_VALUE = 1; - public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999; - - public static boolean isCastable(Playable media){ - if (media == null || media instanceof ExternalMedia) { - return false; - } - if (media instanceof FeedMedia || media instanceof RemoteMedia){ - String url = media.getStreamUrl(); - if(url == null || url.isEmpty()){ - return false; - } - switch (media.getMediaType()) { - case UNKNOWN: - return false; - case AUDIO: - return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT, true); - case VIDEO: - return CastManager.getInstance().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT, true); - } - } - return false; - } - - /** - * Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device. - * Before using this method, one should make sure {@link #isCastable(Playable)} returns - * {@code true}. - * - * Unless media.{@link FeedMedia#loadMetadata() loadMetadata()} has already been called, - * this method should not run on the main thread. - * - * @param media The {@link FeedMedia} object to be converted. - * @return {@link MediaInfo} object in a format proper for casting. - */ - public static MediaInfo convertFromFeedMedia(FeedMedia media){ - if(media == null) { - return null; - } - MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC); - try{ - media.loadMetadata(); - } catch (Playable.PlayableException e) { - Log.e(TAG, "Unable to load FeedMedia metadata", e); - } - FeedItem feedItem = media.getItem(); - if (feedItem != null) { - metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle()); - String subtitle = media.getFeedTitle(); - if (subtitle != null) { - metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle); - } - FeedImage image = feedItem.getImage(); - if (image != null && !TextUtils.isEmpty(image.getDownload_url())) { - metadata.addImage(new WebImage(Uri.parse(image.getDownload_url()))); - } - Calendar calendar = Calendar.getInstance(); - calendar.setTime(media.getItem().getPubDate()); - metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar); - Feed feed = feedItem.getFeed(); - if (feed != null) { - if (!TextUtils.isEmpty(feed.getAuthor())) { - metadata.putString(MediaMetadata.KEY_ARTIST, feed.getAuthor()); - } - if (!TextUtils.isEmpty(feed.getDownload_url())) { - metadata.putString(KEY_FEED_URL, feed.getDownload_url()); - } - if (!TextUtils.isEmpty(feed.getLink())) { - metadata.putString(KEY_FEED_WEBSITE, feed.getLink()); - } - } - if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) { - metadata.putString(KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier()); - } else { - metadata.putString(KEY_EPISODE_IDENTIFIER, media.getStreamUrl()); - } - if (!TextUtils.isEmpty(feedItem.getLink())) { - metadata.putString(KEY_EPISODE_LINK, feedItem.getLink()); - } - } - String notes = null; - try { - notes = media.loadShownotes().call(); - } catch (Exception e) { - Log.e(TAG, "Unable to load FeedMedia notes", e); - } - if (notes != null) { - if (notes.length() > EPISODE_NOTES_MAX_LENGTH) { - notes = notes.substring(0, EPISODE_NOTES_MAX_LENGTH); - } - metadata.putString(KEY_EPISODE_NOTES, notes); - } - // This field only identifies the id on the device that has the original version. - // Idea is to perhaps, on a first approach, check if the version on the local DB with the - // same id matches the remote object, and if not then search for episode and feed identifiers. - // This at least should make media recognition for a single device much quicker. - metadata.putInt(KEY_MEDIA_ID, ((Long) media.getIdentifier()).intValue()); - // A way to identify different casting media formats in case we change it in the future and - // senders with different versions share a casting device. - metadata.putInt(KEY_FORMAT_VERSION, FORMAT_VERSION_VALUE); - - MediaInfo.Builder builder = new MediaInfo.Builder(media.getStreamUrl()) - .setContentType(media.getMime_type()) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setMetadata(metadata); - if (media.getDuration() > 0) { - builder.setStreamDuration(media.getDuration()); - } - return builder.build(); - } - - //TODO make unit tests for all the conversion methods - /** - * Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}. - * - * Unless searchFeedMedia is set to false, this method should not run - * on the GUI thread. - * - * @param media The {@link MediaInfo} object to be converted. - * @param searchFeedMedia If set to true, the database will be queried to find a - * {@link FeedMedia} instance that matches {@param media}. - * @return {@link Playable} object in a format proper for casting. - */ - public static Playable getPlayable(MediaInfo media, boolean searchFeedMedia) { - Log.d(TAG, "getPlayable called with searchFeedMedia=" + searchFeedMedia); - if (media == null) { - Log.d(TAG, "MediaInfo object provided is null, not converting to any Playable instance"); - return null; - } - MediaMetadata metadata = media.getMetadata(); - int version = metadata.getInt(KEY_FORMAT_VERSION); - if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) { - Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this" + - "version of AntennaPod CastUtils, curVer=" + FORMAT_VERSION_VALUE + - ", object version=" + version); - return null; - } - Playable result = null; - if (searchFeedMedia) { - long mediaId = metadata.getInt(KEY_MEDIA_ID); - if (mediaId > 0) { - FeedMedia fMedia = DBReader.getFeedMedia(mediaId); - if (fMedia != null) { - try { - fMedia.loadMetadata(); - if (matches(media, fMedia)) { - result = fMedia; - Log.d(TAG, "FeedMedia object obtained matches the MediaInfo provided. id=" + mediaId); - } else { - Log.d(TAG, "FeedMedia object obtained does NOT match the MediaInfo provided. id=" + mediaId); - } - } catch (Playable.PlayableException e) { - Log.e(TAG, "Unable to load FeedMedia metadata to compare with MediaInfo", e); - } - } else { - Log.d(TAG, "Unable to find in database a FeedMedia with id=" + mediaId); - } - } - if (result == null) { - FeedItem feedItem = DBReader.getFeedItem(metadata.getString(KEY_FEED_URL), - metadata.getString(KEY_EPISODE_IDENTIFIER)); - if (feedItem != null) { - result = feedItem.getMedia(); - Log.d(TAG, "Found episode that matches the MediaInfo provided. Using its media, if existing."); - } - } - } - if (result == null) { - List imageList = metadata.getImages(); - String imageUrl = null; - if (!imageList.isEmpty()) { - imageUrl = imageList.get(0).getUrl().toString(); - } - result = new RemoteMedia(media.getContentId(), - metadata.getString(KEY_EPISODE_IDENTIFIER), - metadata.getString(KEY_FEED_URL), - metadata.getString(MediaMetadata.KEY_SUBTITLE), - metadata.getString(MediaMetadata.KEY_TITLE), - metadata.getString(KEY_EPISODE_LINK), - metadata.getString(MediaMetadata.KEY_ARTIST), - imageUrl, - metadata.getString(KEY_FEED_WEBSITE), - media.getContentType(), - metadata.getDate(MediaMetadata.KEY_RELEASE_DATE).getTime()); - String notes = metadata.getString(KEY_EPISODE_NOTES); - if (!TextUtils.isEmpty(notes)) { - ((RemoteMedia) result).setNotes(notes); - } - Log.d(TAG, "Converted MediaInfo into RemoteMedia"); - } - if (result.getDuration() == 0 && media.getStreamDuration() > 0) { - result.setDuration((int) media.getStreamDuration()); - } - return result; - } - - /** - * Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they - * represent the same podcast episode. - * - * @param info the {@link MediaInfo} object to be compared. - * @param media the {@link FeedMedia} object to be compared. - * @return true if there's a match, false otherwise. - * - * @see RemoteMedia#equals(Object) - */ - public static boolean matches(MediaInfo info, FeedMedia media) { - if (info == null || media == null) { - return false; - } - if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) { - return false; - } - MediaMetadata metadata = info.getMetadata(); - FeedItem fi = media.getItem(); - if (fi == null || metadata == null || - !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) { - return false; - } - Feed feed = fi.getFeed(); - return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url()); - } - - /** - * Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they - * represent the same podcast episode. - * - * @param info the {@link MediaInfo} object to be compared. - * @param media the {@link RemoteMedia} object to be compared. - * @return true if there's a match, false otherwise. - * - * @see RemoteMedia#equals(Object) - */ - public static boolean matches(MediaInfo info, RemoteMedia media) { - if (info == null || media == null) { - return false; - } - if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) { - return false; - } - MediaMetadata metadata = info.getMetadata(); - return metadata != null && - TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier()) && - TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl()); - } - - /** - * Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they - * represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device - * and want to avoid unnecessary conversions. - * - * @param info the {@link MediaInfo} object to be compared. - * @param media the {@link Playable} object to be compared. - * @return true if there's a match, false otherwise. - * - * @see RemoteMedia#equals(Object) - */ - public static boolean matches(MediaInfo info, Playable media) { - if (info == null || media == null) { - return false; - } - if (media instanceof RemoteMedia) { - return matches(info, (RemoteMedia) media); - } - return media instanceof FeedMedia && matches(info, (FeedMedia) media); - } - - - //TODO Queue handling perhaps -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java b/core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java deleted file mode 100644 index fe4183d54..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/cast/DefaultCastConsumer.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import com.google.android.libraries.cast.companionlibrary.cast.callbacks.VideoCastConsumerImpl; - -public class DefaultCastConsumer extends VideoCastConsumerImpl implements CastConsumer { - @Override - public void onStreamVolumeChanged(double value, boolean isMute) { - // no-op - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java b/core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java deleted file mode 100644 index e2d8f8ad5..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/cast/RemoteMedia.java +++ /dev/null @@ -1,357 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.common.images.WebImage; - -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.concurrent.Callable; - -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.core.util.playback.Playable; - -import org.apache.commons.lang3.builder.HashCodeBuilder; - -/** - * Playable implementation for media on a Cast Device for which a local version of - * {@link de.danoeh.antennapod.core.feed.FeedMedia} hasn't been found. - */ -public class RemoteMedia implements Playable { - public static final String TAG = "RemoteMedia"; - - public static final int PLAYABLE_TYPE_REMOTE_MEDIA = 3; - - private String downloadUrl; - private String itemIdentifier; - private String feedUrl; - private String feedTitle; - private String episodeTitle; - private String episodeLink; - private String feedAuthor; - private String imageUrl; - private String feedLink; - private String mime_type; - private Date pubDate; - private String notes; - private List chapters; - private int duration; - private int position; - private long lastPlayedTime; - - public RemoteMedia(String downloadUrl, String itemId, String feedUrl, String feedTitle, - String episodeTitle, String episodeLink, String feedAuthor, - String imageUrl, String feedLink, String mime_type, Date pubDate) { - this.downloadUrl = downloadUrl; - this.itemIdentifier = itemId; - this.feedUrl = feedUrl; - this.feedTitle = feedTitle; - this.episodeTitle = episodeTitle; - this.episodeLink = episodeLink; - this.feedAuthor = feedAuthor; - this.imageUrl = imageUrl; - this.feedLink = feedLink; - this.mime_type = mime_type; - this.pubDate = pubDate; - } - - public void setNotes(String notes) { - this.notes = notes; - } - - public MediaInfo extractMediaInfo() { - MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC); - - metadata.putString(MediaMetadata.KEY_TITLE, episodeTitle); - metadata.putString(MediaMetadata.KEY_SUBTITLE, feedTitle); - if (!TextUtils.isEmpty(imageUrl)) { - metadata.addImage(new WebImage(Uri.parse(imageUrl))); - } - Calendar calendar = Calendar.getInstance(); - calendar.setTime(pubDate); - metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar); - if (!TextUtils.isEmpty(feedAuthor)) { - metadata.putString(MediaMetadata.KEY_ARTIST, feedAuthor); - } - if (!TextUtils.isEmpty(feedUrl)) { - metadata.putString(CastUtils.KEY_FEED_URL, feedUrl); - } - if (!TextUtils.isEmpty(feedLink)) { - metadata.putString(CastUtils.KEY_FEED_WEBSITE, feedLink); - } - if (!TextUtils.isEmpty(itemIdentifier)) { - metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, itemIdentifier); - } else { - metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, downloadUrl); - } - if (!TextUtils.isEmpty(episodeLink)) { - metadata.putString(CastUtils.KEY_EPISODE_LINK, episodeLink); - } - String notes = this.notes; - if (notes != null) { - if (notes.length() > CastUtils.EPISODE_NOTES_MAX_LENGTH) { - notes = notes.substring(0, CastUtils.EPISODE_NOTES_MAX_LENGTH); - } - metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes); - } - // Default id value - metadata.putInt(CastUtils.KEY_MEDIA_ID, 0); - metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE); - - MediaInfo.Builder builder = new MediaInfo.Builder(downloadUrl) - .setContentType(mime_type) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setMetadata(metadata); - if (duration > 0) { - builder.setStreamDuration(duration); - } - return builder.build(); - } - - public String getEpisodeIdentifier() { - return itemIdentifier; - } - - public String getFeedUrl() { - return feedUrl; - } - - public FeedMedia lookForFeedMedia() { - FeedItem feedItem = DBReader.getFeedItem(feedUrl, itemIdentifier); - if (feedItem == null) { - return null; - } - return feedItem.getMedia(); - } - - @Override - public void writeToPreferences(SharedPreferences.Editor prefEditor) { - //it seems pointless to do it, since the session should be kept by the remote device. - } - - @Override - public void loadMetadata() throws PlayableException { - //Already loaded - } - - @Override - public void loadChapterMarks() { - ChapterUtils.loadChaptersFromStreamUrl(this); - } - - @Override - public String getEpisodeTitle() { - return episodeTitle; - } - - @Override - public List getChapters() { - return chapters; - } - - @Override - public String getWebsiteLink() { - if (episodeLink != null) { - return episodeLink; - } else { - return feedUrl; - } - } - - @Override - public String getPaymentLink() { - return null; - } - - @Override - public String getFeedTitle() { - return feedTitle; - } - - @Override - public Object getIdentifier() { - return itemIdentifier + "@" + feedUrl; - } - - @Override - public int getDuration() { - return duration; - } - - @Override - public int getPosition() { - return position; - } - - @Override - public long getLastPlayedTime() { - return lastPlayedTime; - } - - @Override - public MediaType getMediaType() { - return MediaType.fromMimeType(mime_type); - } - - @Override - public String getLocalMediaUrl() { - return null; - } - - @Override - public String getStreamUrl() { - return downloadUrl; - } - - @Override - public boolean localFileAvailable() { - return false; - } - - @Override - public boolean streamAvailable() { - return true; - } - - @Override - public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp) { - //we're not saving playback information for this kind of items on preferences - setPosition(newPosition); - setLastPlayedTime(timestamp); - } - - @Override - public void setPosition(int newPosition) { - position = newPosition; - } - - @Override - public void setDuration(int newDuration) { - duration = newDuration; - } - - @Override - public void setLastPlayedTime(long lastPlayedTimestamp) { - lastPlayedTime = lastPlayedTimestamp; - } - - @Override - public void onPlaybackStart() { - // no-op - } - - @Override - public void onPlaybackCompleted() { - // no-op - } - - @Override - public int getPlayableType() { - return PLAYABLE_TYPE_REMOTE_MEDIA; - } - - @Override - public void setChapters(List chapters) { - this.chapters = chapters; - } - - @Override - @Nullable - public String getImageLocation() { - return imageUrl; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public Callable loadShownotes() { - return () -> (notes != null) ? notes : ""; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(downloadUrl); - dest.writeString(itemIdentifier); - dest.writeString(feedUrl); - dest.writeString(feedTitle); - dest.writeString(episodeTitle); - dest.writeString(episodeLink); - dest.writeString(feedAuthor); - dest.writeString(imageUrl); - dest.writeString(feedLink); - dest.writeString(mime_type); - dest.writeLong(pubDate.getTime()); - dest.writeString(notes); - dest.writeInt(duration); - dest.writeInt(position); - dest.writeLong(lastPlayedTime); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public RemoteMedia createFromParcel(Parcel in) { - RemoteMedia result = new RemoteMedia(in.readString(), in.readString(), in.readString(), - in.readString(), in.readString(), in.readString(), in.readString(), in.readString(), - in.readString(), in.readString(), new Date(in.readLong())); - result.setNotes(in.readString()); - result.setDuration(in.readInt()); - result.setPosition(in.readInt()); - result.setLastPlayedTime(in.readLong()); - return result; - } - - @Override - public RemoteMedia[] newArray(int size) { - return new RemoteMedia[size]; - } - }; - - @Override - public boolean equals(Object other) { - if (other instanceof RemoteMedia) { - RemoteMedia rm = (RemoteMedia) other; - return TextUtils.equals(downloadUrl, rm.downloadUrl) && - TextUtils.equals(feedUrl, rm.feedUrl) && - TextUtils.equals(itemIdentifier, rm.itemIdentifier); - } - if (other instanceof FeedMedia) { - FeedMedia fm = (FeedMedia) other; - if (!TextUtils.equals(downloadUrl, fm.getStreamUrl())) { - return false; - } - FeedItem fi = fm.getItem(); - if (fi == null || !TextUtils.equals(itemIdentifier, fi.getItemIdentifier())) { - return false; - } - Feed feed = fi.getFeed(); - return feed != null && TextUtils.equals(feedUrl, feed.getDownload_url()); - } - return false; - } - - @Override - public int hashCode() { - return new HashCodeBuilder() - .append(downloadUrl) - .append(feedUrl) - .append(itemIdentifier) - .toHashCode(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java b/core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java deleted file mode 100644 index f063cf5e3..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/cast/SwitchableMediaRouteActionProvider.java +++ /dev/null @@ -1,106 +0,0 @@ -package de.danoeh.antennapod.core.cast; - -import android.app.Activity; -import android.content.Context; -import android.content.ContextWrapper; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.FragmentManager; -import android.support.v7.app.MediaRouteActionProvider; -import android.support.v7.app.MediaRouteChooserDialogFragment; -import android.support.v7.app.MediaRouteControllerDialogFragment; -import android.support.v7.media.MediaRouter; -import android.util.Log; - -/** - *

Action Provider that extends {@link MediaRouteActionProvider} and allows the client to - * disable completely the button by calling {@link #setEnabled(boolean)}.

- * - *

It is disabled by default, so if a client wants to initially have it enabled it must call - * setEnabled(true).

- */ -public class SwitchableMediaRouteActionProvider extends MediaRouteActionProvider { - public static final String TAG = "SwitchblMediaRtActProv"; - - private static final String CHOOSER_FRAGMENT_TAG = - "android.support.v7.mediarouter:MediaRouteChooserDialogFragment"; - private static final String CONTROLLER_FRAGMENT_TAG = - "android.support.v7.mediarouter:MediaRouteControllerDialogFragment"; - private boolean enabled; - - public SwitchableMediaRouteActionProvider(Context context) { - super(context); - enabled = false; - } - - /** - *

Sets whether the Media Router button should be allowed to become visible or not.

- * - *

It's invisible by default.

- */ - public void setEnabled(boolean newVal) { - enabled = newVal; - refreshVisibility(); - } - - @Override - public boolean isVisible() { - return enabled && super.isVisible(); - } - - @Override - public boolean onPerformDefaultAction() { - if (!super.onPerformDefaultAction()) { - // there is no button, but we should still show the dialog if it's the case. - if (!isVisible()) { - return false; - } - FragmentManager fm = getFragmentManager(); - if (fm == null) { - return false; - } - MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute(); - if (route.isDefault() || !route.matchesSelector(getRouteSelector())) { - if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) { - Log.w(TAG, "showDialog(): Route chooser dialog already showing!"); - return false; - } - MediaRouteChooserDialogFragment f = - getDialogFactory().onCreateChooserDialogFragment(); - f.setRouteSelector(getRouteSelector()); - f.show(fm, CHOOSER_FRAGMENT_TAG); - } else { - if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) { - Log.w(TAG, "showDialog(): Route controller dialog already showing!"); - return false; - } - MediaRouteControllerDialogFragment f = - getDialogFactory().onCreateControllerDialogFragment(); - f.show(fm, CONTROLLER_FRAGMENT_TAG); - } - return true; - - } else { - return true; - } - } - - private FragmentManager getFragmentManager() { - Activity activity = getActivity(); - if (activity instanceof FragmentActivity) { - return ((FragmentActivity)activity).getSupportFragmentManager(); - } - return null; - } - - private Activity getActivity() { - // Gross way of unwrapping the Activity so we can get the FragmentManager - Context context = getContext(); - while (context instanceof ContextWrapper) { - if (context instanceof Activity) { - return (Activity)context; - } - context = ((ContextWrapper)context).getBaseContext(); - } - return null; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java deleted file mode 100644 index 068669af9..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ /dev/null @@ -1,568 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.database.Cursor; -import android.media.MediaMetadataRetriever; -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.Nullable; - -import java.util.Date; -import java.util.List; -import java.util.concurrent.Callable; - -import de.danoeh.antennapod.core.cast.RemoteMedia; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.storage.PodDBAdapter; -import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.core.util.playback.Playable; - -public class FeedMedia extends FeedFile implements Playable { - private static final String TAG = "FeedMedia"; - - public static final int FEEDFILETYPE_FEEDMEDIA = 2; - public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; - - public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; - public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; - - /** - * Indicates we've checked on the size of the item via the network - * and got an invalid response. Using Integer.MIN_VALUE because - * 1) we'll still check on it in case it gets downloaded (it's <= 0) - * 2) By default all FeedMedia have a size of 0 if we don't know it, - * so this won't conflict with existing practice. - */ - private static final int CHECKED_ON_SIZE_BUT_UNKNOWN = Integer.MIN_VALUE; - - private int duration; - private int position; // Current position in file - private long lastPlayedTime; // Last time this media was played (in ms) - private int played_duration; // How many ms of this file have been played (for autoflattring) - private long size; // File size in Byte - private String mime_type; - @Nullable private volatile FeedItem item; - private Date playbackCompletionDate; - - // if null: unknown, will be checked - private Boolean hasEmbeddedPicture; - - /* Used for loading item when restoring from parcel. */ - private long itemID; - - public FeedMedia(FeedItem i, String download_url, long size, - String mime_type) { - super(null, download_url, false); - this.item = i; - this.size = size; - this.mime_type = mime_type; - } - - public FeedMedia(long id, FeedItem item, int duration, int position, - long size, String mime_type, String file_url, String download_url, - boolean downloaded, Date playbackCompletionDate, int played_duration, - long lastPlayedTime) { - super(file_url, download_url, downloaded); - this.id = id; - this.item = item; - this.duration = duration; - this.position = position; - this.played_duration = played_duration; - this.size = size; - this.mime_type = mime_type; - this.playbackCompletionDate = playbackCompletionDate == null - ? null : (Date) playbackCompletionDate.clone(); - this.lastPlayedTime = lastPlayedTime; - } - - public FeedMedia(long id, FeedItem item, int duration, int position, - long size, String mime_type, String file_url, String download_url, - boolean downloaded, Date playbackCompletionDate, int played_duration, - Boolean hasEmbeddedPicture, long lastPlayedTime) { - this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded, - playbackCompletionDate, played_duration, lastPlayedTime); - this.hasEmbeddedPicture = hasEmbeddedPicture; - } - - public static FeedMedia fromCursor(Cursor cursor) { - int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); - int indexPlaybackCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE); - int indexDuration = cursor.getColumnIndex(PodDBAdapter.KEY_DURATION); - int indexPosition = cursor.getColumnIndex(PodDBAdapter.KEY_POSITION); - int indexSize = cursor.getColumnIndex(PodDBAdapter.KEY_SIZE); - int indexMimeType = cursor.getColumnIndex(PodDBAdapter.KEY_MIME_TYPE); - int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL); - int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL); - int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED); - int indexPlayedDuration = cursor.getColumnIndex(PodDBAdapter.KEY_PLAYED_DURATION); - int indexLastPlayedTime = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_PLAYED_TIME); - - long mediaId = cursor.getLong(indexId); - Date playbackCompletionDate = null; - long playbackCompletionTime = cursor.getLong(indexPlaybackCompletionDate); - if (playbackCompletionTime > 0) { - playbackCompletionDate = new Date(playbackCompletionTime); - } - - Boolean hasEmbeddedPicture; - switch(cursor.getInt(cursor.getColumnIndex(PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE))) { - case 1: - hasEmbeddedPicture = Boolean.TRUE; - break; - case 0: - hasEmbeddedPicture = Boolean.FALSE; - break; - default: - hasEmbeddedPicture = null; - break; - } - - return new FeedMedia( - mediaId, - null, - cursor.getInt(indexDuration), - cursor.getInt(indexPosition), - cursor.getLong(indexSize), - cursor.getString(indexMimeType), - cursor.getString(indexFileUrl), - cursor.getString(indexDownloadUrl), - cursor.getInt(indexDownloaded) > 0, - playbackCompletionDate, - cursor.getInt(indexPlayedDuration), - hasEmbeddedPicture, - cursor.getLong(indexLastPlayedTime) - ); - } - - - @Override - public String getHumanReadableIdentifier() { - if (item != null && item.getTitle() != null) { - return item.getTitle(); - } else { - return download_url; - } - } - - /** - * Uses mimetype to determine the type of media. - */ - public MediaType getMediaType() { - return MediaType.fromMimeType(mime_type); - } - - public void updateFromOther(FeedMedia other) { - super.updateFromOther(other); - if (other.size > 0) { - size = other.size; - } - if (other.mime_type != null) { - mime_type = other.mime_type; - } - } - - public boolean compareWithOther(FeedMedia other) { - if (super.compareWithOther(other)) { - return true; - } - if (other.mime_type != null) { - if (mime_type == null || !mime_type.equals(other.mime_type)) { - return true; - } - } - if (other.size > 0 && other.size != size) { - return true; - } - return false; - } - - /** - * Reads playback preferences to determine whether this FeedMedia object is - * currently being played. - */ - public boolean isPlaying() { - return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA - && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id; - } - - /** - * Reads playback preferences to determine whether this FeedMedia object is - * currently being played and the current player status is playing. - */ - public boolean isCurrentlyPlaying() { - return isPlaying() && - ((PlaybackPreferences.getCurrentPlayerStatus() == PlaybackPreferences.PLAYER_STATUS_PLAYING)); - } - - /** - * Reads playback preferences to determine whether this FeedMedia object is - * currently being played and the current player status is paused. - */ - public boolean isCurrentlyPaused() { - return isPlaying() && - ((PlaybackPreferences.getCurrentPlayerStatus() == PlaybackPreferences.PLAYER_STATUS_PAUSED)); - } - - - public boolean hasAlmostEnded() { - int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); - return this.position >= this.duration - smartMarkAsPlayedSecs * 1000; - } - - @Override - public int getTypeAsInt() { - return FEEDFILETYPE_FEEDMEDIA; - } - - public int getDuration() { - return duration; - } - - public void setDuration(int duration) { - this.duration = duration; - } - - @Override - public void setLastPlayedTime(long lastPlayedTime) { - this.lastPlayedTime = lastPlayedTime; - } - - public int getPlayedDuration() { - return played_duration; - } - - public void setPlayedDuration(int played_duration) { - this.played_duration = played_duration; - } - - public int getPosition() { - return position; - } - - @Override - public long getLastPlayedTime() { - return lastPlayedTime; - } - - public void setPosition(int position) { - this.position = position; - if(position > 0 && item != null && item.isNew()) { - this.item.setPlayed(false); - } - } - - public long getSize() { - return size; - } - - public void setSize(long size) { - this.size = size; - } - - /** - * Indicates we asked the service what the size was, but didn't - * get a valid answer and we shoudln't check using the network again. - */ - public void setCheckedOnSizeButUnknown() { - this.size = CHECKED_ON_SIZE_BUT_UNKNOWN; - } - - public boolean checkedOnSizeButUnknown() { - return (CHECKED_ON_SIZE_BUT_UNKNOWN == this.size); - } - - public String getMime_type() { - return mime_type; - } - - public void setMime_type(String mime_type) { - this.mime_type = mime_type; - } - - @Nullable - public FeedItem getItem() { - return item; - } - - /** - * Sets the item object of this FeedMedia. If the given - * FeedItem object is not null, it's 'media'-attribute value - * will also be set to this media object. - */ - public void setItem(FeedItem item) { - this.item = item; - if (item != null && item.getMedia() != this) { - item.setMedia(this); - } - } - - public Date getPlaybackCompletionDate() { - return playbackCompletionDate == null - ? null : (Date) playbackCompletionDate.clone(); - } - - public void setPlaybackCompletionDate(Date playbackCompletionDate) { - this.playbackCompletionDate = playbackCompletionDate == null - ? null : (Date) playbackCompletionDate.clone(); - } - - public boolean isInProgress() { - return (this.position > 0); - } - - @Override - public int describeContents() { - return 0; - } - - public boolean hasEmbeddedPicture() { - if(hasEmbeddedPicture == null) { - checkEmbeddedPicture(); - } - return hasEmbeddedPicture; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeLong(id); - dest.writeLong(item != null ? item.getId() : 0L); - - dest.writeInt(duration); - dest.writeInt(position); - dest.writeLong(size); - dest.writeString(mime_type); - dest.writeString(file_url); - dest.writeString(download_url); - dest.writeByte((byte) ((downloaded) ? 1 : 0)); - dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0); - dest.writeInt(played_duration); - dest.writeLong(lastPlayedTime); - } - - @Override - public void writeToPreferences(Editor prefEditor) { - if(item != null && item.getFeed() != null) { - prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId()); - } else { - prefEditor.putLong(PREF_FEED_ID, 0L); - } - prefEditor.putLong(PREF_MEDIA_ID, id); - } - - @Override - public void loadMetadata() throws PlayableException { - if (item == null && itemID != 0) { - item = DBReader.getFeedItem(itemID); - } - } - - @Override - public void loadChapterMarks() { - if (item == null && itemID != 0) { - item = DBReader.getFeedItem(itemID); - } - // check if chapters are stored in db and not loaded yet. - if (item != null && item.hasChapters() && item.getChapters() == null) { - DBReader.loadChaptersOfFeedItem(item); - } else if (item != null && item.getChapters() == null) { - if(localFileAvailable()) { - ChapterUtils.loadChaptersFromFileUrl(this); - } else { - ChapterUtils.loadChaptersFromStreamUrl(this); - } - if (getChapters() != null && item != null) { - DBWriter.setFeedItem(item); - } - } - } - - @Override - public String getEpisodeTitle() { - if (item == null) { - return null; - } - if (item.getTitle() != null) { - return item.getTitle(); - } else { - return item.getIdentifyingValue(); - } - } - - @Override - public List getChapters() { - if (item == null) { - return null; - } - return item.getChapters(); - } - - @Override - public String getWebsiteLink() { - if (item == null) { - return null; - } - return item.getLink(); - } - - @Override - public String getFeedTitle() { - if (item == null || item.getFeed() == null) { - return null; - } - return item.getFeed().getTitle(); - } - - @Override - public Object getIdentifier() { - return id; - } - - @Override - public String getLocalMediaUrl() { - return file_url; - } - - @Override - public String getStreamUrl() { - return download_url; - } - - @Override - public String getPaymentLink() { - if (item == null) { - return null; - } - return item.getPaymentLink(); - } - - @Override - public boolean localFileAvailable() { - return isDownloaded() && file_url != null; - } - - @Override - public boolean streamAvailable() { - return download_url != null; - } - - @Override - public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timeStamp) { - if(item != null && item.isNew()) { - DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); - } - setPosition(newPosition); - setLastPlayedTime(timeStamp); - DBWriter.setFeedMediaPlaybackInformation(this); - } - - @Override - public void onPlaybackStart() { - } - @Override - public void onPlaybackCompleted() { - - } - - @Override - public int getPlayableType() { - return PLAYABLE_TYPE_FEEDMEDIA; - } - - @Override - public void setChapters(List chapters) { - if(item != null) { - item.setChapters(chapters); - } - } - - @Override - public Callable loadShownotes() { - return () -> { - if (item == null) { - item = DBReader.getFeedItem( - itemID); - } - if (item.getContentEncoded() == null || item.getDescription() == null) { - DBReader.loadExtraInformationOfFeedItem( - item); - - } - return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription(); - }; - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - public FeedMedia createFromParcel(Parcel in) { - final long id = in.readLong(); - final long itemID = in.readLong(); - FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(), - in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt(), in.readLong()); - result.itemID = itemID; - return result; - } - - public FeedMedia[] newArray(int size) { - return new FeedMedia[size]; - } - }; - - @Override - public String getImageLocation() { - if (hasEmbeddedPicture()) { - return getLocalMediaUrl(); - } else if(item != null) { - return item.getImageLocation(); - } else { - return null; - } - } - - public void setHasEmbeddedPicture(Boolean hasEmbeddedPicture) { - this.hasEmbeddedPicture = hasEmbeddedPicture; - } - - @Override - public void setDownloaded(boolean downloaded) { - super.setDownloaded(downloaded); - if(item != null && downloaded) { - item.setPlayed(false); - } - } - - @Override - public void setFile_url(String file_url) { - super.setFile_url(file_url); - } - - public void checkEmbeddedPicture() { - if (!localFileAvailable()) { - hasEmbeddedPicture = Boolean.FALSE; - return; - } - MediaMetadataRetriever mmr = new MediaMetadataRetriever(); - try { - mmr.setDataSource(getLocalMediaUrl()); - byte[] image = mmr.getEmbeddedPicture(); - if(image != null) { - hasEmbeddedPicture = Boolean.TRUE; - } else { - hasEmbeddedPicture = Boolean.FALSE; - } - } catch (Exception e) { - e.printStackTrace(); - hasEmbeddedPicture = Boolean.FALSE; - } - } - - @Override - public boolean equals(Object o) { - if (o instanceof RemoteMedia) { - return o.equals(this); - } - return super.equals(o); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java deleted file mode 100644 index e2d63a385..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ /dev/null @@ -1,1780 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.bluetooth.BluetoothA2dp; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.net.NetworkInfo; -import android.net.wifi.WifiManager; -import android.os.Binder; -import android.os.Build; -import android.os.IBinder; -import android.os.Vibrator; -import android.preference.PreferenceManager; -import android.support.annotation.NonNull; -import android.support.annotation.StringRes; -import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.PlaybackStateCompat; -import android.support.v7.app.NotificationCompat; -import android.support.v7.media.MediaRouter; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; -import android.view.Display; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.SurfaceHolder; -import android.view.WindowManager; -import android.widget.Toast; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.target.Target; -import com.google.android.gms.cast.ApplicationMetadata; -import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager; - -import java.util.List; - -import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.cast.CastConsumer; -import de.danoeh.antennapod.core.cast.CastManager; -import de.danoeh.antennapod.core.cast.DefaultCastConsumer; -import de.danoeh.antennapod.core.feed.Chapter; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.glide.ApGlideSettings; -import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; -import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; -import de.danoeh.antennapod.core.preferences.GpodnetPreferences; -import de.danoeh.antennapod.core.preferences.PlaybackPreferences; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.core.storage.DBTasks; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.util.IntList; -import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.QueueAccess; -import de.danoeh.antennapod.core.util.flattr.FlattrUtils; -import de.danoeh.antennapod.core.util.playback.ExternalMedia; -import de.danoeh.antennapod.core.util.playback.Playable; - -/** - * Controls the MediaPlayer that plays a FeedMedia-file - */ -public class PlaybackService extends Service { - public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE"; - public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE"; - /** - * Logging tag - */ - private static final String TAG = "PlaybackService"; - - /** - * Parcelable of type Playable. - */ - public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; - /** - * True if cast session should disconnect. - */ - public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect"; - /** - * True if media should be streamed. - */ - public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream"; - /** - * True if playback should be started immediately after media has been - * prepared. - */ - public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared"; - - public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately"; - - public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.core.service.playerStatusChanged"; - public static final String EXTRA_NEW_PLAYER_STATUS = "extra.de.danoeh.antennapod.service.playerStatusChanged.newStatus"; - private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; - private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; - - public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.core.service.playerNotification"; - public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.core.service.notificationCode"; - public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.core.service.notificationType"; - - /** - * If the PlaybackService receives this action, it will stop playback and - * try to shutdown. - */ - public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.core.service.actionShutdownPlaybackService"; - - /** - * If the PlaybackService receives this action, it will end playback of the - * current episode and load the next episode if there is one available. - */ - public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.skipCurrentEpisode"; - - /** - * If the PlaybackService receives this action, it will pause playback. - */ - public static final String ACTION_PAUSE_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.pausePlayCurrentEpisode"; - - - /** - * If the PlaybackService receives this action, it will resume playback. - */ - public static final String ACTION_RESUME_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.resumePlayCurrentEpisode"; - - - /** - * Used in NOTIFICATION_TYPE_RELOAD. - */ - public static final int EXTRA_CODE_AUDIO = 1; - public static final int EXTRA_CODE_VIDEO = 2; - public static final int EXTRA_CODE_CAST = 3; - - public static final int NOTIFICATION_TYPE_ERROR = 0; - public static final int NOTIFICATION_TYPE_INFO = 1; - public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; - - /** - * Receivers of this intent should update their information about the curently playing media - */ - public static final int NOTIFICATION_TYPE_RELOAD = 3; - /** - * The state of the sleeptimer changed. - */ - public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; - public static final int NOTIFICATION_TYPE_BUFFER_START = 5; - public static final int NOTIFICATION_TYPE_BUFFER_END = 6; - /** - * No more episodes are going to be played. - */ - public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; - - /** - * Playback speed has changed - */ - public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; - - /** - * Ability to set the playback speed has changed - */ - public static final int NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED = 9; - - /** - * Send a message to the user (with provided String resource id) - */ - public static final int NOTIFICATION_TYPE_SHOW_TOAST = 10; - - /** - * Returned by getPositionSafe() or getDurationSafe() if the playbackService - * is in an invalid state. - */ - public static final int INVALID_TIME = -1; - - /** - * Time in seconds during which the CastManager will try to reconnect to the Cast Device after - * the Wifi Connection is regained. - */ - private static final int RECONNECTION_ATTEMPT_PERIOD_S = 15; - - /** - * Is true if service is running. - */ - public static boolean isRunning = false; - /** - * Is true if service has received a valid start command. - */ - public static boolean started = false; - /** - * Is true if the service was running, but paused due to headphone disconnect - */ - public static boolean transientPause = false; - /** - * Is true if a Cast Device is connected to the service. - */ - private static volatile boolean isCasting = false; - /** - * Stores the state of the cast playback just before it disconnects. - */ - private volatile PlaybackServiceMediaPlayer.PSMPInfo infoBeforeCastDisconnection; - - private boolean wifiConnectivity = true; - private BroadcastReceiver wifiBroadcastReceiver; - - private static final int NOTIFICATION_ID = 1; - - private PlaybackServiceMediaPlayer mediaPlayer; - private PlaybackServiceTaskManager taskManager; - - private CastManager castManager; - private MediaRouter mediaRouter; - /** - * Only used for Lollipop notifications. - */ - private MediaSessionCompat mediaSession; - - private int startPosition; - - private static volatile MediaType currentMediaType = MediaType.UNKNOWN; - - private final IBinder mBinder = new LocalBinder(); - - public class LocalBinder extends Binder { - public PlaybackService getService() { - return PlaybackService.this; - } - } - - @Override - public boolean onUnbind(Intent intent) { - Log.d(TAG, "Received onUnbind event"); - return super.onUnbind(intent); - } - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. If the playbackservice is not - * running, the type of the last played media will be looked up. - */ - public static Intent getPlayerActivityIntent(Context context) { - if (isRunning) { - return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType, isCasting); - } else { - if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { - return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO, isCasting); - } else { - return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO, isCasting); - } - } - } - - /** - * Same as getPlayerActivityIntent(context), but here the type of activity - * depends on the FeedMedia that is provided as an argument. - */ - public static Intent getPlayerActivityIntent(Context context, Playable media) { - MediaType mt = media.getMediaType(); - return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt, isCasting); - } - - @Override - public void onCreate() { - super.onCreate(); - Log.d(TAG, "Service created."); - isRunning = true; - - registerReceiver(headsetDisconnected, new IntentFilter( - Intent.ACTION_HEADSET_PLUG)); - registerReceiver(shutdownReceiver, new IntentFilter( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - registerReceiver(bluetoothStateUpdated, new IntentFilter( - BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)); - } - registerReceiver(audioBecomingNoisy, new IntentFilter( - AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( - ACTION_SKIP_CURRENT_EPISODE)); - registerReceiver(pausePlayCurrentEpisodeReceiver, new IntentFilter( - ACTION_PAUSE_PLAY_CURRENT_EPISODE)); - registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter( - ACTION_RESUME_PLAY_CURRENT_EPISODE)); - taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); - - mediaRouter = MediaRouter.getInstance(getApplicationContext()); - PreferenceManager.getDefaultSharedPreferences(this) - .registerOnSharedPreferenceChangeListener(prefListener); - - ComponentName eventReceiver = new ComponentName(getApplicationContext(), - MediaButtonReceiver.class); - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(eventReceiver); - PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); - - mediaSession = new MediaSessionCompat(getApplicationContext(), TAG, eventReceiver, buttonReceiverIntent); - - try { - mediaSession.setCallback(sessionCallback); - mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); - } catch (NullPointerException npe) { - // on some devices (Huawei) setting active can cause a NullPointerException - // even with correct use of the api. - // See http://stackoverflow.com/questions/31556679/android-huawei-mediassessioncompat - // and https://plus.google.com/+IanLake/posts/YgdTkKFxz7d - Log.e(TAG, "NullPointerException while setting up MediaSession"); - npe.printStackTrace(); - } - - castManager = CastManager.getInstance(); - castManager.addCastConsumer(castConsumer); - isCasting = castManager.isConnected(); - if (isCasting) { - if (UserPreferences.isCastEnabled()) { - onCastAppConnected(false); - } else { - castManager.disconnect(); - } - } else { - mediaPlayer = new LocalPSMP(this, mediaPlayerCallback); - } - - mediaSession.setActive(true); - } - - @Override - public void onDestroy() { - super.onDestroy(); - Log.d(TAG, "Service is about to be destroyed"); - isRunning = false; - started = false; - currentMediaType = MediaType.UNKNOWN; - - PreferenceManager.getDefaultSharedPreferences(this) - .unregisterOnSharedPreferenceChangeListener(prefListener); - if (mediaSession != null) { - mediaSession.release(); - } - unregisterReceiver(headsetDisconnected); - unregisterReceiver(shutdownReceiver); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - unregisterReceiver(bluetoothStateUpdated); - } - unregisterReceiver(audioBecomingNoisy); - unregisterReceiver(skipCurrentEpisodeReceiver); - unregisterReceiver(pausePlayCurrentEpisodeReceiver); - unregisterReceiver(pauseResumeCurrentEpisodeReceiver); - castManager.removeCastConsumer(castConsumer); - unregisterWifiBroadcastReceiver(); - mediaPlayer.shutdown(); - taskManager.shutdown(); - } - - @Override - public IBinder onBind(Intent intent) { - Log.d(TAG, "Received onBind event"); - return mBinder; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - - Log.d(TAG, "OnStartCommand called"); - final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); - final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false); - final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); - if (keycode == -1 && playable == null && !castDisconnect) { - Log.e(TAG, "PlaybackService was started with no arguments"); - stopSelf(); - return Service.START_REDELIVER_INTENT; - } - - if ((flags & Service.START_FLAG_REDELIVERY) != 0) { - Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); - stopForeground(true); - } else { - - if (keycode != -1) { - Log.d(TAG, "Received media button event"); - handleKeycode(keycode, intent.getIntExtra(MediaButtonReceiver.EXTRA_SOURCE, - InputDevice.SOURCE_CLASS_NONE)); - } else if (castDisconnect) { - castManager.disconnect(); - } else { - started = true; - boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, - true); - boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); - boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - //If the user asks to play External Media, the casting session, if on, should end. - if (playable instanceof ExternalMedia) { - castManager.disconnect(); - } - mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); - } - } - - return Service.START_REDELIVER_INTENT; - } - - /** - * Handles media button events - */ - private void handleKeycode(int keycode, int source) { - Log.d(TAG, "Handling keycode: " + keycode); - final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); - final PlayerStatus status = info.playerStatus; - switch (keycode) { - case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(!UserPreferences.isPersistNotify(), true); - } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - mediaPlayer.resume(); - } else if (status == PlayerStatus.PREPARING) { - mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); - } else if (status == PlayerStatus.INITIALIZED) { - mediaPlayer.setStartWhenPrepared(true); - mediaPlayer.prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - mediaPlayer.resume(); - } else if (status == PlayerStatus.INITIALIZED) { - mediaPlayer.setStartWhenPrepared(true); - mediaPlayer.prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(!UserPreferences.isPersistNotify(), true); - } - - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - if(source == InputDevice.SOURCE_CLASS_NONE || - UserPreferences.shouldHardwareButtonSkip()) { - // assume the skip command comes from a notification or the lockscreen - // a >| skip button should actually skip - mediaPlayer.endPlayback(true, false); - } else { - // assume skip command comes from a (bluetooth) media button - // user actually wants to fast-forward - seekDelta(UserPreferences.getFastFowardSecs() * 1000); - } - break; - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - mediaPlayer.seekDelta(UserPreferences.getFastFowardSecs() * 1000); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - case KeyEvent.KEYCODE_MEDIA_REWIND: - mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); - break; - case KeyEvent.KEYCODE_MEDIA_STOP: - if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, true); - started = false; - } - - stopForeground(true); // gets rid of persistent notification - break; - default: - Log.d(TAG, "Unhandled key code: " + keycode); - if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something - String message = String.format(getResources().getString(R.string.unknown_media_key), keycode); - Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); - } - break; - } - } - - /** - * Called by a mediaplayer Activity as soon as it has prepared its - * mediaplayer. - */ - public void setVideoSurface(SurfaceHolder sh) { - Log.d(TAG, "Setting display"); - mediaPlayer.setVideoSurface(sh); - } - - /** - * Called when the surface holder of the mediaplayer has to be changed. - */ - private void resetVideoSurface() { - taskManager.cancelPositionSaver(); - mediaPlayer.resetVideoSurface(); - } - - public void notifyVideoSurfaceAbandoned() { - stopForeground(!UserPreferences.isPersistNotify()); - mediaPlayer.resetVideoSurface(); - } - - private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { - @Override - public void positionSaverTick() { - saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); - } - - @Override - public void onSleepTimerAlmostExpired() { - float leftVolume = 0.1f * UserPreferences.getLeftVolume(); - float rightVolume = 0.1f * UserPreferences.getRightVolume(); - mediaPlayer.setVolume(leftVolume, rightVolume); - } - - @Override - public void onSleepTimerExpired() { - mediaPlayer.pause(true, true); - float leftVolume = UserPreferences.getLeftVolume(); - float rightVolume = UserPreferences.getRightVolume(); - mediaPlayer.setVolume(leftVolume, rightVolume); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - @Override - public void onSleepTimerReset() { - float leftVolume = UserPreferences.getLeftVolume(); - float rightVolume = UserPreferences.getRightVolume(); - mediaPlayer.setVolume(leftVolume, rightVolume); - } - - @Override - public void onWidgetUpdaterTick() { - updateWidget(); - } - - @Override - public void onChapterLoaded(Playable media) { - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - } - }; - - private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { - @Override - public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { - currentMediaType = mediaPlayer.getCurrentMediaType(); - updateMediaSession(newInfo.playerStatus); - switch (newInfo.playerStatus) { - case INITIALIZED: - writePlaybackPreferences(); - break; - - case PREPARED: - taskManager.startChapterLoader(newInfo.playable); - break; - - case PAUSED: - taskManager.cancelPositionSaver(); - saveCurrentPosition(false, 0); - taskManager.cancelWidgetUpdater(); - if ((UserPreferences.isPersistNotify() || isCasting) && - android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - // do not remove notification on pause based on user pref and whether android version supports expanded notifications - // Change [Play] button to [Pause] - setupNotification(newInfo); - } else if (!UserPreferences.isPersistNotify() && !isCasting) { - // remove notification on pause - stopForeground(true); - } - writePlayerStatusPlaybackPreferences(); - - final Playable playable = newInfo.playable; - - // Gpodder: send play action - if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) - .currentDeviceId() - .currentTimestamp() - .started(startPosition / 1000) - .position(getCurrentPosition() / 1000) - .total(getDuration() / 1000) - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } - break; - - case STOPPED: - //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); - //stopSelf(); - break; - - case PLAYING: - Log.d(TAG, "Audiofocus successfully requested"); - Log.d(TAG, "Resuming/Starting playback"); - - taskManager.startPositionSaver(); - taskManager.startWidgetUpdater(); - writePlayerStatusPlaybackPreferences(); - setupNotification(newInfo); - started = true; - startPosition = mediaPlayer.getPosition(); - break; - - case ERROR: - writePlaybackPreferencesNoMediaPlaying(); - break; - - } - - Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED); - // statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal()); - sendBroadcast(statusUpdate); - updateWidget(); - bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); - bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); - } - - @Override - public void shouldStop() { - stopSelf(); - } - - @Override - public void playbackSpeedChanged(float s) { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); - } - - public void setSpeedAbilityChanged() { - sendNotificationBroadcast(NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED, 0); - } - - @Override - public void onBufferingUpdate(int percent) { - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); - } - - @Override - public void onMediaChanged(boolean reloadUI) { - Log.d(TAG, "reloadUI callback reached"); - if (reloadUI) { - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - } - PlaybackService.this.updateMediaSessionMetadata(getPlayable()); - } - - @Override - public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) { - switch (code) { - case MediaPlayer.MEDIA_INFO_BUFFERING_START: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); - return true; - case MediaPlayer.MEDIA_INFO_BUFFERING_END: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); - return true; - case RemotePSMP.CAST_ERROR: - sendNotificationBroadcast(NOTIFICATION_TYPE_SHOW_TOAST, resourceId); - return true; - case RemotePSMP.CAST_ERROR_PRIORITY_HIGH: - Toast.makeText(PlaybackService.this, resourceId, Toast.LENGTH_SHORT).show(); - return true; - default: - return false; - } - } - - @Override - public boolean onMediaPlayerError(Object inObj, int what, int extra) { - final String TAG = "PlaybackSvc.onErrorLtsn"; - Log.w(TAG, "An error has occured: " + what + " " + extra); - if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, false); - } - sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); - writePlaybackPreferencesNoMediaPlaying(); - stopSelf(); - return true; - } - - @Override - public boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - PlaybackService.this.endPlayback(media, playNextEpisode, wasSkipped, switchingPlayers); - return true; - } - }; - - private void endPlayback(final Playable playable, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { - Log.d(TAG, "Playback ended" + (switchingPlayers ? " from switching players": "")); - - if (playable == null) { - Log.e(TAG, "Cannot end playback: media was null"); - return; - } - - taskManager.cancelPositionSaver(); - - boolean isInQueue = false; - FeedItem nextItem = null; - - if (playable instanceof FeedMedia && ((FeedMedia) playable).getItem() != null) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - - if (!switchingPlayers) { - try { - final List queue = taskManager.getQueue(); - isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId()); - nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue); - } catch (InterruptedException e) { - e.printStackTrace(); - // isInQueue remains false - } - - boolean shouldKeep = wasSkipped && UserPreferences.shouldSkipKeepEpisode(); - - if (!shouldKeep) { - // only mark the item as played if we're not keeping it anyways - DBWriter.markItemPlayed(item, FeedItem.PLAYED, true); - - if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item, true); - } - - // Delete episode if enabled - if (item.getFeed().getPreferences().getCurrentAutoDelete()) { - DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); - Log.d(TAG, "Episode Deleted"); - } - } - } - - - DBWriter.addItemToPlaybackHistory(media); - - // auto-flattr if enabled - if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { - DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); - } - - // gpodder play action - if(GpodnetPreferences.loggedIn()) { - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) - .currentDeviceId() - .currentTimestamp() - .started(startPosition / 1000) - .position(getDuration() / 1000) - .total(getDuration() / 1000) - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } - } - - if (!switchingPlayers) { - // Load next episode if previous episode was in the queue and if there - // is an episode in the queue left. - // Start playback immediately if continuous playback is enabled - Playable nextMedia = null; - boolean loadNextItem = ClientConfig.playbackServiceCallbacks.useQueue() && - isInQueue && - nextItem != null; - - playNextEpisode = playNextEpisode && - loadNextItem && - UserPreferences.isFollowQueue(); - - if (loadNextItem) { - Log.d(TAG, "Loading next item in queue"); - nextMedia = nextItem.getMedia(); - } - final boolean prepareImmediately; - final boolean startWhenPrepared; - final boolean stream; - - if (playNextEpisode) { - Log.d(TAG, "Playback of next episode will start immediately."); - prepareImmediately = startWhenPrepared = true; - } else { - Log.d(TAG, "No more episodes available to play"); - prepareImmediately = startWhenPrepared = false; - stopForeground(true); - stopWidgetUpdater(); - } - - writePlaybackPreferencesNoMediaPlaying(); - if (nextMedia != null) { - stream = !nextMedia.localFileAvailable(); - mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - isCasting ? EXTRA_CODE_CAST : - (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); - } else { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); - mediaPlayer.stop(); - //stopSelf(); - } - } - } - - public void setSleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) { - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds"); - taskManager.setSleepTimer(waitingTime, shakeToReset, vibrate); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - public void disableSleepTimer() { - taskManager.disableSleepTimer(); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - private void writePlaybackPreferencesNoMediaPlaying() { - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putInt( - PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, - PlaybackPreferences.PLAYER_STATUS_OTHER); - editor.commit(); - } - - private int getCurrentPlayerStatusAsInt(PlayerStatus playerStatus) { - int playerStatusAsInt; - switch (playerStatus) { - case PLAYING: - playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PLAYING; - break; - case PAUSED: - playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PAUSED; - break; - default: - playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_OTHER; - } - return playerStatusAsInt; - } - - private void writePlaybackPreferences() { - Log.d(TAG, "Writing playback preferences"); - - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); - MediaType mediaType = mediaPlayer.getCurrentMediaType(); - boolean stream = mediaPlayer.isStreaming(); - int playerStatus = getCurrentPlayerStatusAsInt(info.playerStatus); - - if (info.playable != null) { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - info.playable.getPlayableType()); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, - stream); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, - mediaType == MediaType.VIDEO); - if (info.playable instanceof FeedMedia) { - FeedMedia fMedia = (FeedMedia) info.playable; - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - fMedia.getItem().getFeed().getId()); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - fMedia.getId()); - } else { - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - info.playable.writeToPreferences(editor); - } else { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - editor.putInt( - PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus); - - editor.commit(); - } - - private void writePlayerStatusPlaybackPreferences() { - Log.d(TAG, "Writing player status playback preferences"); - - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - int playerStatus = getCurrentPlayerStatusAsInt(mediaPlayer.getPlayerStatus()); - - editor.putInt( - PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus); - - editor.commit(); - } - - /** - * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. - */ - private void postStatusUpdateIntent() { - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); - } - - private void sendNotificationBroadcast(int type, int code) { - Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); - intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); - intent.putExtra(EXTRA_NOTIFICATION_CODE, code); - sendBroadcast(intent); - } - - /** - * Updates the Media Session for the corresponding status. - * @param playerStatus the current {@link PlayerStatus} - */ - private void updateMediaSession(final PlayerStatus playerStatus) { - PlaybackStateCompat.Builder sessionState = new PlaybackStateCompat.Builder(); - - int state; - if (playerStatus != null) { - switch (playerStatus) { - case PLAYING: - state = PlaybackStateCompat.STATE_PLAYING; - break; - case PREPARED: - case PAUSED: - state = PlaybackStateCompat.STATE_PAUSED; - break; - case STOPPED: - state = PlaybackStateCompat.STATE_STOPPED; - break; - case SEEKING: - state = PlaybackStateCompat.STATE_FAST_FORWARDING; - break; - case PREPARING: - case INITIALIZING: - state = PlaybackStateCompat.STATE_CONNECTING; - break; - case INITIALIZED: - case INDETERMINATE: - state = PlaybackStateCompat.STATE_NONE; - break; - case ERROR: - state = PlaybackStateCompat.STATE_ERROR; - break; - default: - state = PlaybackStateCompat.STATE_NONE; - break; - } - } else { - state = PlaybackStateCompat.STATE_NONE; - } - sessionState.setState(state, mediaPlayer.getPosition(), mediaPlayer.getPlaybackSpeed()); - sessionState.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE - | PlaybackStateCompat.ACTION_REWIND - | PlaybackStateCompat.ACTION_FAST_FORWARD - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT); - mediaSession.setPlaybackState(sessionState.build()); - } - - /** - * Used by updateMediaSessionMetadata to load notification data in another thread. - */ - private Thread mediaSessionSetupThread; - - private void updateMediaSessionMetadata(final Playable p) { - if (p == null || mediaSession == null) { - return; - } - if (mediaSessionSetupThread != null) { - mediaSessionSetupThread.interrupt(); - } - - Runnable mediaSessionSetupTask = () -> { - MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); - builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle()); - builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle()); - builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration()); - builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle()); - builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle()); - - if (p.getImageLocation() != null && UserPreferences.setLockscreenBackground()) { - builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, p.getImageLocation().toString()); - try { - if (isCasting) { - Bitmap art = Glide.with(this) - .load(p.getImageLocation()) - .asBitmap() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) - .get(); - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art); - } else { - WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - Display display = wm.getDefaultDisplay(); - Bitmap art = Glide.with(this) - .load(p.getImageLocation()) - .asBitmap() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .centerCrop() - .into(display.getWidth(), display.getHeight()) - .get(); - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art); - } - } catch (Throwable tr) { - Log.e(TAG, Log.getStackTraceString(tr)); - } - } - if (!Thread.currentThread().isInterrupted() && started) { - mediaSession.setMetadata(builder.build()); - } - }; - - mediaSessionSetupThread = new Thread(mediaSessionSetupTask); - mediaSessionSetupThread.start(); - } - - /** - * Used by setupNotification to load notification data in another thread. - */ - private Thread notificationSetupThread; - - /** - * Prepares notification and starts the service in the foreground. - */ - private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { - final PendingIntent pIntent = PendingIntent.getActivity(this, 0, - PlaybackService.getPlayerActivityIntent(this), - PendingIntent.FLAG_UPDATE_CURRENT); - - if (notificationSetupThread != null) { - notificationSetupThread.interrupt(); - } - Runnable notificationSetupTask = new Runnable() { - Bitmap icon = null; - - @Override - public void run() { - Log.d(TAG, "Starting background work"); - if (android.os.Build.VERSION.SDK_INT >= 11) { - if (info.playable != null) { - int iconSize = getResources().getDimensionPixelSize( - android.R.dimen.notification_large_icon_width); - try { - icon = Glide.with(PlaybackService.this) - .load(info.playable.getImageLocation()) - .asBitmap() - .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .centerCrop() - .into(iconSize, iconSize) - .get(); - } catch (Throwable tr) { - Log.e(TAG, "Error loading the media icon for the notification", tr); - } - } - } - if (icon == null) { - icon = BitmapFactory.decodeResource(getApplicationContext().getResources(), - ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext())); - } - - if (mediaPlayer == null) { - return; - } - PlayerStatus playerStatus = mediaPlayer.getPlayerStatus(); - final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()); - - if (!Thread.currentThread().isInterrupted() && started && info.playable != null) { - String contentText = info.playable.getEpisodeTitle(); - String contentTitle = info.playable.getFeedTitle(); - Notification notification; - - // Builder is v7, even if some not overwritten methods return its parent's v4 interface - NotificationCompat.Builder notificationBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setOngoing(false) - .setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(smallIcon) - .setWhen(0) // we don't need the time - .setPriority(UserPreferences.getNotifyPriority()); // set notification priority - IntList compactActionList = new IntList(); - - int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction - - if (isCasting) { - Intent stopCastingIntent = new Intent(PlaybackService.this, PlaybackService.class); - stopCastingIntent.putExtra(EXTRA_CAST_DISCONNECT, true); - PendingIntent stopCastingPendingIntent = PendingIntent.getService(PlaybackService.this, - numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT); - notificationBuilder.addAction(R.drawable.ic_media_cast_disconnect, - getString(R.string.cast_disconnect_label), - stopCastingPendingIntent); - numActions++; - } - - // always let them rewind - PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_REWIND, numActions); - notificationBuilder.addAction(android.R.drawable.ic_media_rew, - getString(R.string.rewind_label), - rewindButtonPendingIntent); - if(UserPreferences.showRewindOnCompactNotification()) { - compactActionList.add(numActions); - } - numActions++; - - if (playerStatus == PlayerStatus.PLAYING) { - PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_PAUSE, numActions); - notificationBuilder.addAction(android.R.drawable.ic_media_pause, //pause action - getString(R.string.pause_label), - pauseButtonPendingIntent); - compactActionList.add(numActions++); - } else { - PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_PLAY, numActions); - notificationBuilder.addAction(android.R.drawable.ic_media_play, //play action - getString(R.string.play_label), - playButtonPendingIntent); - compactActionList.add(numActions++); - } - - // ff follows play, then we have skip (if it's present) - PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions); - notificationBuilder.addAction(android.R.drawable.ic_media_ff, - getString(R.string.fast_forward_label), - ffButtonPendingIntent); - if(UserPreferences.showFastForwardOnCompactNotification()) { - compactActionList.add(numActions); - } - numActions++; - - if (UserPreferences.isFollowQueue()) { - PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_NEXT, numActions); - notificationBuilder.addAction(android.R.drawable.ic_media_next, - getString(R.string.skip_episode_label), - skipButtonPendingIntent); - if(UserPreferences.showSkipOnCompactNotification()) { - compactActionList.add(numActions); - } - numActions++; - } - - PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_STOP, numActions); - notificationBuilder.setStyle(new android.support.v7.app.NotificationCompat.MediaStyle() - .setMediaSession(mediaSession.getSessionToken()) - .setShowActionsInCompactView(compactActionList.toArray()) - .setShowCancelButton(true) - .setCancelButtonIntent(stopButtonPendingIntent)) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .setColor(Notification.COLOR_DEFAULT); - - notification = notificationBuilder.build(); - - if (playerStatus == PlayerStatus.PLAYING || - playerStatus == PlayerStatus.PREPARING || - playerStatus == PlayerStatus.SEEKING || - isCasting) { - startForeground(NOTIFICATION_ID, notification); - } else { - stopForeground(false); - NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - mNotificationManager.notify(NOTIFICATION_ID, notification); - } - Log.d(TAG, "Notification set up"); - } - } - }; - notificationSetupThread = new Thread(notificationSetupTask); - notificationSetupThread.start(); - } - - private PendingIntent getPendingIntentForMediaAction(int keycodeValue, int requestCode) { - Intent intent = new Intent( - PlaybackService.this, PlaybackService.class); - intent.putExtra( - MediaButtonReceiver.EXTRA_KEYCODE, - keycodeValue); - return PendingIntent - .getService(PlaybackService.this, requestCode, - intent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - - /** - * Persists the current position and last played time of the media file. - * - * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects - * @param deltaPlayedDuration value by which played_duration should be increased. - */ - private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { - int position = getCurrentPosition(); - int duration = getDuration(); - float playbackSpeed = getCurrentPlaybackSpeed(); - final Playable playable = mediaPlayer.getPlayable(); - if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { - Log.d(TAG, "Saving current position to " + position); - if (updatePlayedDuration && playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - media.setPlayedDuration(media.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); - // Auto flattr - if (isAutoFlattrable(media) && - (media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { - Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.getPlayedDuration()) - + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); - DBTasks.flattrItemIfLoggedIn(this, item); - } - } - playable.saveCurrentPosition( - PreferenceManager.getDefaultSharedPreferences(getApplicationContext()), - position, - System.currentTimeMillis()); - } - } - - private void stopWidgetUpdater() { - taskManager.cancelWidgetUpdater(); - sendBroadcast(new Intent(STOP_WIDGET_UPDATE)); - } - - private void updateWidget() { - PlaybackService.this.sendBroadcast(new Intent( - FORCE_WIDGET_UPDATE)); - } - - public boolean sleepTimerActive() { - return taskManager.isSleepTimerActive(); - } - - public long getSleepTimerTimeLeft() { - return taskManager.getSleepTimerTimeLeft(); - } - - private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { - boolean isPlaying = false; - - if (info.playerStatus == PlayerStatus.PLAYING) { - isPlaying = true; - } - - if (info.playable != null) { - Intent i = new Intent(whatChanged); - i.putExtra("id", 1); - i.putExtra("artist", ""); - i.putExtra("album", info.playable.getFeedTitle()); - i.putExtra("track", info.playable.getEpisodeTitle()); - i.putExtra("playing", isPlaying); - final List queue = taskManager.getQueueIfLoaded(); - if (queue != null) { - i.putExtra("ListSize", queue.size()); - } - i.putExtra("duration", info.playable.getDuration()); - i.putExtra("position", info.playable.getPosition()); - sendBroadcast(i); - } - } - - /** - * Pauses playback when the headset is disconnected and the preference is - * set - */ - private final BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { - private static final String TAG = "headsetDisconnected"; - private static final int UNPLUGGED = 0; - private static final int PLUGGED = 1; - - @Override - public void onReceive(Context context, Intent intent) { - if (TextUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { - int state = intent.getIntExtra("state", -1); - if (state != -1) { - Log.d(TAG, "Headset plug event. State is " + state); - if (state == UNPLUGGED) { - Log.d(TAG, "Headset was unplugged during playback."); - pauseIfPauseOnDisconnect(); - } else if (state == PLUGGED) { - Log.d(TAG, "Headset was plugged in during playback."); - unpauseIfPauseOnDisconnect(false); - } - } else { - Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); - } - } - } - }; - - private final BroadcastReceiver bluetoothStateUpdated = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - if (TextUtils.equals(intent.getAction(), BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { - int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1); - if (state == BluetoothA2dp.STATE_CONNECTED) { - Log.d(TAG, "Received bluetooth connection intent"); - unpauseIfPauseOnDisconnect(true); - } - } - } - } - }; - - private final BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - // sound is about to change, eg. bluetooth -> speaker - Log.d(TAG, "Pausing playback because audio is becoming noisy"); - pauseIfPauseOnDisconnect(); - } - // android.media.AUDIO_BECOMING_NOISY - }; - - /** - * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. - */ - private void pauseIfPauseOnDisconnect() { - if (UserPreferences.isPauseOnHeadsetDisconnect()) { - if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { - transientPause = true; - } - mediaPlayer.pause(!UserPreferences.isPersistNotify(), true); - } - } - - /** - * @param bluetooth true if the event for unpausing came from bluetooth - */ - private void unpauseIfPauseOnDisconnect(boolean bluetooth) { - if (transientPause) { - transientPause = false; - if (!bluetooth && UserPreferences.isUnpauseOnHeadsetReconnect()) { - mediaPlayer.resume(); - } else if (bluetooth && UserPreferences.isUnpauseOnBluetoothReconnect()){ - // let the user know we've started playback again... - Vibrator v = (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE); - if(v != null) { - v.vibrate(500); - } - mediaPlayer.resume(); - } - } - } - - private final BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - stopSelf(); - } - } - - }; - - private final BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (TextUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { - Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); - mediaPlayer.endPlayback(true, false); - } - } - }; - - private final BroadcastReceiver pauseResumeCurrentEpisodeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (TextUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) { - Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent"); - mediaPlayer.resume(); - } - } - }; - - private final BroadcastReceiver pausePlayCurrentEpisodeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (TextUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) { - Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent"); - mediaPlayer.pause(false, false); - } - } - }; - - public static MediaType getCurrentMediaType() { - return currentMediaType; - } - - public static boolean isCasting() { - return isCasting; - } - - public void resume() { - mediaPlayer.resume(); - } - - public void prepare() { - mediaPlayer.prepare(); - } - - public void pause(boolean abandonAudioFocus, boolean reinit) { - mediaPlayer.pause(abandonAudioFocus, reinit); - } - - public void reinit() { - mediaPlayer.reinit(); - } - - public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() { - return mediaPlayer.getPSMPInfo(); - } - - public PlayerStatus getStatus() { - return mediaPlayer.getPlayerStatus(); - } - - public Playable getPlayable() { return mediaPlayer.getPlayable(); } - - public boolean canSetSpeed() { - return mediaPlayer.canSetSpeed(); - } - - public void setSpeed(float speed) { - mediaPlayer.setSpeed(speed); - } - - public void setVolume(float leftVolume, float rightVolume) { - mediaPlayer.setVolume(leftVolume, rightVolume); - } - - public float getCurrentPlaybackSpeed() { - return mediaPlayer.getPlaybackSpeed(); - } - - public boolean canDownmix() { - return mediaPlayer.canDownmix(); - } - - public void setDownmix(boolean enable) { - mediaPlayer.setDownmix(enable); - } - - public boolean isStartWhenPrepared() { - return mediaPlayer.isStartWhenPrepared(); - } - - public void setStartWhenPrepared(boolean s) { - mediaPlayer.setStartWhenPrepared(s); - } - - - public void seekTo(final int t) { - if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING - && GpodnetPreferences.loggedIn()) { - final Playable playable = mediaPlayer.getPlayable(); - if (playable instanceof FeedMedia) { - FeedMedia media = (FeedMedia) playable; - FeedItem item = media.getItem(); - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) - .currentDeviceId() - .currentTimestamp() - .started(startPosition / 1000) - .position(getCurrentPosition() / 1000) - .total(getDuration() / 1000) - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); - } - } - mediaPlayer.seekTo(t); - if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING ) { - startPosition = t; - } - } - - - public void seekDelta(final int d) { - mediaPlayer.seekDelta(d); - } - - /** - * @see LocalPSMP#seekToChapter(de.danoeh.antennapod.core.feed.Chapter) - */ - public void seekToChapter(Chapter c) { - mediaPlayer.seekToChapter(c); - } - - /** - * call getDuration() on mediaplayer or return INVALID_TIME if player is in - * an invalid state. - */ - public int getDuration() { - return mediaPlayer.getDuration(); - } - - /** - * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player - * is in an invalid state. - */ - public int getCurrentPosition() { - return mediaPlayer.getPosition(); - } - - public boolean isStreaming() { - return mediaPlayer.isStreaming(); - } - - public Pair getVideoSize() { - return mediaPlayer.getVideoSize(); - } - - private boolean isAutoFlattrable(FeedMedia media) { - if (media != null) { - FeedItem item = media.getItem(); - return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); - } else { - return false; - } - } - - private CastConsumer castConsumer = new DefaultCastConsumer() { - @Override - public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) { - PlaybackService.this.onCastAppConnected(wasLaunched); - } - - @Override - public void onDisconnectionReason(int reason) { - Log.d(TAG, "onDisconnectionReason() with code " + reason); - // This is our final chance to update the underlying stream position - // In onDisconnected(), the underlying CastPlayback#mVideoCastConsumer - // is disconnected and hence we update our local value of stream position - // to the latest position. - if (mediaPlayer != null) { - saveCurrentPosition(false, 0); - infoBeforeCastDisconnection = mediaPlayer.getPSMPInfo(); - if (reason != BaseCastManager.DISCONNECT_REASON_EXPLICIT && - infoBeforeCastDisconnection.playerStatus == PlayerStatus.PLAYING) { - // If it's NOT based on user action, we shouldn't automatically resume local playback - infoBeforeCastDisconnection.playerStatus = PlayerStatus.PAUSED; - } - } - } - - @Override - public void onDisconnected() { - Log.d(TAG, "onDisconnected()"); - isCasting = false; - PlaybackServiceMediaPlayer.PSMPInfo info = infoBeforeCastDisconnection; - infoBeforeCastDisconnection = null; - if (info == null && mediaPlayer != null) { - info = mediaPlayer.getPSMPInfo(); - } - if (info == null) { - info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null); - } - switchMediaPlayer(new LocalPSMP(PlaybackService.this, mediaPlayerCallback), - info, true); - if (info.playable != null) { - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - info.playable.getMediaType() == MediaType.AUDIO ? EXTRA_CODE_AUDIO : EXTRA_CODE_VIDEO); - } else { - Log.d(TAG, "Cast session disconnected, but no current media"); - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); - } - // hardware volume buttons control the local device volume - mediaRouter.setMediaSessionCompat(null); - unregisterWifiBroadcastReceiver(); - PlayerStatus status = info.playerStatus; - if ((status == PlayerStatus.PLAYING || - status == PlayerStatus.SEEKING || - status == PlayerStatus.PREPARING || - UserPreferences.isPersistNotify()) && - android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - setupNotification(info); - } else if (!UserPreferences.isPersistNotify()){ - stopForeground(true); - } - } - }; - - private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() { - - private static final String TAG = "MediaSessionCompat"; - - @Override - public void onPlay() { - Log.d(TAG, "onPlay()"); - PlayerStatus status = getStatus(); - if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - resume(); - } else if (status == PlayerStatus.INITIALIZED) { - setStartWhenPrepared(true); - prepare(); - } - } - - @Override - public void onPause() { - Log.d(TAG, "onPause()"); - if (getStatus() == PlayerStatus.PLAYING) { - pause(false, true); - } - if (UserPreferences.isPersistNotify()) { - pause(false, true); - } else { - pause(true, true); - } - } - - @Override - public void onStop() { - Log.d(TAG, "onStop()"); - mediaPlayer.stop(); - } - - @Override - public void onSkipToPrevious() { - Log.d(TAG, "onSkipToPrevious()"); - seekDelta(-UserPreferences.getRewindSecs() * 1000); - } - - @Override - public void onRewind() { - Log.d(TAG, "onRewind()"); - seekDelta(-UserPreferences.getRewindSecs() * 1000); - } - - @Override - public void onFastForward() { - Log.d(TAG, "onFastForward()"); - seekDelta(UserPreferences.getFastFowardSecs() * 1000); - } - - @Override - public void onSkipToNext() { - Log.d(TAG, "onSkipToNext()"); - if(UserPreferences.shouldHardwareButtonSkip()) { - mediaPlayer.endPlayback(true, false); - } else { - seekDelta(UserPreferences.getFastFowardSecs() * 1000); - } - } - - - @Override - public void onSeekTo(long pos) { - Log.d(TAG, "onSeekTo()"); - seekTo((int) pos); - } - - @Override - public boolean onMediaButtonEvent(final Intent mediaButton) { - Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")"); - if (mediaButton != null) { - KeyEvent keyEvent = (KeyEvent) mediaButton.getExtras().get(Intent.EXTRA_KEY_EVENT); - if (keyEvent != null && - keyEvent.getAction() == KeyEvent.ACTION_DOWN && - keyEvent.getRepeatCount() == 0){ - handleKeycode(keyEvent.getKeyCode(), keyEvent.getSource()); - } - } - return false; - } - }; - - private void onCastAppConnected(boolean wasLaunched) { - Log.d(TAG, "A cast device application was " + (wasLaunched ? "launched" : "joined")); - isCasting = true; - PlaybackServiceMediaPlayer.PSMPInfo info = null; - if (mediaPlayer != null) { - info = mediaPlayer.getPSMPInfo(); - if (info.playerStatus == PlayerStatus.PLAYING) { - // could be pause, but this way we make sure the new player will get the correct position, - // since pause runs asynchronously and we could be directing the new player to play even before - // the old player gives us back the position. - saveCurrentPosition(false, 0); - } - } - if (info == null) { - info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null); - } - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, EXTRA_CODE_CAST); - switchMediaPlayer(new RemotePSMP(PlaybackService.this, mediaPlayerCallback), - info, - wasLaunched); - // hardware volume buttons control the remote device volume - mediaRouter.setMediaSessionCompat(mediaSession); - registerWifiBroadcastReceiver(); - setupNotification(info); - } - - private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer, - @NonNull PlaybackServiceMediaPlayer.PSMPInfo info, - boolean wasLaunched) { - if (mediaPlayer != null) { - mediaPlayer.endPlayback(true, true); - mediaPlayer.shutdownQuietly(); - } - mediaPlayer = newPlayer; - Log.d(TAG, "switched to " + mediaPlayer.getClass().getSimpleName()); - if (!wasLaunched) { - PlaybackServiceMediaPlayer.PSMPInfo candidate = mediaPlayer.getPSMPInfo(); - if (candidate.playable != null && - candidate.playerStatus.isAtLeast(PlayerStatus.PREPARING)) { - // do not automatically send new media to cast device - info.playable = null; - } - } - if (info.playable != null) { - mediaPlayer.playMediaObject(info.playable, - !info.playable.localFileAvailable(), - info.playerStatus == PlayerStatus.PLAYING, - info.playerStatus.isAtLeast(PlayerStatus.PREPARING)); - } - } - - private void registerWifiBroadcastReceiver() { - if (wifiBroadcastReceiver != null) { - return; - } - wifiBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { - NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); - boolean isConnected = info.isConnected(); - //apparently this method gets called twice when a change happens, but one run is enough. - if (isConnected && !wifiConnectivity) { - wifiConnectivity = true; - castManager.startCastDiscovery(); - castManager.reconnectSessionIfPossible(RECONNECTION_ATTEMPT_PERIOD_S, NetworkUtils.getWifiSsid()); - } else { - wifiConnectivity = isConnected; - } - } - } - }; - registerReceiver(wifiBroadcastReceiver, - new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION)); - } - - private void unregisterWifiBroadcastReceiver() { - if (wifiBroadcastReceiver != null) { - unregisterReceiver(wifiBroadcastReceiver); - wifiBroadcastReceiver = null; - } - } - - private SharedPreferences.OnSharedPreferenceChangeListener prefListener = - (sharedPreferences, key) -> { - if (UserPreferences.PREF_CAST_ENABLED.equals(key)) { - if (!UserPreferences.isCastEnabled()) { - if (castManager.isConnecting() || castManager.isConnected()) { - Log.d(TAG, "Disconnecting cast device due to a change in user preferences"); - castManager.disconnect(); - } - } - } else if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) { - updateMediaSessionMetadata(getPlayable()); - } - }; -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java deleted file mode 100644 index 4262b8a70..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/RemotePSMP.java +++ /dev/null @@ -1,592 +0,0 @@ -package de.danoeh.antennapod.core.service.playback; - -import android.content.Context; -import android.media.MediaPlayer; -import android.support.annotation.NonNull; -import android.util.Log; -import android.util.Pair; -import android.view.SurfaceHolder; - -import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.CastStatusCodes; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaStatus; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.CastException; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.NoConnectionException; -import com.google.android.libraries.cast.companionlibrary.cast.exceptions.TransientNetworkDisconnectionException; - -import java.util.concurrent.atomic.AtomicBoolean; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.cast.CastConsumer; -import de.danoeh.antennapod.core.cast.CastManager; -import de.danoeh.antennapod.core.cast.CastUtils; -import de.danoeh.antennapod.core.cast.DefaultCastConsumer; -import de.danoeh.antennapod.core.cast.RemoteMedia; -import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.feed.MediaType; -import de.danoeh.antennapod.core.util.RewindAfterPauseUtils; -import de.danoeh.antennapod.core.util.playback.Playable; - -/** - * Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices. - */ -public class RemotePSMP extends PlaybackServiceMediaPlayer { - - public static final String TAG = "RemotePSMP"; - - public static final int CAST_ERROR = 3001; - - public static final int CAST_ERROR_PRIORITY_HIGH = 3005; - - private final CastManager castMgr; - - private volatile Playable media; - private volatile MediaInfo remoteMedia; - private volatile MediaType mediaType; - - private final AtomicBoolean isBuffering; - - private final AtomicBoolean startWhenPrepared; - - public RemotePSMP(@NonNull Context context, @NonNull PSMPCallback callback) { - super(context, callback); - - castMgr = CastManager.getInstance(); - media = null; - mediaType = null; - startWhenPrepared = new AtomicBoolean(false); - isBuffering = new AtomicBoolean(false); - - try { - if (castMgr.isConnected() && castMgr.isRemoteMediaLoaded()) { - // updates the state, but does not start playing new media if it was going to - onRemoteMediaPlayerStatusUpdated( - ((p, playNextEpisode, wasSkipped, switchingPlayers) -> - this.callback.endPlayback(p, false, wasSkipped, switchingPlayers))); - } - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to do initial check for loaded media", e); - } - - castMgr.addCastConsumer(castConsumer); - //TODO - } - - private CastConsumer castConsumer = new DefaultCastConsumer() { - @Override - public void onRemoteMediaPlayerMetadataUpdated() { - RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(callback::endPlayback); - } - - @Override - public void onRemoteMediaPlayerStatusUpdated() { - RemotePSMP.this.onRemoteMediaPlayerStatusUpdated(callback::endPlayback); - } - - @Override - public void onMediaLoadResult(int statusCode) { - if (playerStatus == PlayerStatus.PREPARING) { - if (statusCode == CastStatusCodes.SUCCESS) { - setPlayerStatus(PlayerStatus.PREPARED, media); - if (media.getDuration() == 0) { - Log.d(TAG, "Setting duration of media"); - try { - media.setDuration((int) castMgr.getMediaDuration()); - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to get remote media's duration"); - } - } - } else if (statusCode != CastStatusCodes.REPLACED){ - Log.d(TAG, "Remote media failed to load"); - setPlayerStatus(PlayerStatus.INITIALIZED, media); - } - } else { - Log.d(TAG, "onMediaLoadResult called, but Player Status wasn't in preparing state, so we ignore the result"); - } - } - - @Override - public void onApplicationStatusChanged(String appStatus) { - if (playerStatus != PlayerStatus.PLAYING) { - Log.d(TAG, "onApplicationStatusChanged, but no media was playing"); - return; - } - boolean playbackEnded = false; - try { - int standbyState = castMgr.getApplicationStandbyState(); - Log.d(TAG, "standbyState: " + standbyState); - playbackEnded = standbyState == Cast.STANDBY_STATE_YES; - } catch (IllegalStateException e) { - Log.d(TAG, "unable to get standbyState on onApplicationStatusChanged()"); - } - if (playbackEnded) { - setPlayerStatus(PlayerStatus.INDETERMINATE, media); - callback.endPlayback(media, true, false, false); - } - } - - @Override - public void onFailed(int resourceId, int statusCode) { - callback.onMediaPlayerInfo(CAST_ERROR, resourceId); - } - }; - - private void setBuffering(boolean buffering) { - if (buffering && isBuffering.compareAndSet(false, true)) { - callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_START, 0); - } else if (!buffering && isBuffering.compareAndSet(true, false)) { - callback.onMediaPlayerInfo(MediaPlayer.MEDIA_INFO_BUFFERING_END, 0); - } - } - - private Playable localVersion(MediaInfo info){ - if (info == null) { - return null; - } - if (CastUtils.matches(info, media)) { - return media; - } - return CastUtils.getPlayable(info, true); - } - - private MediaInfo remoteVersion(Playable playable) { - if (playable == null) { - return null; - } - if (CastUtils.matches(remoteMedia, playable)) { - return remoteMedia; - } - if (playable instanceof FeedMedia) { - return CastUtils.convertFromFeedMedia((FeedMedia) playable); - } - if (playable instanceof RemoteMedia) { - return ((RemoteMedia) playable).extractMediaInfo(); - } - return null; - } - - private void onRemoteMediaPlayerStatusUpdated(@NonNull EndPlaybackCall endPlaybackCall) { - MediaStatus status = castMgr.getMediaStatus(); - if (status == null) { - Log.d(TAG, "Received null MediaStatus"); - //setBuffering(false); - //setPlayerStatus(PlayerStatus.INDETERMINATE, null); - return; - } else { - Log.d(TAG, "Received remote status/media update. New state=" + status.getPlayerState()); - } - Playable currentMedia = localVersion(status.getMediaInfo()); - boolean updateUI = currentMedia != media; - if (currentMedia != null) { - long position = status.getStreamPosition(); - if (position > 0 && currentMedia.getPosition() == 0) { - currentMedia.setPosition((int) position); - } - } - int state = status.getPlayerState(); - setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING); - switch (state) { - case MediaStatus.PLAYER_STATE_PLAYING: - setPlayerStatus(PlayerStatus.PLAYING, currentMedia); - break; - case MediaStatus.PLAYER_STATE_PAUSED: - setPlayerStatus(PlayerStatus.PAUSED, currentMedia); - break; - case MediaStatus.PLAYER_STATE_BUFFERING: - setPlayerStatus(playerStatus, currentMedia); - break; - case MediaStatus.PLAYER_STATE_IDLE: - int reason = status.getIdleReason(); - switch (reason) { - case MediaStatus.IDLE_REASON_CANCELED: - // check if we're already loading something else - if (!updateUI || media == null) { - setPlayerStatus(PlayerStatus.STOPPED, currentMedia); - } else { - updateUI = false; - } - break; - case MediaStatus.IDLE_REASON_INTERRUPTED: - // check if we're already loading something else - if (!updateUI || media == null) { - setPlayerStatus(PlayerStatus.PREPARING, currentMedia); - } else { - updateUI = false; - } - break; - case MediaStatus.IDLE_REASON_NONE: - setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia); - break; - case MediaStatus.IDLE_REASON_FINISHED: - boolean playing = playerStatus == PlayerStatus.PLAYING; - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); - endPlaybackCall.endPlayback(currentMedia,playing, false, false); - // endPlayback already updates the UI, so no need to trigger it ourselves - updateUI = false; - break; - case MediaStatus.IDLE_REASON_ERROR: - Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode..."); - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); - callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, - R.string.cast_failed_media_error_skipping); - endPlaybackCall.endPlayback(currentMedia, startWhenPrepared.get(), true, false); - // endPlayback already updates the UI, so no need to trigger it ourselves - updateUI = false; - } - break; - case MediaStatus.PLAYER_STATE_UNKNOWN: - //is this right? - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); - break; - default: - Log.e(TAG, "Remote media state undetermined!"); - setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia); - } - if (updateUI) { - callback.onMediaChanged(true); - } - } - - @Override - public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - Log.d(TAG, "playMediaObject() called"); - playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); - } - - /** - * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if - * the given playable parameter is the same object as the currently playing media. - * - * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, boolean) - */ - private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - if (!CastUtils.isCastable(playable)) { - Log.d(TAG, "media provided is not compatible with cast device"); - callback.onMediaPlayerInfo(CAST_ERROR_PRIORITY_HIGH, R.string.cast_not_castable); - try { - playable.loadMetadata(); - } catch (Playable.PlayableException e) { - Log.e(TAG, "Unable to load metadata of playable", e); - } - callback.endPlayback(playable, startWhenPrepared, true, false); - return; - } - - if (media != null) { - if (!forceReset && media.getIdentifier().equals(playable.getIdentifier()) - && playerStatus == PlayerStatus.PLAYING) { - // episode is already playing -> ignore method call - Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); - return; - } else { - // set temporarily to pause in order to update list with current position - try { - if (castMgr.isRemoteMediaPlaying()) { - setPlayerStatus(PlayerStatus.PAUSED, media); - } - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to determine whether media was playing, falling back to stored player status", e); - // this might end up just being pointless if we need to query the remote device for the position - if (playerStatus == PlayerStatus.PLAYING) { - setPlayerStatus(PlayerStatus.PAUSED, media); - } - } - smartMarkAsPlayed(media); - - - setPlayerStatus(PlayerStatus.INDETERMINATE, null); - } - } - - this.media = playable; - remoteMedia = remoteVersion(playable); - //this.stream = stream; - this.mediaType = media.getMediaType(); - this.startWhenPrepared.set(startWhenPrepared); - setPlayerStatus(PlayerStatus.INITIALIZING, media); - try { - media.loadMetadata(); - callback.onMediaChanged(true); - setPlayerStatus(PlayerStatus.INITIALIZED, media); - if (prepareImmediately) { - prepare(); - } - } catch (Playable.PlayableException e) { - Log.e(TAG, "Error while loading media metadata", e); - setPlayerStatus(PlayerStatus.STOPPED, null); - } - } - - @Override - public void resume() { - try { - // TODO see comment on prepare() - // setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume()); - if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { - int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind( - media.getPosition(), - media.getLastPlayedTime()); - castMgr.play(newPosition); - } - castMgr.play(); - } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to resume remote playback", e); - } - } - - @Override - public void pause(boolean abandonFocus, boolean reinit) { - try { - if (castMgr.isRemoteMediaPlaying()) { - castMgr.pause(); - } - } catch (CastException | TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to pause", e); - } - } - - @Override - public void prepare() { - if (playerStatus == PlayerStatus.INITIALIZED) { - Log.d(TAG, "Preparing media player"); - setPlayerStatus(PlayerStatus.PREPARING, media); - try { - int position = media.getPosition(); - if (position > 0) { - position = RewindAfterPauseUtils.calculatePositionWithRewind( - position, - media.getLastPlayedTime()); - } - // TODO We're not supporting user set stream volume yet, as we need to make a UI - // that doesn't allow changing playback speed or have different values for left/right - //setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume()); - castMgr.loadMedia(remoteMedia, startWhenPrepared.get(), position); - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Error loading media", e); - setPlayerStatus(PlayerStatus.INITIALIZED, media); - } - } - } - - @Override - public void reinit() { - Log.d(TAG, "reinit() called"); - if (media != null) { - playMediaObject(media, true, false, startWhenPrepared.get(), false); - } else { - Log.d(TAG, "Call to reinit was ignored: media was null"); - } - } - - @Override - public void seekTo(int t) { - //TODO check other seek implementations and see if there's no issue with sending too many seek commands to the remote media player - try { - if (castMgr.isRemoteMediaLoaded()) { - setPlayerStatus(PlayerStatus.SEEKING, media); - castMgr.seek(t); - } else if (media != null && playerStatus == PlayerStatus.INITIALIZED){ - media.setPosition(t); - startWhenPrepared.set(false); - prepare(); - } - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to seek", e); - } - } - - @Override - public void seekDelta(int d) { - int position = getPosition(); - if (position != INVALID_TIME) { - seekTo(position + d); - } else { - Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); - } - } - - @Override - public int getDuration() { - int retVal = INVALID_TIME; - boolean prepared; - try { - prepared = castMgr.isRemoteMediaLoaded(); - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to check if remote media is loaded", e); - prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED); - } - if (prepared) { - try { - retVal = (int) castMgr.getMediaDuration(); - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to determine remote media's duration", e); - } - } - if(retVal == INVALID_TIME && media != null && media.getDuration() > 0) { - retVal = media.getDuration(); - } - Log.d(TAG, "getDuration() -> " + retVal); - return retVal; - } - - @Override - public int getPosition() { - int retVal = INVALID_TIME; - boolean prepared; - try { - prepared = castMgr.isRemoteMediaLoaded(); - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to check if remote media is loaded", e); - prepared = playerStatus.isAtLeast(PlayerStatus.PREPARED); - } - if (prepared) { - try { - retVal = (int) castMgr.getCurrentMediaPosition(); - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Unable to determine remote media's position", e); - } - } - if(retVal <= 0 && media != null && media.getPosition() >= 0) { - retVal = media.getPosition(); - } - Log.d(TAG, "getPosition() -> " + retVal); - return retVal; - } - - @Override - public boolean isStartWhenPrepared() { - return startWhenPrepared.get(); - } - - @Override - public void setStartWhenPrepared(boolean startWhenPrepared) { - this.startWhenPrepared.set(startWhenPrepared); - } - - //TODO I believe some parts of the code make the same decision skipping this check, so that - //should be changed as well - @Override - public boolean canSetSpeed() { - return false; - } - - @Override - public void setSpeed(float speed) { - throw new UnsupportedOperationException("Setting playback speed unsupported for Remote Playback"); - } - - @Override - public float getPlaybackSpeed() { - return 1; - } - - @Override - public void setVolume(float volumeLeft, float volumeRight) { - Log.d(TAG, "Setting the Stream volume on Remote Media Player"); - double volume = (volumeLeft+volumeRight)/2; - if (volume > 1.0) { - volume = 1.0; - } - if (volume < 0.0) { - volume = 0.0; - } - try { - castMgr.setStreamVolume(volume); - } catch (TransientNetworkDisconnectionException | NoConnectionException | CastException e) { - Log.e(TAG, "Unable to set the volume", e); - } - } - - @Override - public boolean canDownmix() { - return false; - } - - @Override - public void setDownmix(boolean enable) { - throw new UnsupportedOperationException("Setting downmix unsupported in Remote Media Player"); - } - - @Override - public MediaType getCurrentMediaType() { - return mediaType; - } - - @Override - public boolean isStreaming() { - return true; - } - - @Override - public void shutdown() { - castMgr.removeCastConsumer(castConsumer); - } - - @Override - public void shutdownQuietly() { - shutdown(); - } - - @Override - public void setVideoSurface(SurfaceHolder surface) { - throw new UnsupportedOperationException("Setting Video Surface unsupported in Remote Media Player"); - } - - @Override - public void resetVideoSurface() { - Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player"); - } - - @Override - public Pair getVideoSize() { - return null; - } - - @Override - public Playable getPlayable() { - return media; - } - - @Override - protected void setPlayable(Playable playable) { - if (playable != media) { - media = playable; - remoteMedia = remoteVersion(playable); - } - } - - @Override - public void endPlayback(boolean wasSkipped, boolean switchingPlayers) { - Log.d(TAG, "endPlayback() called"); - boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - try { - isPlaying = castMgr.isRemoteMediaPlaying(); - } catch (TransientNetworkDisconnectionException | NoConnectionException e) { - Log.e(TAG, "Could not determine if media is playing", e); - } - // TODO make sure we stop playback whenever there's no next episode. - if (playerStatus != PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.INDETERMINATE, media); - } - callback.endPlayback(media, isPlaying, wasSkipped, switchingPlayers); - } - - @Override - public void stop() { - if (playerStatus == PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.STOPPED, null); - } else { - Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); - } - } - - @Override - protected boolean shouldLockWifi() { - return false; - } - - private interface EndPlaybackCall { - boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java deleted file mode 100644 index 201efbc81..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java +++ /dev/null @@ -1,246 +0,0 @@ -package de.danoeh.antennapod.core.util.playback; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Parcelable; -import android.util.Log; - -import java.util.List; - -import de.danoeh.antennapod.core.asynctask.ImageResource; -import de.danoeh.antennapod.core.cast.RemoteMedia; -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.storage.DBReader; -import de.danoeh.antennapod.core.util.ShownotesProvider; - -/** - * Interface for objects that can be played by the PlaybackService. - */ -public interface Playable extends Parcelable, - ShownotesProvider, ImageResource { - - /** - * Save information about the playable in a preference so that it can be - * restored later via PlayableUtils.createInstanceFromPreferences. - * Implementations must NOT call commit() after they have written the values - * to the preferences file. - */ - void writeToPreferences(SharedPreferences.Editor prefEditor); - - /** - * This method is called from a separate thread by the PlaybackService. - * Playable objects should load their metadata in this method. This method - * should execute as quickly as possible and NOT load chapter marks if no - * local file is available. - */ - void loadMetadata() throws PlayableException; - - /** - * This method is called from a separate thread by the PlaybackService. - * Playable objects should load their chapter marks in this method if no - * local file was available when loadMetadata() was called. - */ - void loadChapterMarks(); - - /** - * Returns the title of the episode that this playable represents - */ - String getEpisodeTitle(); - - /** - * Returns a list of chapter marks or null if this Playable has no chapters. - */ - List getChapters(); - - /** - * Returns a link to a website that is meant to be shown in a browser - */ - String getWebsiteLink(); - - String getPaymentLink(); - - /** - * Returns the title of the feed this Playable belongs to. - */ - String getFeedTitle(); - - /** - * Returns a unique identifier, for example a file url or an ID from a - * database. - */ - Object getIdentifier(); - - /** - * Return duration of object or 0 if duration is unknown. - */ - int getDuration(); - - /** - * Return position of object or 0 if position is unknown. - */ - int getPosition(); - - /** - * Returns last time (in ms) when this playable was played or 0 - * if last played time is unknown. - */ - long getLastPlayedTime(); - - /** - * Returns the type of media. This method should return the correct value - * BEFORE loadMetadata() is called. - */ - MediaType getMediaType(); - - /** - * Returns an url to a local file that can be played or null if this file - * does not exist. - */ - String getLocalMediaUrl(); - - /** - * Returns an url to a file that can be streamed by the player or null if - * this url is not known. - */ - String getStreamUrl(); - - /** - * Returns true if a local file that can be played is available. getFileUrl - * MUST return a non-null string if this method returns true. - */ - boolean localFileAvailable(); - - /** - * Returns true if a streamable file is available. getStreamUrl MUST return - * a non-null string if this method returns true. - */ - boolean streamAvailable(); - - /** - * Saves the current position of this object. Implementations can use the - * provided SharedPreference to save this information and retrieve it later - * via PlayableUtils.createInstanceFromPreferences. - * - * @param pref shared prefs that might be used to store this object - * @param newPosition new playback position in ms - * @param timestamp current time in ms - */ - void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp); - - void setPosition(int newPosition); - - void setDuration(int newDuration); - - /** - * @param lastPlayedTimestamp timestamp in ms - */ - void setLastPlayedTime(long lastPlayedTimestamp); - - /** - * Is called by the PlaybackService when playback starts. - */ - void onPlaybackStart(); - - /** - * Is called by the PlaybackService when playback is completed. - */ - void onPlaybackCompleted(); - - /** - * Returns an integer that must be unique among all Playable classes. The - * return value is later used by PlayableUtils to determine the type of the - * Playable object that is restored. - */ - int getPlayableType(); - - void setChapters(List chapters); - - /** - * Provides utility methods for Playable objects. - */ - class PlayableUtils { - private static final String TAG = "PlayableUtils"; - - /** - * Restores a playable object from a sharedPreferences file. This method might load data from the database, - * depending on the type of playable that was restored. - * - * @param type An integer that represents the type of the Playable object - * that is restored. - * @param pref The SharedPreferences file from which the Playable object - * is restored - * @return The restored Playable object - */ - public static Playable createInstanceFromPreferences(Context context, int type, - SharedPreferences pref) { - Playable result = null; - // ADD new Playable types here: - switch (type) { - case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA: - result = createFeedMediaInstance(pref); - break; - case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA: - result = createExternalMediaInstance(pref); - break; - case RemoteMedia.PLAYABLE_TYPE_REMOTE_MEDIA: - result = createRemoteMediaInstance(pref); - break; - } - if (result == null) { - Log.e(TAG, "Could not restore Playable object from preferences"); - } - return result; - } - - private static Playable createFeedMediaInstance(SharedPreferences pref) { - Playable result = null; - long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); - if (mediaId != -1) { - result = DBReader.getFeedMedia(mediaId); - } - return result; - } - - private static Playable createExternalMediaInstance(SharedPreferences pref) { - Playable result = null; - String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, null); - String mediaType = pref.getString(ExternalMedia.PREF_MEDIA_TYPE, null); - if (source != null && mediaType != null) { - int position = pref.getInt(ExternalMedia.PREF_POSITION, 0); - long lastPlayedTime = pref.getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, 0); - result = new ExternalMedia(source, MediaType.valueOf(mediaType), - position, lastPlayedTime); - } - return result; - } - - private static Playable createRemoteMediaInstance(SharedPreferences pref) { - //TODO there's probably no point in restoring RemoteMedia from preferences, because we - //only care about it while it's playing on the cast device. - return null; - } - } - - class PlayableException extends Exception { - private static final long serialVersionUID = 1L; - - public PlayableException() { - super(); - } - - public PlayableException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public PlayableException(String detailMessage) { - super(detailMessage); - } - - public PlayableException(Throwable throwable) { - super(throwable); - } - - } -} -- cgit v1.2.3