Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.internal.app;
     18 
     19 import com.android.internal.R;
     20 
     21 import android.app.AlertDialog;
     22 import android.app.MediaRouteActionProvider;
     23 import android.app.MediaRouteButton;
     24 import android.content.Context;
     25 import android.content.DialogInterface;
     26 import android.content.res.Resources;
     27 import android.content.res.TypedArray;
     28 import android.graphics.drawable.AnimationDrawable;
     29 import android.graphics.drawable.Drawable;
     30 import android.graphics.drawable.StateListDrawable;
     31 import android.media.MediaRouter;
     32 import android.media.MediaRouter.RouteGroup;
     33 import android.media.MediaRouter.RouteInfo;
     34 import android.os.Bundle;
     35 import android.util.TypedValue;
     36 import android.view.KeyEvent;
     37 import android.view.View;
     38 import android.widget.FrameLayout;
     39 import android.widget.LinearLayout;
     40 import android.widget.SeekBar;
     41 
     42 /**
     43  * This class implements the route controller dialog for {@link MediaRouter}.
     44  * <p>
     45  * This dialog allows the user to control or disconnect from the currently selected route.
     46  * </p>
     47  *
     48  * @see MediaRouteButton
     49  * @see MediaRouteActionProvider
     50  *
     51  * TODO: Move this back into the API, as in the support library media router.
     52  */
     53 public class MediaRouteControllerDialog extends AlertDialog {
     54     // Time to wait before updating the volume when the user lets go of the seek bar
     55     // to allow the route provider time to propagate the change and publish a new
     56     // route descriptor.
     57     private static final int VOLUME_UPDATE_DELAY_MILLIS = 250;
     58 
     59     private final MediaRouter mRouter;
     60     private final MediaRouterCallback mCallback;
     61     private final MediaRouter.RouteInfo mRoute;
     62 
     63     private boolean mCreated;
     64     private Drawable mMediaRouteButtonDrawable;
     65     private int[] mMediaRouteConnectingState = { R.attr.state_checked, R.attr.state_enabled };
     66     private int[] mMediaRouteOnState = { R.attr.state_activated, R.attr.state_enabled };
     67     private Drawable mCurrentIconDrawable;
     68 
     69     private boolean mVolumeControlEnabled = true;
     70     private LinearLayout mVolumeLayout;
     71     private SeekBar mVolumeSlider;
     72     private boolean mVolumeSliderTouched;
     73 
     74     private View mControlView;
     75     private boolean mAttachedToWindow;
     76 
     77     public MediaRouteControllerDialog(Context context, int theme) {
     78         super(context, theme);
     79 
     80         mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
     81         mCallback = new MediaRouterCallback();
     82         mRoute = mRouter.getSelectedRoute();
     83     }
     84 
     85     /**
     86      * Gets the route that this dialog is controlling.
     87      */
     88     public MediaRouter.RouteInfo getRoute() {
     89         return mRoute;
     90     }
     91 
     92     /**
     93      * Provides the subclass an opportunity to create a view that will
     94      * be included within the body of the dialog to offer additional media controls
     95      * for the currently playing content.
     96      *
     97      * @param savedInstanceState The dialog's saved instance state.
     98      * @return The media control view, or null if none.
     99      */
    100     public View onCreateMediaControlView(Bundle savedInstanceState) {
    101         return null;
    102     }
    103 
    104     /**
    105      * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}.
    106      *
    107      * @return The media control view, or null if none.
    108      */
    109     public View getMediaControlView() {
    110         return mControlView;
    111     }
    112 
    113     /**
    114      * Sets whether to enable the volume slider and volume control using the volume keys
    115      * when the route supports it.
    116      * <p>
    117      * The default value is true.
    118      * </p>
    119      */
    120     public void setVolumeControlEnabled(boolean enable) {
    121         if (mVolumeControlEnabled != enable) {
    122             mVolumeControlEnabled = enable;
    123             if (mCreated) {
    124                 updateVolume();
    125             }
    126         }
    127     }
    128 
    129     /**
    130      * Returns whether to enable the volume slider and volume control using the volume keys
    131      * when the route supports it.
    132      */
    133     public boolean isVolumeControlEnabled() {
    134         return mVolumeControlEnabled;
    135     }
    136 
    137     @Override
    138     protected void onCreate(Bundle savedInstanceState) {
    139         setTitle(mRoute.getName());
    140         Resources res = getContext().getResources();
    141         setButton(BUTTON_NEGATIVE, res.getString(R.string.media_route_controller_disconnect),
    142                 new OnClickListener() {
    143                     @Override
    144                     public void onClick(DialogInterface dialogInterface, int id) {
    145                         if (mRoute.isSelected()) {
    146                             if (mRoute.isBluetooth()) {
    147                                 mRouter.getDefaultRoute().select();
    148                             } else {
    149                                 mRouter.getFallbackRoute().select();
    150                             }
    151                         }
    152                         dismiss();
    153                     }
    154                 });
    155         View customView = getLayoutInflater().inflate(R.layout.media_route_controller_dialog, null);
    156         setView(customView, 0, 0, 0, 0);
    157         super.onCreate(savedInstanceState);
    158 
    159         View customPanelView = getWindow().findViewById(R.id.customPanel);
    160         if (customPanelView != null) {
    161             customPanelView.setMinimumHeight(0);
    162         }
    163         mVolumeLayout = (LinearLayout) customView.findViewById(R.id.media_route_volume_layout);
    164         mVolumeSlider = (SeekBar) customView.findViewById(R.id.media_route_volume_slider);
    165         mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    166             private final Runnable mStopTrackingTouch = new Runnable() {
    167                 @Override
    168                 public void run() {
    169                     if (mVolumeSliderTouched) {
    170                         mVolumeSliderTouched = false;
    171                         updateVolume();
    172                     }
    173                 }
    174             };
    175 
    176             @Override
    177             public void onStartTrackingTouch(SeekBar seekBar) {
    178                 if (mVolumeSliderTouched) {
    179                     mVolumeSlider.removeCallbacks(mStopTrackingTouch);
    180                 } else {
    181                     mVolumeSliderTouched = true;
    182                 }
    183             }
    184 
    185             @Override
    186             public void onStopTrackingTouch(SeekBar seekBar) {
    187                 // Defer resetting mVolumeSliderTouched to allow the media route provider
    188                 // a little time to settle into its new state and publish the final
    189                 // volume update.
    190                 mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS);
    191             }
    192 
    193             @Override
    194             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    195                 if (fromUser) {
    196                     mRoute.requestSetVolume(progress);
    197                 }
    198             }
    199         });
    200 
    201         mMediaRouteButtonDrawable = obtainMediaRouteButtonDrawable();
    202         mCreated = true;
    203         if (update()) {
    204             mControlView = onCreateMediaControlView(savedInstanceState);
    205             FrameLayout controlFrame =
    206                     (FrameLayout) customView.findViewById(R.id.media_route_control_frame);
    207             if (mControlView != null) {
    208                 controlFrame.addView(mControlView);
    209                 controlFrame.setVisibility(View.VISIBLE);
    210             } else {
    211                 controlFrame.setVisibility(View.GONE);
    212             }
    213         }
    214     }
    215 
    216     @Override
    217     public void onAttachedToWindow() {
    218         super.onAttachedToWindow();
    219         mAttachedToWindow = true;
    220 
    221         mRouter.addCallback(0, mCallback, MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS);
    222         update();
    223     }
    224 
    225     @Override
    226     public void onDetachedFromWindow() {
    227         mRouter.removeCallback(mCallback);
    228         mAttachedToWindow = false;
    229 
    230         super.onDetachedFromWindow();
    231     }
    232 
    233     @Override
    234     public boolean onKeyDown(int keyCode, KeyEvent event) {
    235         if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
    236                 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
    237             mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1);
    238             return true;
    239         }
    240         return super.onKeyDown(keyCode, event);
    241     }
    242 
    243     @Override
    244     public boolean onKeyUp(int keyCode, KeyEvent event) {
    245         if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
    246                 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
    247             return true;
    248         }
    249         return super.onKeyUp(keyCode, event);
    250     }
    251 
    252     private boolean update() {
    253         if (!mRoute.isSelected() || mRoute.isDefault()) {
    254             dismiss();
    255             return false;
    256         }
    257 
    258         setTitle(mRoute.getName());
    259         updateVolume();
    260 
    261         Drawable icon = getIconDrawable();
    262         if (icon != mCurrentIconDrawable) {
    263             mCurrentIconDrawable = icon;
    264             if (icon instanceof AnimationDrawable) {
    265                 AnimationDrawable animDrawable = (AnimationDrawable) icon;
    266                 if (!mAttachedToWindow && !mRoute.isConnecting()) {
    267                     // When the route is already connected before the view is attached, show the
    268                     // last frame of the connected animation immediately.
    269                     if (animDrawable.isRunning()) {
    270                         animDrawable.stop();
    271                     }
    272                     icon = animDrawable.getFrame(animDrawable.getNumberOfFrames() - 1);
    273                 } else if (!animDrawable.isRunning()) {
    274                     animDrawable.start();
    275                 }
    276             }
    277             setIcon(icon);
    278         }
    279         return true;
    280     }
    281 
    282     private Drawable obtainMediaRouteButtonDrawable() {
    283         Context context = getContext();
    284         TypedValue value = new TypedValue();
    285         if (!context.getTheme().resolveAttribute(R.attr.mediaRouteButtonStyle, value, true)) {
    286             return null;
    287         }
    288         int[] drawableAttrs = new int[] { R.attr.externalRouteEnabledDrawable };
    289         TypedArray a = context.obtainStyledAttributes(value.data, drawableAttrs);
    290         Drawable drawable = a.getDrawable(0);
    291         a.recycle();
    292         return drawable;
    293     }
    294 
    295     private Drawable getIconDrawable() {
    296         if (!(mMediaRouteButtonDrawable instanceof StateListDrawable)) {
    297             return mMediaRouteButtonDrawable;
    298         } else if (mRoute.isConnecting()) {
    299             StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable;
    300             stateListDrawable.setState(mMediaRouteConnectingState);
    301             return stateListDrawable.getCurrent();
    302         } else {
    303             StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable;
    304             stateListDrawable.setState(mMediaRouteOnState);
    305             return stateListDrawable.getCurrent();
    306         }
    307     }
    308 
    309     private void updateVolume() {
    310         if (!mVolumeSliderTouched) {
    311             if (isVolumeControlAvailable()) {
    312                 mVolumeLayout.setVisibility(View.VISIBLE);
    313                 mVolumeSlider.setMax(mRoute.getVolumeMax());
    314                 mVolumeSlider.setProgress(mRoute.getVolume());
    315             } else {
    316                 mVolumeLayout.setVisibility(View.GONE);
    317             }
    318         }
    319     }
    320 
    321     private boolean isVolumeControlAvailable() {
    322         return mVolumeControlEnabled && mRoute.getVolumeHandling() ==
    323                 MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
    324     }
    325 
    326     private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
    327         @Override
    328         public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
    329             update();
    330         }
    331 
    332         @Override
    333         public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
    334             update();
    335         }
    336 
    337         @Override
    338         public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
    339             if (route == mRoute) {
    340                 updateVolume();
    341             }
    342         }
    343 
    344         @Override
    345         public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
    346                 int index) {
    347             update();
    348         }
    349 
    350         @Override
    351         public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
    352             update();
    353         }
    354     }
    355 }
    356