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