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