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