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