/* * Copyright (C) 2013 The Android Open Source Project * * 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. */ package android.support.v7.app; import android.content.Context; import android.content.IntentSender; 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.media.MediaRouteSelector; import android.support.v7.media.MediaRouter; import android.support.v7.mediarouter.R; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; /** * This class implements the route controller dialog for {@link MediaRouter}. * <p> * This dialog allows the user to control or disconnect from the currently selected route. * </p> * * @see MediaRouteButton * @see MediaRouteActionProvider */ public class MediaRouteControllerDialog extends AlertDialog { private static final String TAG = "MediaRouteControllerDialog"; // Time to wait before updating the volume when the user lets go of the seek bar // to allow the route provider time to propagate the change and publish a new // route descriptor. private static final int VOLUME_UPDATE_DELAY_MILLIS = 250; private final MediaRouter mRouter; private final MediaRouterCallback mCallback; private final MediaRouter.RouteInfo mRoute; private boolean mCreated; private boolean mAttachedToWindow; private Drawable mMediaRouteConnectingDrawable; private Drawable mMediaRouteOnDrawable; private View mControlView; private Button mDisconnectButton; private Button mStopCastingButton; private ImageButton mPlayPauseButton; private ImageButton mSettingsButton; private ImageView mArtView; private TextView mTitleView; private TextView mSubtitleView; private TextView mRouteNameView; private boolean mVolumeControlEnabled = true; private LinearLayout mVolumeLayout; private SeekBar mVolumeSlider; private boolean mVolumeSliderTouched; private MediaControllerCompat mMediaController; private MediaControllerCallback mControllerCallback; private PlaybackStateCompat mState; private MediaDescriptionCompat mDescription; public MediaRouteControllerDialog(Context context) { this(context, 0); } public MediaRouteControllerDialog(Context context, int theme) { super(MediaRouterThemeHelper.createThemedContext(context), theme); context = getContext(); mControllerCallback = new MediaControllerCallback(); mRouter = MediaRouter.getInstance(context); mCallback = new MediaRouterCallback(); mRoute = mRouter.getSelectedRoute(); setMediaSession(mRouter.getMediaSessionToken()); } /** * Gets the route that this dialog is controlling. */ public MediaRouter.RouteInfo getRoute() { return mRoute; } /** * Provides the subclass an opportunity to create a view that will * be included within the body of the dialog to offer additional media controls * for the currently playing content. * * @param savedInstanceState The dialog's saved instance state. * @return The media control view, or null if none. */ public View onCreateMediaControlView(Bundle savedInstanceState) { return null; } /** * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}. * * @return The media control view, or null if none. */ public View getMediaControlView() { return mControlView; } /** * Sets whether to enable the volume slider and volume control using the volume keys * when the route supports it. * <p> * The default value is true. * </p> */ public void setVolumeControlEnabled(boolean enable) { if (mVolumeControlEnabled != enable) { mVolumeControlEnabled = enable; if (mCreated) { updateVolume(); } } } /** * Returns whether to enable the volume slider and volume control using the volume keys * when the route supports it. */ public boolean isVolumeControlEnabled() { return mVolumeControlEnabled; } /** * Set the session to use for metadata and transport controls. The dialog * will listen to changes on this session and update the UI automatically in * response to changes. * * @param sessionToken The token for the session to use. */ private void setMediaSession(MediaSessionCompat.Token sessionToken) { if (mMediaController != null) { mMediaController.unregisterCallback(mControllerCallback); mMediaController = null; } if (sessionToken == null) { return; } if (!mAttachedToWindow) { return; } try { mMediaController = new MediaControllerCompat(getContext(), sessionToken); } catch (RemoteException e) { Log.e(TAG, "Error creating media controller in setMediaSession.", e); } if (mMediaController != null) { mMediaController.registerCallback(mControllerCallback); } MediaMetadataCompat metadata = mMediaController == null ? null : mMediaController.getMetadata(); mDescription = metadata == null ? null : metadata.getDescription(); mState = mMediaController == null ? null : mMediaController.getPlaybackState(); update(); } /** * Gets the description being used by the default UI. * * @return The current description. */ public MediaSessionCompat.Token getMediaSession() { return mMediaController == null ? null : mMediaController.getSessionToken(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.mr_media_route_controller_material_dialog_b); ClickListener listener = new ClickListener(); mDisconnectButton = (Button) findViewById(R.id.disconnect); mDisconnectButton.setOnClickListener(listener); mStopCastingButton = (Button) findViewById(R.id.stop); mStopCastingButton.setOnClickListener(listener); mSettingsButton = (ImageButton) findViewById(R.id.settings); mSettingsButton.setOnClickListener(listener); mArtView = (ImageView) findViewById(R.id.art); mTitleView = (TextView) findViewById(R.id.title); mSubtitleView = (TextView) findViewById(R.id.subtitle); mPlayPauseButton = (ImageButton) findViewById(R.id.play_pause); mPlayPauseButton.setOnClickListener(listener); mRouteNameView = (TextView) findViewById(R.id.route_name); mVolumeLayout = (LinearLayout)findViewById(R.id.media_route_volume_layout); mVolumeSlider = (SeekBar)findViewById(R.id.media_route_volume_slider); mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { private final Runnable mStopTrackingTouch = new Runnable() { @Override public void run() { if (mVolumeSliderTouched) { mVolumeSliderTouched = false; updateVolume(); } } }; @Override public void onStartTrackingTouch(SeekBar seekBar) { if (mVolumeSliderTouched) { mVolumeSlider.removeCallbacks(mStopTrackingTouch); } else { mVolumeSliderTouched = true; } } @Override public void onStopTrackingTouch(SeekBar seekBar) { // Defer resetting mVolumeSliderTouched to allow the media route provider // a little time to settle into its new state and publish the final // volume update. mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS); } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { mRoute.requestSetVolume(progress); } } }); mCreated = true; if (update()) { mControlView = onCreateMediaControlView(savedInstanceState); FrameLayout controlFrame = (FrameLayout)findViewById(R.id.media_route_control_frame); if (mControlView != null) { controlFrame.findViewById(R.id.default_control_frame).setVisibility(View.GONE); controlFrame.addView(mControlView); } } } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); mAttachedToWindow = true; mRouter.addCallback(MediaRouteSelector.EMPTY, mCallback, MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS); setMediaSession(mRouter.getMediaSessionToken()); } @Override public void onDetachedFromWindow() { mRouter.removeCallback(mCallback); setMediaSession(null); mAttachedToWindow = false; super.onDetachedFromWindow(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1); return true; } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { return true; } return super.onKeyUp(keyCode, event); } private boolean update() { if (!mRoute.isSelected() || mRoute.isDefault()) { dismiss(); return false; } if (!mCreated) { return false; } updateVolume(); mRouteNameView.setText(mRoute.getName()); if (mRoute.canDisconnect()) { mDisconnectButton.setVisibility(View.VISIBLE); } else { mDisconnectButton.setVisibility(View.GONE); } if (mRoute.getSettingsIntent() != null) { mSettingsButton.setVisibility(View.VISIBLE); } else { mSettingsButton.setVisibility(View.GONE); } if (mControlView == null) { if (mDescription != null && mDescription.getIconBitmap() != null) { mArtView.setImageBitmap(mDescription.getIconBitmap()); mArtView.setVisibility(View.VISIBLE); } else if (mDescription != null && mDescription.getIconUri() != null) { // TODO replace with background load of icon mArtView.setImageURI(mDescription.getIconUri()); mArtView.setVisibility(View.VISIBLE); } else { mArtView.setImageDrawable(null); mArtView.setVisibility(View.GONE); } CharSequence title = mDescription == null ? null : mDescription.getTitle(); boolean hasTitle = !TextUtils.isEmpty(title); CharSequence subtitle = mDescription == null ? null : mDescription.getSubtitle(); boolean hasSubtitle = !TextUtils.isEmpty(subtitle); if (!hasTitle && !hasSubtitle) { mTitleView.setText(R.string.mr_media_route_controller_no_info_available); mTitleView.setEnabled(false); mTitleView.setVisibility(View.VISIBLE); mSubtitleView.setVisibility(View.GONE); } else { mTitleView.setText(title); mTitleView.setEnabled(hasTitle); mTitleView.setVisibility(hasTitle ? View.VISIBLE : View.GONE); mSubtitleView.setText(subtitle); mSubtitleView.setVisibility(hasSubtitle ? View.VISIBLE : View.GONE); } if (mState != null) { boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_BUFFERING || mState.getState() == PlaybackStateCompat.STATE_PLAYING; boolean supportsPlay = (mState.getActions() & (PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0; boolean supportsPause = (mState.getActions() & (PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0; if (isPlaying && supportsPause) { mPlayPauseButton.setVisibility(View.VISIBLE); mPlayPauseButton.setImageResource(MediaRouterThemeHelper.getThemeResource( getContext(), R.attr.mediaRoutePauseDrawable)); mPlayPauseButton.setContentDescription(getContext().getResources() .getText(R.string.mr_media_route_controller_pause)); } else if (!isPlaying && supportsPlay) { mPlayPauseButton.setVisibility(View.VISIBLE); mPlayPauseButton.setImageResource(MediaRouterThemeHelper.getThemeResource( getContext(), R.attr.mediaRoutePlayDrawable)); mPlayPauseButton.setContentDescription(getContext().getResources() .getText(R.string.mr_media_route_controller_play)); } else { mPlayPauseButton.setVisibility(View.GONE); } } else { mPlayPauseButton.setVisibility(View.GONE); } } return true; } private Drawable getIconDrawable() { if (mRoute.isConnecting()) { if (mMediaRouteConnectingDrawable == null) { mMediaRouteConnectingDrawable = MediaRouterThemeHelper.getThemeDrawable( getContext(), R.attr.mediaRouteConnectingDrawable); } return mMediaRouteConnectingDrawable; } else { if (mMediaRouteOnDrawable == null) { mMediaRouteOnDrawable = MediaRouterThemeHelper.getThemeDrawable( getContext(), R.attr.mediaRouteOnDrawable); } return mMediaRouteOnDrawable; } } private void updateVolume() { if (!mVolumeSliderTouched) { if (isVolumeControlAvailable()) { mVolumeLayout.setVisibility(View.VISIBLE); mVolumeSlider.setMax(mRoute.getVolumeMax()); mVolumeSlider.setProgress(mRoute.getVolume()); } else { mVolumeLayout.setVisibility(View.GONE); } } } private boolean isVolumeControlAvailable() { return mVolumeControlEnabled && mRoute.getVolumeHandling() == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE; } private final class MediaRouterCallback extends MediaRouter.Callback { @Override public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) { update(); } @Override public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { update(); } @Override public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) { if (route == mRoute) { updateVolume(); } } } private final class MediaControllerCallback extends MediaControllerCompat.Callback { @Override public void onSessionDestroyed() { if (mMediaController != null) { mMediaController.unregisterCallback(mControllerCallback); mMediaController = null; } } @Override public void onPlaybackStateChanged(PlaybackStateCompat state) { mState = state; update(); } @Override public void onMetadataChanged(MediaMetadataCompat metadata) { mDescription = metadata == null ? null : metadata.getDescription(); update(); } } private final class ClickListener implements View.OnClickListener { @Override public void onClick(View v) { int id = v.getId(); if (id == R.id.stop || id == R.id.disconnect) { if (mRoute.isSelected()) { mRouter.unselect(id == R.id.stop ? MediaRouter.UNSELECT_REASON_STOPPED : MediaRouter.UNSELECT_REASON_DISCONNECTED); } dismiss(); } else if (id == R.id.play_pause) { if (mMediaController != null && mState != null) { if (mState.getState() == PlaybackStateCompat.STATE_PLAYING) { mMediaController.getTransportControls().pause(); } else { mMediaController.getTransportControls().play(); } } } else if (id == R.id.settings) { IntentSender is = mRoute.getSettingsIntent(); if (is != null) { try { is.sendIntent(null, 0, null, null, null); dismiss(); } catch (Exception e) { Log.e(TAG, "Error opening route settings.", e); } } } } } }