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
17package android.support.v7.app;
18
19import android.content.Context;
20import android.support.annotation.NonNull;
21import android.support.annotation.Nullable;
22import android.support.v4.view.ActionProvider;
23import android.support.v7.media.MediaRouter;
24import android.support.v7.media.MediaRouteSelector;
25import android.util.Log;
26import android.view.View;
27import android.view.ViewGroup;
28
29import 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 ActionBarActivity} 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 */
128public 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     */
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
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     */
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
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     */
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
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     */
240    public MediaRouteButton onCreateMediaRouteButton() {
241        return new MediaRouteButton(getContext());
242    }
243
244    @Override
245    @SuppressWarnings("deprecation")
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
264    public boolean onPerformDefaultAction() {
265        if (mButton != null) {
266            return mButton.showDialog();
267        }
268        return false;
269    }
270
271    @Override
272    public boolean overridesItemVisibility() {
273        return true;
274    }
275
276    @Override
277    public boolean isVisible() {
278        return mRouter.isRouteAvailable(mSelector,
279                MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
280    }
281
282    private void refreshRoute() {
283        refreshVisibility();
284    }
285
286    private static final class MediaRouterCallback extends MediaRouter.Callback {
287        private final WeakReference<MediaRouteActionProvider> mProviderWeak;
288
289        public MediaRouterCallback(MediaRouteActionProvider provider) {
290            mProviderWeak = new WeakReference<MediaRouteActionProvider>(provider);
291        }
292
293        @Override
294        public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
295            refreshRoute(router);
296        }
297
298        @Override
299        public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
300            refreshRoute(router);
301        }
302
303        @Override
304        public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
305            refreshRoute(router);
306        }
307
308        @Override
309        public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
310            refreshRoute(router);
311        }
312
313        @Override
314        public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
315            refreshRoute(router);
316        }
317
318        @Override
319        public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
320            refreshRoute(router);
321        }
322
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