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 androidx.mediarouter.media;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.os.Handler;
25 import android.os.Message;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 import androidx.annotation.RestrictTo;
30 import androidx.core.util.ObjectsCompat;
31 import androidx.mediarouter.media.MediaRouter.ControlRequestCallback;
32 
33 /**
34  * Media route providers are used to publish additional media routes for
35  * use within an application.  Media route providers may also be declared
36  * as a service to publish additional media routes to all applications
37  * in the system.
38  * <p>
39  * The purpose of a media route provider is to discover media routes that satisfy
40  * the criteria specified by the current {@link MediaRouteDiscoveryRequest} and publish a
41  * {@link MediaRouteProviderDescriptor} with information about each route by calling
42  * {@link #setDescriptor} to notify the currently registered {@link Callback}.
43  * </p><p>
44  * The provider should watch for changes to the discovery request by implementing
45  * {@link #onDiscoveryRequestChanged} and updating the set of routes that it is
46  * attempting to discover.  It should also handle route control requests such
47  * as volume changes or {@link MediaControlIntent media control intents}
48  * by implementing {@link #onCreateRouteController} to return a {@link RouteController}
49  * for a particular route.
50  * </p><p>
51  * A media route provider may be used privately within the scope of a single
52  * application process by calling {@link MediaRouter#addProvider MediaRouter.addProvider}
53  * to add it to the local {@link MediaRouter}.  A media route provider may also be made
54  * available globally to all applications by registering a {@link MediaRouteProviderService}
55  * in the provider's manifest.  When the media route provider is registered
56  * as a service, all applications that use the media router API will be able to
57  * discover and used the provider's routes without having to install anything else.
58  * </p><p>
59  * This object must only be accessed on the main thread.
60  * </p>
61  */
62 public abstract class MediaRouteProvider {
63     static final int MSG_DELIVER_DESCRIPTOR_CHANGED = 1;
64     static final int MSG_DELIVER_DISCOVERY_REQUEST_CHANGED = 2;
65 
66     private final Context mContext;
67     private final ProviderMetadata mMetadata;
68     private final ProviderHandler mHandler = new ProviderHandler();
69 
70     private Callback mCallback;
71 
72     private MediaRouteDiscoveryRequest mDiscoveryRequest;
73     private boolean mPendingDiscoveryRequestChange;
74 
75     private MediaRouteProviderDescriptor mDescriptor;
76     private boolean mPendingDescriptorChange;
77 
78     /**
79      * Creates a media route provider.
80      *
81      * @param context The context.
82      */
MediaRouteProvider(@onNull Context context)83     public MediaRouteProvider(@NonNull Context context) {
84         this(context, null);
85     }
86 
MediaRouteProvider(Context context, ProviderMetadata metadata)87     MediaRouteProvider(Context context, ProviderMetadata metadata) {
88         if (context == null) {
89             throw new IllegalArgumentException("context must not be null");
90         }
91 
92         mContext = context;
93         if (metadata == null) {
94             mMetadata = new ProviderMetadata(new ComponentName(context, getClass()));
95         } else {
96             mMetadata = metadata;
97         }
98     }
99 
100     /**
101      * Gets the context of the media route provider.
102      */
getContext()103     public final Context getContext() {
104         return mContext;
105     }
106 
107     /**
108      * Gets the provider's handler which is associated with the main thread.
109      */
getHandler()110     public final Handler getHandler() {
111         return mHandler;
112     }
113 
114     /**
115      * Gets some metadata about the provider's implementation.
116      */
getMetadata()117     public final ProviderMetadata getMetadata() {
118         return mMetadata;
119     }
120 
121     /**
122      * Sets a callback to invoke when the provider's descriptor changes.
123      *
124      * @param callback The callback to use, or null if none.
125      */
setCallback(@ullable Callback callback)126     public final void setCallback(@Nullable Callback callback) {
127         MediaRouter.checkCallingThread();
128         mCallback = callback;
129     }
130 
131     /**
132      * Gets the current discovery request which informs the provider about the
133      * kinds of routes to discover and whether to perform active scanning.
134      *
135      * @return The current discovery request, or null if no discovery is needed at this time.
136      *
137      * @see #onDiscoveryRequestChanged
138      */
139     @Nullable
getDiscoveryRequest()140     public final MediaRouteDiscoveryRequest getDiscoveryRequest() {
141         return mDiscoveryRequest;
142     }
143 
144     /**
145      * Sets a discovery request to inform the provider about the kinds of
146      * routes that its clients would like to discover and whether to perform active scanning.
147      *
148      * @param request The discovery request, or null if no discovery is needed at this time.
149      *
150      * @see #onDiscoveryRequestChanged
151      */
setDiscoveryRequest(MediaRouteDiscoveryRequest request)152     public final void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
153         MediaRouter.checkCallingThread();
154 
155         if (ObjectsCompat.equals(mDiscoveryRequest, request)) {
156             return;
157         }
158 
159         mDiscoveryRequest = request;
160         if (!mPendingDiscoveryRequestChange) {
161             mPendingDiscoveryRequestChange = true;
162             mHandler.sendEmptyMessage(MSG_DELIVER_DISCOVERY_REQUEST_CHANGED);
163         }
164     }
165 
deliverDiscoveryRequestChanged()166     void deliverDiscoveryRequestChanged() {
167         mPendingDiscoveryRequestChange = false;
168         onDiscoveryRequestChanged(mDiscoveryRequest);
169     }
170 
171     /**
172      * Called by the media router when the {@link MediaRouteDiscoveryRequest discovery request}
173      * has changed.
174      * <p>
175      * Whenever an applications calls {@link MediaRouter#addCallback} to register
176      * a callback, it also provides a selector to specify the kinds of routes that
177      * it is interested in.  The media router combines all of these selectors together
178      * to generate a {@link MediaRouteDiscoveryRequest} and notifies each provider when a change
179      * occurs by calling {@link #setDiscoveryRequest} which posts a message to invoke
180      * this method asynchronously.
181      * </p><p>
182      * The provider should examine the {@link MediaControlIntent media control categories}
183      * in the discovery request's {@link MediaRouteSelector selector} to determine what
184      * kinds of routes it should try to discover and whether it should perform active
185      * or passive scans.  In many cases, the provider may be able to save power by
186      * determining that the selector does not contain any categories that it supports
187      * and it can therefore avoid performing any scans at all.
188      * </p>
189      *
190      * @param request The new discovery request, or null if no discovery is needed at this time.
191      *
192      * @see MediaRouter#addCallback
193      */
onDiscoveryRequestChanged(@ullable MediaRouteDiscoveryRequest request)194     public void onDiscoveryRequestChanged(@Nullable MediaRouteDiscoveryRequest request) {
195     }
196 
197     /**
198      * Gets the provider's descriptor.
199      * <p>
200      * The descriptor describes the state of the media route provider and
201      * the routes that it publishes.  Watch for changes to the descriptor
202      * by registering a {@link Callback callback} with {@link #setCallback}.
203      * </p>
204      *
205      * @return The media route provider descriptor, or null if none.
206      *
207      * @see Callback#onDescriptorChanged
208      */
209     @Nullable
getDescriptor()210     public final MediaRouteProviderDescriptor getDescriptor() {
211         return mDescriptor;
212     }
213 
214     /**
215      * Sets the provider's descriptor.
216      * <p>
217      * The provider must call this method to notify the currently registered
218      * {@link Callback callback} about the change to the provider's descriptor.
219      * </p>
220      *
221      * @param descriptor The updated route provider descriptor, or null if none.
222      *
223      * @see Callback#onDescriptorChanged
224      */
setDescriptor(@ullable MediaRouteProviderDescriptor descriptor)225     public final void setDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
226         MediaRouter.checkCallingThread();
227 
228         if (mDescriptor != descriptor) {
229             mDescriptor = descriptor;
230             if (!mPendingDescriptorChange) {
231                 mPendingDescriptorChange = true;
232                 mHandler.sendEmptyMessage(MSG_DELIVER_DESCRIPTOR_CHANGED);
233             }
234         }
235     }
236 
deliverDescriptorChanged()237     void deliverDescriptorChanged() {
238         mPendingDescriptorChange = false;
239 
240         if (mCallback != null) {
241             mCallback.onDescriptorChanged(this, mDescriptor);
242         }
243     }
244 
245     /**
246      * Called by the media router to obtain a route controller for a particular route.
247      * <p>
248      * The media router will invoke the {@link RouteController#onRelease} method of the route
249      * controller when it is no longer needed to allow it to free its resources.
250      * </p>
251      *
252      * @param routeId The unique id of the route.
253      * @return The route controller.  Returns null if there is no such route or if the route
254      * cannot be controlled using the route controller interface.
255      */
256     @Nullable
onCreateRouteController(@onNull String routeId)257     public RouteController onCreateRouteController(@NonNull String routeId) {
258         if (routeId == null) {
259             throw new IllegalArgumentException("routeId cannot be null");
260         }
261         return null;
262     }
263 
264     /**
265      * Called by the media router to obtain a route controller for a particular route which is a
266      * member of {@link MediaRouter.RouteGroup}.
267      * <p>
268      * The media router will invoke the {@link RouteController#onRelease} method of the route
269      * controller when it is no longer needed to allow it to free its resources.
270      * </p>
271      *
272      * @param routeId The unique id of the member route.
273      * @param routeGroupId The unique id of the route group.
274      * @return The route controller.  Returns null if there is no such route or if the route
275      * cannot be controlled using the route controller interface.
276      * @hide
277      */
278     @RestrictTo(LIBRARY_GROUP)
279     @Nullable
onCreateRouteController(@onNull String routeId, @NonNull String routeGroupId)280     public RouteController onCreateRouteController(@NonNull String routeId,
281             @NonNull String routeGroupId) {
282         if (routeId == null) {
283             throw new IllegalArgumentException("routeId cannot be null");
284         }
285         if (routeGroupId == null) {
286             throw new IllegalArgumentException("routeGroupId cannot be null");
287         }
288         return onCreateRouteController(routeId);
289     }
290 
291     /**
292      * Describes properties of the route provider's implementation.
293      * <p>
294      * This object is immutable once created.
295      * </p>
296      */
297     public static final class ProviderMetadata {
298         private final ComponentName mComponentName;
299 
ProviderMetadata(ComponentName componentName)300         ProviderMetadata(ComponentName componentName) {
301             if (componentName == null) {
302                 throw new IllegalArgumentException("componentName must not be null");
303             }
304             mComponentName = componentName;
305         }
306 
307         /**
308          * Gets the provider's package name.
309          */
getPackageName()310         public String getPackageName() {
311             return mComponentName.getPackageName();
312         }
313 
314         /**
315          * Gets the provider's component name.
316          */
getComponentName()317         public ComponentName getComponentName() {
318             return mComponentName;
319         }
320 
321         @Override
toString()322         public String toString() {
323             return "ProviderMetadata{ componentName="
324                     + mComponentName.flattenToShortString() + " }";
325         }
326     }
327 
328     /**
329      * Provides control over a particular route.
330      * <p>
331      * The media router obtains a route controller for a route whenever it needs
332      * to control a route.  When a route is selected, the media router invokes
333      * the {@link #onSelect} method of its route controller.  While selected,
334      * the media router may call other methods of the route controller to
335      * request that it perform certain actions to the route.  When a route is
336      * unselected, the media router invokes the {@link #onUnselect} method of its
337      * route controller.  When the media route no longer needs the route controller
338      * it will invoke the {@link #onRelease} method to allow the route controller
339      * to free its resources.
340      * </p><p>
341      * There may be multiple route controllers simultaneously active for the
342      * same route.  Each route controller will be released separately.
343      * </p><p>
344      * All operations on the route controller are asynchronous and
345      * results are communicated via callbacks.
346      * </p>
347      */
348     public static abstract class RouteController {
349         /**
350          * Releases the route controller, allowing it to free its resources.
351          */
onRelease()352         public void onRelease() {
353         }
354 
355         /**
356          * Selects the route.
357          */
onSelect()358         public void onSelect() {
359         }
360 
361         /**
362          * Unselects the route.
363          */
onUnselect()364         public void onUnselect() {
365         }
366 
367         /**
368          * Unselects the route and provides a reason. The default implementation
369          * calls {@link #onUnselect()}.
370          * <p>
371          * The reason provided will be one of the following:
372          * <ul>
373          * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
374          * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
375          * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
376          * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
377          * </ul>
378          *
379          * @param reason The reason for unselecting the route.
380          */
onUnselect(int reason)381         public void onUnselect(int reason) {
382             onUnselect();
383         }
384 
385         /**
386          * Requests to set the volume of the route.
387          *
388          * @param volume The new volume value between 0 and {@link MediaRouteDescriptor#getVolumeMax}.
389          */
onSetVolume(int volume)390         public void onSetVolume(int volume) {
391         }
392 
393         /**
394          * Requests an incremental volume update for the route.
395          *
396          * @param delta The delta to add to the current volume.
397          */
onUpdateVolume(int delta)398         public void onUpdateVolume(int delta) {
399         }
400 
401         /**
402          * Performs a {@link MediaControlIntent media control} request
403          * asynchronously on behalf of the route.
404          *
405          * @param intent A {@link MediaControlIntent media control intent}.
406          * @param callback A {@link ControlRequestCallback} to invoke with the result
407          * of the request, or null if no result is required.
408          * @return True if the controller intends to handle the request and will
409          * invoke the callback when finished.  False if the controller will not
410          * handle the request and will not invoke the callback.
411          *
412          * @see MediaControlIntent
413          */
onControlRequest(Intent intent, @Nullable ControlRequestCallback callback)414         public boolean onControlRequest(Intent intent, @Nullable ControlRequestCallback callback) {
415             return false;
416         }
417     }
418 
419     /**
420      * Callback which is invoked when route information becomes available or changes.
421      */
422     public static abstract class Callback {
423         /**
424          * Called when information about a route provider and its routes changes.
425          *
426          * @param provider The media route provider that changed, never null.
427          * @param descriptor The new media route provider descriptor, or null if none.
428          */
onDescriptorChanged(@onNull MediaRouteProvider provider, @Nullable MediaRouteProviderDescriptor descriptor)429         public void onDescriptorChanged(@NonNull MediaRouteProvider provider,
430                 @Nullable MediaRouteProviderDescriptor descriptor) {
431         }
432     }
433 
434     private final class ProviderHandler extends Handler {
ProviderHandler()435         ProviderHandler() {
436         }
437 
438         @Override
handleMessage(Message msg)439         public void handleMessage(Message msg) {
440             switch (msg.what) {
441                 case MSG_DELIVER_DESCRIPTOR_CHANGED:
442                     deliverDescriptorChanged();
443                     break;
444                 case MSG_DELIVER_DISCOVERY_REQUEST_CHANGED:
445                     deliverDiscoveryRequestChanged();
446                     break;
447             }
448         }
449     }
450 }
451