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 * <menu xmlns:android="http://schemas.android.com/apk/res/android" 63 * xmlns:app="http://schemas.android.com/apk/res-auto"> 64 * <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 * </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