SampleMediaRouteProvider.java revision 57456222f52fbfc1dcfb1b99030aad6acc02ad23
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 com.example.android.supportv7.media;
18
19import com.example.android.supportv7.R;
20
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.content.IntentFilter.MalformedMimeTypeException;
25import android.content.res.Resources;
26import android.media.MediaRouter;
27import android.net.Uri;
28import android.os.Bundle;
29import android.app.PendingIntent;
30import android.support.v7.media.MediaControlIntent;
31import android.support.v7.media.MediaItemStatus;
32import android.support.v7.media.MediaRouteProvider;
33import android.support.v7.media.MediaRouter.ControlRequestCallback;
34import android.support.v7.media.MediaRouteProviderDescriptor;
35import android.support.v7.media.MediaRouteDescriptor;
36import android.util.Log;
37import android.widget.Toast;
38import android.view.Gravity;
39import java.util.ArrayList;
40
41/**
42 * Demonstrates how to create a custom media route provider.
43 *
44 * @see SampleMediaRouteProviderService
45 */
46final class SampleMediaRouteProvider extends MediaRouteProvider {
47    private static final String TAG = "SampleMediaRouteProvider";
48
49    private static final String FIXED_VOLUME_ROUTE_ID = "fixed";
50    private static final String VARIABLE_VOLUME_ROUTE_ID = "variable";
51    private static final int VOLUME_MAX = 10;
52
53    /**
54     * A custom media control intent category for special requests that are
55     * supported by this provider's routes.
56     */
57    public static final String CATEGORY_SAMPLE_ROUTE =
58            "com.example.android.supportv7.media.CATEGORY_SAMPLE_ROUTE";
59
60    /**
61     * A custom media control intent action for special requests that are
62     * supported by this provider's routes.
63     * <p>
64     * This particular request is designed to return a bundle of not very
65     * interesting statistics for demonstration purposes.
66     * </p>
67     *
68     * @see #DATA_PLAYBACK_COUNT
69     */
70    public static final String ACTION_GET_STATISTICS =
71            "com.example.android.supportv7.media.ACTION_GET_STATISTICS";
72
73    /**
74     * {@link #ACTION_GET_STATISTICS} result data: Number of times the
75     * playback action was invoked.
76     */
77    public static final String DATA_PLAYBACK_COUNT =
78            "com.example.android.supportv7.media.EXTRA_PLAYBACK_COUNT";
79
80    /*
81     * Set ENABLE_QUEUEING to true to test queuing on MRP. This will make
82     * MRP expose the following two experimental hidden APIs:
83     *     ACTION_ENQUEUE
84     *     ACTION_REMOVE
85     */
86    public static final boolean ENABLE_QUEUEING = false;
87
88    private static final ArrayList<IntentFilter> CONTROL_FILTERS;
89    static {
90        IntentFilter f1 = new IntentFilter();
91        f1.addCategory(CATEGORY_SAMPLE_ROUTE);
92        f1.addAction(ACTION_GET_STATISTICS);
93
94        IntentFilter f2 = new IntentFilter();
95        f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
96        f2.addAction(MediaControlIntent.ACTION_PLAY);
97        f2.addDataScheme("http");
98        f2.addDataScheme("https");
99        f2.addDataScheme("rtsp");
100        f2.addDataScheme("file");
101        addDataTypeUnchecked(f2, "video/*");
102
103        IntentFilter f3 = new IntentFilter();
104        f3.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
105        f3.addAction(MediaControlIntent.ACTION_SEEK);
106        f3.addAction(MediaControlIntent.ACTION_GET_STATUS);
107        f3.addAction(MediaControlIntent.ACTION_PAUSE);
108        f3.addAction(MediaControlIntent.ACTION_RESUME);
109        f3.addAction(MediaControlIntent.ACTION_STOP);
110
111        IntentFilter f4 = new IntentFilter();
112        f4.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
113        f4.addAction(MediaControlIntent.ACTION_ENQUEUE);
114        f4.addDataScheme("http");
115        f4.addDataScheme("https");
116        f4.addDataScheme("rtsp");
117        f4.addDataScheme("file");
118        addDataTypeUnchecked(f4, "video/*");
119
120        IntentFilter f5 = new IntentFilter();
121        f5.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
122        f5.addAction(MediaControlIntent.ACTION_REMOVE);
123
124        CONTROL_FILTERS = new ArrayList<IntentFilter>();
125        CONTROL_FILTERS.add(f1);
126        CONTROL_FILTERS.add(f2);
127        CONTROL_FILTERS.add(f3);
128        if (ENABLE_QUEUEING) {
129            CONTROL_FILTERS.add(f4);
130            CONTROL_FILTERS.add(f5);
131        }
132    }
133
134    private static void addDataTypeUnchecked(IntentFilter filter, String type) {
135        try {
136            filter.addDataType(type);
137        } catch (MalformedMimeTypeException ex) {
138            throw new RuntimeException(ex);
139        }
140    }
141
142    private int mVolume = 5;
143    private int mEnqueueCount;
144
145    public SampleMediaRouteProvider(Context context) {
146        super(context);
147
148        publishRoutes();
149    }
150
151    @Override
152    public RouteController onCreateRouteController(String routeId) {
153        return new SampleRouteController(routeId);
154    }
155
156    private void publishRoutes() {
157        Resources r = getContext().getResources();
158
159        MediaRouteDescriptor routeDescriptor1 = new MediaRouteDescriptor.Builder(
160                FIXED_VOLUME_ROUTE_ID,
161                r.getString(R.string.fixed_volume_route_name))
162                .setDescription(r.getString(R.string.sample_route_description))
163                .addControlFilters(CONTROL_FILTERS)
164                .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
165                .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED)
166                .setVolume(VOLUME_MAX)
167                .build();
168
169        MediaRouteDescriptor routeDescriptor2 = new MediaRouteDescriptor.Builder(
170                VARIABLE_VOLUME_ROUTE_ID,
171                r.getString(R.string.variable_volume_route_name))
172                .setDescription(r.getString(R.string.sample_route_description))
173                .addControlFilters(CONTROL_FILTERS)
174                .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
175                .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
176                .setVolumeMax(VOLUME_MAX)
177                .setVolume(mVolume)
178                .build();
179
180        MediaRouteProviderDescriptor providerDescriptor =
181                new MediaRouteProviderDescriptor.Builder()
182                .addRoute(routeDescriptor1)
183                .addRoute(routeDescriptor2)
184                .build();
185        setDescriptor(providerDescriptor);
186    }
187
188    private void showToast(String msg) {
189        Toast toast = Toast.makeText(getContext(),
190                "[provider] " + msg, Toast.LENGTH_LONG);
191        toast.setGravity(Gravity.TOP, 0, 100);
192        toast.show();
193    }
194
195    private final class SampleRouteController extends MediaRouteProvider.RouteController {
196        private final String mRouteId;
197        // Create an overlay display window (used for simulating the remote playback only)
198        private final OverlayDisplayWindow mOverlay = new OverlayDisplayWindow(getContext(),
199                getContext().getResources().getString(R.string.sample_media_route_provider_remote),
200                1024, 768, Gravity.CENTER);
201        private final MediaPlayerWrapper mMediaPlayer = new MediaPlayerWrapper(getContext());
202        private final MediaSessionManager mSessionManager = new MediaSessionManager();
203
204        public SampleRouteController(String routeId) {
205            mRouteId = routeId;
206            mSessionManager.setCallback(mMediaPlayer);
207            mOverlay.setOverlayWindowListener(mMediaPlayer);
208            mMediaPlayer.setCallback(new MediaPlayerCallback());
209            Log.d(TAG, mRouteId + ": Controller created");
210        }
211
212        @Override
213        public void onRelease() {
214            Log.d(TAG, mRouteId + ": Controller released");
215            mMediaPlayer.release();
216        }
217
218        @Override
219        public void onSelect() {
220            Log.d(TAG, mRouteId + ": Selected");
221            mOverlay.show();
222        }
223
224        @Override
225        public void onUnselect() {
226            Log.d(TAG, mRouteId + ": Unselected");
227            mMediaPlayer.onStop();
228            mOverlay.dismiss();
229        }
230
231        @Override
232        public void onSetVolume(int volume) {
233            Log.d(TAG, mRouteId + ": Set volume to " + volume);
234            if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) {
235                setVolumeInternal(volume);
236            }
237        }
238
239        @Override
240        public void onUpdateVolume(int delta) {
241            Log.d(TAG, mRouteId + ": Update volume by " + delta);
242            if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) {
243                setVolumeInternal(mVolume + delta);
244            }
245        }
246
247        @Override
248        public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
249            Log.d(TAG, mRouteId + ": Received control request " + intent);
250            String action = intent.getAction();
251            if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
252                boolean success = false;
253                if (action.equals(MediaControlIntent.ACTION_PLAY)) {
254                    success = handlePlay(intent, callback);
255                } else if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) {
256                    success = handleEnqueue(intent, callback);
257                } else if (action.equals(MediaControlIntent.ACTION_REMOVE)) {
258                    success = handleRemove(intent, callback);
259                } else if (action.equals(MediaControlIntent.ACTION_SEEK)) {
260                    success = handleSeek(intent, callback);
261                } else if (action.equals(MediaControlIntent.ACTION_GET_STATUS)) {
262                    success = handleGetStatus(intent, callback);
263                } else if (action.equals(MediaControlIntent.ACTION_PAUSE)) {
264                    success = handlePause(intent, callback);
265                } else if (action.equals(MediaControlIntent.ACTION_RESUME)) {
266                    success = handleResume(intent, callback);
267                } else if (action.equals(MediaControlIntent.ACTION_STOP)) {
268                    success = handleStop(intent, callback);
269                }
270                Log.d(TAG, mSessionManager.toString());
271                return success;
272            }
273
274            if (action.equals(ACTION_GET_STATISTICS)
275                    && intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) {
276                Bundle data = new Bundle();
277                data.putInt(DATA_PLAYBACK_COUNT, mEnqueueCount);
278                if (callback != null) {
279                    callback.onResult(data);
280                }
281                return true;
282            }
283            return false;
284        }
285
286        private void setVolumeInternal(int volume) {
287            if (volume >= 0 && volume <= VOLUME_MAX) {
288                mVolume = volume;
289                Log.d(TAG, mRouteId + ": New volume is " + mVolume);
290                publishRoutes();
291            }
292        }
293
294        private boolean handlePlay(Intent intent, ControlRequestCallback callback) {
295            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
296            if (sid == null || mSessionManager.stop(sid)) {
297                Log.d(TAG, "handleEnqueue");
298                return handleEnqueue(intent, callback);
299            }
300            return false;
301        }
302
303        private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) {
304            if (intent.getData() == null) {
305                return false;
306            }
307
308            mEnqueueCount +=1;
309
310            boolean enqueue = intent.getAction().equals(MediaControlIntent.ACTION_ENQUEUE);
311            Uri uri = intent.getData();
312            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
313            long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
314            Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA);
315            Bundle headers = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS);
316            PendingIntent receiver = (PendingIntent)intent.getParcelableExtra(
317                    MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER);
318
319            Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request"
320                    + ", uri=" + uri
321                    + ", sid=" + sid
322                    + ", pos=" + pos
323                    + ", metadata=" + metadata
324                    + ", headers=" + headers
325                    + ", receiver=" + receiver);
326            MediaQueueItem item = mSessionManager.enqueue(sid, uri, receiver);
327            if (callback != null) {
328                if (item != null) {
329                    Bundle result = new Bundle();
330                    result.putString(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
331                    result.putString(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
332                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
333                            item.getStatus().asBundle());
334                    callback.onResult(result);
335                } else {
336                    callback.onError("Failed to open " + uri.toString(), null);
337                }
338            }
339            return true;
340        }
341
342        private boolean handleRemove(Intent intent, ControlRequestCallback callback) {
343            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
344            String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
345            MediaQueueItem item = mSessionManager.remove(sid, iid);
346            if (callback != null) {
347                if (item != null) {
348                    Bundle result = new Bundle();
349                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
350                            item.getStatus().asBundle());
351                    callback.onResult(result);
352                } else {
353                    callback.onError("Failed to remove" +
354                            ", sid=" + sid + ", iid=" + iid, null);
355                }
356            }
357            return (item != null);
358        }
359
360        private boolean handleSeek(Intent intent, ControlRequestCallback callback) {
361            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
362            String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
363            long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0);
364            Log.d(TAG, mRouteId + ": Received seek request, pos=" + pos);
365            MediaQueueItem item = mSessionManager.seek(sid, iid, pos);
366            if (callback != null) {
367                if (item != null) {
368                    Bundle result = new Bundle();
369                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
370                            item.getStatus().asBundle());
371                    callback.onResult(result);
372                } else {
373                    callback.onError("Failed to seek" +
374                            ", sid=" + sid + ", iid=" + iid + ", pos=" + pos, null);
375                }
376            }
377            return (item != null);
378        }
379
380        private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) {
381            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
382            String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
383            MediaQueueItem item = mSessionManager.getStatus(sid, iid);
384            if (callback != null) {
385                if (item != null) {
386                    Bundle result = new Bundle();
387                    result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
388                            item.getStatus().asBundle());
389                    callback.onResult(result);
390                } else {
391                    callback.onError("Failed to get status" +
392                            ", sid=" + sid + ", iid=" + iid, null);
393                }
394            }
395            return (item != null);
396        }
397
398        private boolean handlePause(Intent intent, ControlRequestCallback callback) {
399            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
400            boolean success = mSessionManager.pause(sid);
401            if (callback != null) {
402                if (success) {
403                    callback.onResult(null);
404                } else {
405                    callback.onError("Failed to pause, sid=" + sid, null);
406                }
407            }
408            return success;
409        }
410
411        private boolean handleResume(Intent intent, ControlRequestCallback callback) {
412            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
413            boolean success = mSessionManager.resume(sid);
414            if (callback != null) {
415                if (success) {
416                    callback.onResult(null);
417                } else {
418                    callback.onError("Failed to resume, sid=" + sid, null);
419                }
420            }
421            return success;
422        }
423
424        private boolean handleStop(Intent intent, ControlRequestCallback callback) {
425            String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
426            boolean success = mSessionManager.stop(sid);
427            if (callback != null) {
428                if (success) {
429                    callback.onResult(null);
430                } else {
431                    callback.onError("Failed to stop, sid=" + sid, null);
432                }
433            }
434            return success;
435        }
436
437        private void handleFinish(boolean error) {
438            MediaQueueItem item = mSessionManager.finish(error);
439            if (item != null) {
440                handleStatusChange(item);
441            }
442        }
443
444        private void handleStatusChange(MediaQueueItem item) {
445            if (item == null) {
446                item = mSessionManager.getCurrentItem();
447            }
448            if (item != null) {
449                PendingIntent receiver = item.getUpdateReceiver();
450                if (receiver != null) {
451                    Intent intent = new Intent();
452                    intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId());
453                    intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId());
454                    intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS,
455                            item.getStatus().asBundle());
456                    try {
457                        receiver.send(getContext(), 0, intent);
458                        Log.d(TAG, mRouteId + ": Sending status update from provider");
459                    } catch (PendingIntent.CanceledException e) {
460                        Log.d(TAG, mRouteId + ": Failed to send status update!");
461                    }
462                }
463            }
464        }
465
466        private final class MediaPlayerCallback extends MediaPlayerWrapper.Callback {
467            @Override
468            public void onError() {
469                handleFinish(true);
470            }
471
472            @Override
473            public void onCompletion() {
474                handleFinish(false);
475            }
476
477            @Override
478            public void onStatusChanged() {
479                handleStatusChange(null);
480            }
481
482            @Override
483            public void onSizeChanged(int width, int height) {
484                mOverlay.updateAspectRatio(width, height);
485            }
486        }
487    }
488}