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