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