implement custom mediaroute playback controller

This commit is contained in:
Domingos Lopes 2016-06-28 02:07:43 -04:00
parent 926f3912cf
commit 6d0debfd4d
11 changed files with 363 additions and 18 deletions

View File

@ -0,0 +1,7 @@
package de.danoeh.antennapod.config;
import de.danoeh.antennapod.core.CastCallbacks;
public class CastCallbackImpl implements CastCallbacks {
}

View File

@ -16,5 +16,6 @@ public class ClientConfigurator {
ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl();
ClientConfig.flattrCallbacks = new FlattrCallbacksImpl();
ClientConfig.dbTasksCallbacks = new DBTasksCallbacksImpl();
ClientConfig.castCallbacks = new CastCallbackImpl();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
};
}
};
}
}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--<ImageView android:id="@+id/mrc_art"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="wrap_content"-->
<!--android:adjustViewBounds="true"-->
<!--android:scaleType="fitCenter"-->
<!--android:background="?attr/colorPrimary"-->
<!--android:layout_gravity="top"-->
<!--android:visibility="gone"/>-->
<RelativeLayout android:id="@+id/mrc_playback_control"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:paddingLeft="24dp"
android:paddingStart="24dp"
android:paddingRight="12dp"
android:paddingEnd="12dp">
<ImageButton android:id="@+id/mrc_control_play_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:contentDescription="@string/mr_controller_play"
android:background="?attr/selectableItemBackgroundBorderless"/>
<LinearLayout android:id="@+id/mrc_control_title_container"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_toLeftOf="@id/mrc_control_play_pause"
android:layout_toStartOf="@id/mrc_control_play_pause">
<TextView android:id="@+id/mrc_control_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/mediaRouteControllerPrimaryTextStyle"
android:singleLine="true" />
<TextView android:id="@+id/mrc_control_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/mediaRouteControllerSecondaryTextStyle"
android:singleLine="true" />
</LinearLayout>
</RelativeLayout>
<View android:id="@+id/mrc_control_divider"
android:layout_width="fill_parent"
android:layout_height="8dp"/>
</LinearLayout>

View File

@ -0,0 +1,7 @@
package de.danoeh.antennapod.core;
/**
* Callbacks for Chromecast support on the core module
*/
public interface CastCallbacks {
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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();
}

View File

@ -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) {

View File

@ -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()