Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright 2018 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.support.mediarouter.app;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.content.Context;
     22 import android.support.v4.view.ActionProvider;
     23 import android.util.Log;
     24 import android.view.View;
     25 import android.view.ViewGroup;
     26 
     27 import com.android.support.mediarouter.media.MediaRouteSelector;
     28 import com.android.support.mediarouter.media.MediaRouter;
     29 
     30 import java.lang.ref.WeakReference;
     31 
     32 /**
     33  * The media route action provider displays a {@link MediaRouteButton media route button}
     34  * in the application's {@link ActionBar} to allow the user to select routes and
     35  * to control the currently selected route.
     36  * <p>
     37  * The application must specify the kinds of routes that the user should be allowed
     38  * to select by specifying a {@link MediaRouteSelector selector} with the
     39  * {@link #setRouteSelector} method.
     40  * </p><p>
     41  * Refer to {@link MediaRouteButton} for a description of the button that will
     42  * appear in the action bar menu.  Note that instead of disabling the button
     43  * when no routes are available, the action provider will instead make the
     44  * menu item invisible.  In this way, the button will only be visible when it
     45  * is possible for the user to discover and select a matching route.
     46  * </p>
     47  *
     48  * <h3>Prerequisites</h3>
     49  * <p>
     50  * To use the media route action provider, the activity must be a subclass of
     51  * {@link AppCompatActivity} from the <code>android.support.v7.appcompat</code>
     52  * support library.  Refer to support library documentation for details.
     53  * </p>
     54  *
     55  * <h3>Example</h3>
     56  * <p>
     57  * </p><p>
     58  * The application should define a menu resource to include the provider in the
     59  * action bar options menu.  Note that the support library action bar uses attributes
     60  * that are defined in the application's resource namespace rather than the framework's
     61  * resource namespace to configure each item.
     62  * </p><pre>
     63  * &lt;menu xmlns:android="http://schemas.android.com/apk/res/android"
     64  *         xmlns:app="http://schemas.android.com/apk/res-auto">
     65  *     &lt;item android:id="@+id/media_route_menu_item"
     66  *         android:title="@string/media_route_menu_title"
     67  *         app:showAsAction="always"
     68  *         app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"/>
     69  * &lt;/menu>
     70  * </pre><p>
     71  * Then configure the menu and set the route selector for the chooser.
     72  * </p><pre>
     73  * public class MyActivity extends AppCompatActivity {
     74  *     private MediaRouter mRouter;
     75  *     private MediaRouter.Callback mCallback;
     76  *     private MediaRouteSelector mSelector;
     77  *
     78  *     protected void onCreate(Bundle savedInstanceState) {
     79  *         super.onCreate(savedInstanceState);
     80  *
     81  *         mRouter = Mediarouter.getInstance(this);
     82  *         mSelector = new MediaRouteSelector.Builder()
     83  *                 .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
     84  *                 .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
     85  *                 .build();
     86  *         mCallback = new MyCallback();
     87  *     }
     88  *
     89  *     // Add the callback on start to tell the media router what kinds of routes
     90  *     // the application is interested in so that it can try to discover suitable ones.
     91  *     public void onStart() {
     92  *         super.onStart();
     93  *
     94  *         mediaRouter.addCallback(mSelector, mCallback,
     95  *                 MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
     96  *
     97  *         MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector);
     98  *         // do something with the route...
     99  *     }
    100  *
    101  *     // Remove the selector on stop to tell the media router that it no longer
    102  *     // needs to invest effort trying to discover routes of these kinds for now.
    103  *     public void onStop() {
    104  *         super.onStop();
    105  *
    106  *         mediaRouter.removeCallback(mCallback);
    107  *     }
    108  *
    109  *     public boolean onCreateOptionsMenu(Menu menu) {
    110  *         super.onCreateOptionsMenu(menu);
    111  *
    112  *         getMenuInflater().inflate(R.menu.sample_media_router_menu, menu);
    113  *
    114  *         MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
    115  *         MediaRouteActionProvider mediaRouteActionProvider =
    116  *                 (MediaRouteActionProvider)MenuItemCompat.getActionProvider(mediaRouteMenuItem);
    117  *         mediaRouteActionProvider.setRouteSelector(mSelector);
    118  *         return true;
    119  *     }
    120  *
    121  *     private final class MyCallback extends MediaRouter.Callback {
    122  *         // Implement callback methods as needed.
    123  *     }
    124  * }
    125  * </pre>
    126  *
    127  * @see #setRouteSelector
    128  */
    129 public class MediaRouteActionProvider extends ActionProvider {
    130     private static final String TAG = "MediaRouteActionProvider";
    131 
    132     private final MediaRouter mRouter;
    133     private final MediaRouterCallback mCallback;
    134 
    135     private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
    136     private MediaRouteDialogFactory mDialogFactory = MediaRouteDialogFactory.getDefault();
    137     private MediaRouteButton mButton;
    138 
    139     /**
    140      * Creates the action provider.
    141      *
    142      * @param context The context.
    143      */
    144     public MediaRouteActionProvider(Context context) {
    145         super(context);
    146 
    147         mRouter = MediaRouter.getInstance(context);
    148         mCallback = new MediaRouterCallback(this);
    149     }
    150 
    151     /**
    152      * Gets the media route selector for filtering the routes that the user can
    153      * select using the media route chooser dialog.
    154      *
    155      * @return The selector, never null.
    156      */
    157     @NonNull
    158     public MediaRouteSelector getRouteSelector() {
    159         return mSelector;
    160     }
    161 
    162     /**
    163      * Sets the media route selector for filtering the routes that the user can
    164      * select using the media route chooser dialog.
    165      *
    166      * @param selector The selector, must not be null.
    167      */
    168     public void setRouteSelector(@NonNull MediaRouteSelector selector) {
    169         if (selector == null) {
    170             throw new IllegalArgumentException("selector must not be null");
    171         }
    172 
    173         if (!mSelector.equals(selector)) {
    174             // FIXME: We currently have no way of knowing whether the action provider
    175             // is still needed by the UI.  Unfortunately this means the action provider
    176             // may leak callbacks until garbage collection occurs.  This may result in
    177             // media route providers doing more work than necessary in the short term
    178             // while trying to discover routes that are no longer of interest to the
    179             // application.  To solve this problem, the action provider will need some
    180             // indication from the framework that it is being destroyed.
    181             if (!mSelector.isEmpty()) {
    182                 mRouter.removeCallback(mCallback);
    183             }
    184             if (!selector.isEmpty()) {
    185                 mRouter.addCallback(selector, mCallback);
    186             }
    187             mSelector = selector;
    188             refreshRoute();
    189 
    190             if (mButton != null) {
    191                 mButton.setRouteSelector(selector);
    192             }
    193         }
    194     }
    195 
    196     /**
    197      * Gets the media route dialog factory to use when showing the route chooser
    198      * or controller dialog.
    199      *
    200      * @return The dialog factory, never null.
    201      */
    202     @NonNull
    203     public MediaRouteDialogFactory getDialogFactory() {
    204         return mDialogFactory;
    205     }
    206 
    207     /**
    208      * Sets the media route dialog factory to use when showing the route chooser
    209      * or controller dialog.
    210      *
    211      * @param factory The dialog factory, must not be null.
    212      */
    213     public void setDialogFactory(@NonNull MediaRouteDialogFactory factory) {
    214         if (factory == null) {
    215             throw new IllegalArgumentException("factory must not be null");
    216         }
    217 
    218         if (mDialogFactory != factory) {
    219             mDialogFactory = factory;
    220 
    221             if (mButton != null) {
    222                 mButton.setDialogFactory(factory);
    223             }
    224         }
    225     }
    226 
    227     /**
    228      * Gets the associated media route button, or null if it has not yet been created.
    229      */
    230     @Nullable
    231     public MediaRouteButton getMediaRouteButton() {
    232         return mButton;
    233     }
    234 
    235     /**
    236      * Called when the media route button is being created.
    237      * <p>
    238      * Subclasses may override this method to customize the button.
    239      * </p>
    240      */
    241     public MediaRouteButton onCreateMediaRouteButton() {
    242         return new MediaRouteButton(getContext());
    243     }
    244 
    245     @Override
    246     @SuppressWarnings("deprecation")
    247     public View onCreateActionView() {
    248         if (mButton != null) {
    249             Log.e(TAG, "onCreateActionView: this ActionProvider is already associated " +
    250                     "with a menu item. Don't reuse MediaRouteActionProvider instances! " +
    251                     "Abandoning the old menu item...");
    252         }
    253 
    254         mButton = onCreateMediaRouteButton();
    255         mButton.setCheatSheetEnabled(true);
    256         mButton.setRouteSelector(mSelector);
    257         mButton.setDialogFactory(mDialogFactory);
    258         mButton.setLayoutParams(new ViewGroup.LayoutParams(
    259                 ViewGroup.LayoutParams.WRAP_CONTENT,
    260                 ViewGroup.LayoutParams.MATCH_PARENT));
    261         return mButton;
    262     }
    263 
    264     @Override
    265     public boolean onPerformDefaultAction() {
    266         if (mButton != null) {
    267             return mButton.showDialog();
    268         }
    269         return false;
    270     }
    271 
    272     @Override
    273     public boolean overridesItemVisibility() {
    274         return true;
    275     }
    276 
    277     @Override
    278     public boolean isVisible() {
    279         return mRouter.isRouteAvailable(mSelector,
    280                 MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
    281     }
    282 
    283     void refreshRoute() {
    284         refreshVisibility();
    285     }
    286 
    287     private static final class MediaRouterCallback extends MediaRouter.Callback {
    288         private final WeakReference<MediaRouteActionProvider> mProviderWeak;
    289 
    290         public MediaRouterCallback(MediaRouteActionProvider provider) {
    291             mProviderWeak = new WeakReference<MediaRouteActionProvider>(provider);
    292         }
    293 
    294         @Override
    295         public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
    296             refreshRoute(router);
    297         }
    298 
    299         @Override
    300         public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
    301             refreshRoute(router);
    302         }
    303 
    304         @Override
    305         public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
    306             refreshRoute(router);
    307         }
    308 
    309         @Override
    310         public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
    311             refreshRoute(router);
    312         }
    313 
    314         @Override
    315         public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
    316             refreshRoute(router);
    317         }
    318 
    319         @Override
    320         public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
    321             refreshRoute(router);
    322         }
    323 
    324         private void refreshRoute(MediaRouter router) {
    325             MediaRouteActionProvider provider = mProviderWeak.get();
    326             if (provider != null) {
    327                 provider.refreshRoute();
    328             } else {
    329                 router.removeCallback(this);
    330             }
    331         }
    332     }
    333 }
    334