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