diff --git a/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java b/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java new file mode 100644 index 000000000..5e714f02c --- /dev/null +++ b/app/src/free/java/de/danoeh/antennapod/config/CastCallbackImpl.java @@ -0,0 +1,7 @@ +package de.danoeh.antennapod.config; + +import de.danoeh.antennapod.core.CastCallbacks; + +public class CastCallbackImpl implements CastCallbacks { + +} diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java index 932b9d22f..f26f2ea76 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java +++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java @@ -16,5 +16,6 @@ public class ClientConfigurator { ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl(); ClientConfig.flattrCallbacks = new FlattrCallbacksImpl(); ClientConfig.dbTasksCallbacks = new DBTasksCallbacksImpl(); + ClientConfig.castCallbacks = new CastCallbackImpl(); } } diff --git a/app/src/play/java/de/danoeh/antennapod/cast/CustomMRControllerDialog.java b/app/src/play/java/de/danoeh/antennapod/cast/CustomMRControllerDialog.java new file mode 100644 index 000000000..7469e830a --- /dev/null +++ b/app/src/play/java/de/danoeh/antennapod/cast/CustomMRControllerDialog.java @@ -0,0 +1,226 @@ +package de.danoeh.antennapod.cast; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.RemoteException; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v7.app.MediaRouteControllerDialog; +import android.support.v7.media.MediaRouter; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import de.danoeh.antennapod.R; + +public class CustomMRControllerDialog extends MediaRouteControllerDialog { + public static final String TAG = "CustomMRContrDialog"; + + private MediaRouter mediaRouter; + private MediaSessionCompat.Token token; + + private ImageView artView; + private TextView titleView; + private TextView subtitleView; + private ImageButton playPauseButton; + private LinearLayout rootView; + + private MediaControllerCompat mediaController; + private MediaControllerCompat.Callback mediaControllerCallback; + + public CustomMRControllerDialog(Context context) { + this(context, 0); + } + + public CustomMRControllerDialog(Context context, int theme) { + super(context, theme); + mediaRouter = MediaRouter.getInstance(getContext()); + token = mediaRouter.getMediaSessionToken(); + try { + if (token != null) { + mediaController = new MediaControllerCompat(getContext(), token); + } + } catch (RemoteException e) { + Log.e(TAG, "Error creating media controller", e); + } + + if (mediaController != null) { + mediaControllerCallback = new MediaControllerCompat.Callback() { + @Override + public void onSessionDestroyed() { + if (mediaController != null) { + mediaController.unregisterCallback(mediaControllerCallback); + mediaController = null; + } + } + + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) { + updateViews(); + } + + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + updateState(); + } + }; + mediaController.registerCallback(mediaControllerCallback); + } + } + + @Override + public View onCreateMediaControlView(Bundle savedInstanceState) { + rootView = new LinearLayout(getContext()); + rootView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + rootView.setOrientation(LinearLayout.VERTICAL); + + artView = new ImageView(getContext()) { + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int desiredWidth = widthMeasureSpec; + int desiredHeight = heightMeasureSpec; + if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) { + Drawable drawable = getDrawable(); + if (drawable != null) { + int originalWidth = MeasureSpec.getSize(widthMeasureSpec); + int intrHeight = drawable.getIntrinsicHeight(); + int intrWidth = drawable.getIntrinsicWidth(); + float scale; + if (intrHeight*16 > intrWidth*9) { + // image is taller than 16:9 + scale = (float) originalWidth * 9 / 16 / intrHeight; + } else { + // image is more horizontal than 16:9 + scale = (float) originalWidth / intrWidth; + } + desiredHeight = MeasureSpec.makeMeasureSpec((int) (intrHeight * scale + 0.5f), MeasureSpec.EXACTLY); + } + } + + super.onMeasure(desiredWidth, desiredHeight); + } + }; + artView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + artView.setScaleType(ImageView.ScaleType.FIT_CENTER); + + rootView.addView(artView); + View playbackControlLayout = View.inflate(getContext(), R.layout.media_router_controller, rootView); + + titleView = (TextView) playbackControlLayout.findViewById(R.id.mrc_control_title); + subtitleView = (TextView) playbackControlLayout.findViewById(R.id.mrc_control_subtitle); + playPauseButton = (ImageButton) playbackControlLayout.findViewById(R.id.mrc_control_play_pause); + + updateViews(); + return rootView; + } + + private void updateViews() { + if (token == null || artView == null || mediaController == null) { + rootView.setVisibility(View.GONE); + return; + } + MediaMetadataCompat metadata = mediaController.getMetadata(); + MediaDescriptionCompat description = metadata == null ? null : metadata.getDescription(); + if (description == null) { + rootView.setVisibility(View.GONE); + return; + } + + PlaybackStateCompat state = mediaController.getPlaybackState(); + MediaRouter.RouteInfo route = MediaRouter.getInstance(getContext()).getSelectedRoute(); + + CharSequence title = description.getTitle(); + boolean hasTitle = !TextUtils.isEmpty(title); + CharSequence subtitle = description.getSubtitle(); + boolean hasSubtitle = !TextUtils.isEmpty(subtitle); + + boolean showTitle = false; + boolean showSubtitle = false; + if (route.getPresentationDisplayId() != MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE) { + // The user is currently casting screen. + titleView.setText(android.support.v7.mediarouter.R.string.mr_controller_casting_screen); + showTitle = true; + } else if (state == null || state.getState() == PlaybackStateCompat.STATE_NONE) { + // Show "No media selected" as we don't yet know the playback state. + // (Only exception is bluetooth where we don't show anything.) + if (!route.isDeviceTypeBluetooth()) { + titleView.setText(android.support.v7.mediarouter.R.string.mr_controller_no_media_selected); + showTitle = true; + } + } else if (!hasTitle && !hasSubtitle) { + titleView.setText(android.support.v7.mediarouter.R.string.mr_controller_no_info_available); + showTitle = true; + } else { + if (hasTitle) { + titleView.setText(title); + showTitle = true; + } + if (hasSubtitle) { + subtitleView.setText(subtitle); + showSubtitle = true; + } + } + titleView.setVisibility(showTitle ? View.VISIBLE : View.GONE); + subtitleView.setVisibility(showSubtitle ? View.VISIBLE : View.GONE); + + updateState(); + + Bitmap art = metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ART); + if (art == null) { + artView.setVisibility(View.GONE); + return; + } + artView.setImageBitmap(art); + artView.setVisibility(View.VISIBLE); + rootView.setVisibility(View.VISIBLE); + } + + private void updateState() { + if (mediaController == null) { + return; + } + PlaybackStateCompat state = mediaController.getPlaybackState(); + if (state != null) { + boolean isPlaying = state.getState() == PlaybackStateCompat.STATE_BUFFERING + || state.getState() == PlaybackStateCompat.STATE_PLAYING; + boolean supportsPlay = (state.getActions() & (PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0; + boolean supportsPause = (state.getActions() & (PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0; + if (isPlaying && supportsPause) { + playPauseButton.setVisibility(View.VISIBLE); + playPauseButton.setImageResource(getThemeResource(getContext(), + android.support.v7.mediarouter.R.attr.mediaRoutePauseDrawable)); + playPauseButton.setContentDescription(getContext().getResources() + .getText(android.support.v7.mediarouter.R.string.mr_controller_pause)); + } else if (!isPlaying && supportsPlay) { + playPauseButton.setVisibility(View.VISIBLE); + playPauseButton.setImageResource(getThemeResource(getContext(), + android.support.v7.mediarouter.R.attr.mediaRoutePlayDrawable)); + playPauseButton.setContentDescription(getContext().getResources() + .getText(android.support.v7.mediarouter.R.string.mr_controller_play)); + } else { + playPauseButton.setVisibility(View.GONE); + } + } + } + + private static int getThemeResource(Context context, int attr) { + TypedValue value = new TypedValue(); + return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0; + } +} diff --git a/app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java b/app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java new file mode 100644 index 000000000..ca3758351 --- /dev/null +++ b/app/src/play/java/de/danoeh/antennapod/config/CastCallbackImpl.java @@ -0,0 +1,29 @@ +package de.danoeh.antennapod.config; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.MediaRouteControllerDialog; +import android.support.v7.app.MediaRouteControllerDialogFragment; +import android.support.v7.app.MediaRouteDialogFactory; + +import de.danoeh.antennapod.cast.CustomMRControllerDialog; +import de.danoeh.antennapod.core.CastCallbacks; + +public class CastCallbackImpl implements CastCallbacks { + @Override + public MediaRouteDialogFactory getMediaRouterDialogFactory() { + return new MediaRouteDialogFactory() { + @NonNull + @Override + public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() { + return new MediaRouteControllerDialogFragment() { + @Override + public MediaRouteControllerDialog onCreateControllerDialog(Context context, Bundle savedInstanceState) { + return new CustomMRControllerDialog(context); + } + }; + } + }; + } +} diff --git a/app/src/play/res/layout/media_router_controller.xml b/app/src/play/res/layout/media_router_controller.xml new file mode 100644 index 000000000..15c2f9731 --- /dev/null +++ b/app/src/play/res/layout/media_router_controller.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java new file mode 100644 index 000000000..2e266c736 --- /dev/null +++ b/core/src/free/java/de/danoeh/antennapod/core/CastCallbacks.java @@ -0,0 +1,7 @@ +package de.danoeh.antennapod.core; + +/** + * Callbacks for Chromecast support on the core module + */ +public interface CastCallbacks { +} diff --git a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java index d1c93d782..eb2e6fc9e 100644 --- a/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/core/src/free/java/de/danoeh/antennapod/core/ClientConfig.java @@ -30,6 +30,8 @@ public class ClientConfig { public static DBTasksCallbacks dbTasksCallbacks; + public static CastCallbacks castCallbacks; + private static boolean initialized = false; public static synchronized void initialize(Context context) { 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 index 76c960607..c79d662cb 100644 --- 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 @@ -28,11 +28,9 @@ import android.support.v7.app.NotificationCompat; 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; @@ -945,31 +943,32 @@ public class PlaybackService extends Service { 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) { + String imageLocation = p.getImageLocation(); + + if (!TextUtils.isEmpty(imageLocation)) { + if (isCasting || UserPreferences.setLockscreenBackground()) { + builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, imageLocation); + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, imageLocation); + try { Bitmap art = Glide.with(this) - .load(p.getImageLocation()) + .load(imageLocation) .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()) + // Icon is useful for MediaDescription, + Bitmap icon = Glide.with(this) + .load(imageLocation) .asBitmap() .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) - .centerCrop() - .into(display.getWidth(), display.getHeight()) + .fitCenter() + .into(128, 128) .get(); - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art); + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, icon); + } catch (Throwable tr) { + Log.e(TAG, Log.getStackTraceString(tr)); } - } catch (Throwable tr) { - Log.e(TAG, Log.getStackTraceString(tr)); } } if (!Thread.currentThread().isInterrupted() && started) { diff --git a/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java b/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java new file mode 100644 index 000000000..770fee9b9 --- /dev/null +++ b/core/src/play/java/de/danoeh/antennapod/core/CastCallbacks.java @@ -0,0 +1,12 @@ +package de.danoeh.antennapod.core; + +import android.support.annotation.Nullable; +import android.support.v7.app.MediaRouteDialogFactory; + +/** + * Callbacks for Chromecast support on the core module + */ +public interface CastCallbacks { + + @Nullable MediaRouteDialogFactory getMediaRouterDialogFactory(); +} diff --git a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java index 9bbccbb82..3dfd6ea65 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/core/src/play/java/de/danoeh/antennapod/core/ClientConfig.java @@ -31,6 +31,8 @@ public class ClientConfig { public static DBTasksCallbacks dbTasksCallbacks; + public static CastCallbacks castCallbacks; + private static boolean initialized = false; public static synchronized void initialize(Context context) { diff --git a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java index 66b4558be..0674990f1 100644 --- a/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java +++ b/core/src/play/java/de/danoeh/antennapod/core/cast/CastManager.java @@ -62,6 +62,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; +import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import static com.google.android.gms.cast.RemoteMediaPlayer.RESUME_STATE_PLAY; @@ -130,12 +131,12 @@ public class CastManager extends BaseCastManager implements OnFailedListener { 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()) + .setMediaRouteDialogFactory(ClientConfig.castCallbacks.getMediaRouterDialogFactory()) .build(); Log.d(TAG, "New instance of CastManager is created"); if (ConnectionResult.SUCCESS != GoogleApiAvailability.getInstance()