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 android.content.Context;
20import android.content.Intent;
21import android.graphics.Bitmap;
22import android.os.Bundle;
23import android.support.v7.media.MediaItemStatus;
24import android.support.v7.media.MediaRouter.ControlRequestCallback;
25import android.support.v7.media.MediaRouter.RouteInfo;
26import android.support.v7.media.MediaSessionStatus;
27import android.support.v7.media.RemotePlaybackClient;
28import android.support.v7.media.RemotePlaybackClient.ItemActionCallback;
29import android.support.v7.media.RemotePlaybackClient.SessionActionCallback;
30import android.support.v7.media.RemotePlaybackClient.StatusCallback;
31import android.util.Log;
32
33import java.util.ArrayList;
34import java.util.List;
35
36/**
37 * Handles playback of media items using a remote route.
38 *
39 * This class is used as a backend by PlaybackManager to feed media items to
40 * the remote route. When the remote route doesn't support queuing, media items
41 * are fed one-at-a-time; otherwise media items are enqueued to the remote side.
42 */
43public class RemotePlayer extends Player {
44    private static final String TAG = "RemotePlayer";
45    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
46    private Context mContext;
47    private RouteInfo mRoute;
48    private boolean mEnqueuePending;
49    private Bitmap mSnapshot;
50    private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>();
51
52    private RemotePlaybackClient mClient;
53    private StatusCallback mStatusCallback = new StatusCallback() {
54        @Override
55        public void onItemStatusChanged(Bundle data,
56                String sessionId, MediaSessionStatus sessionStatus,
57                String itemId, MediaItemStatus itemStatus) {
58            logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus);
59            if (mCallback != null) {
60                mCallback.onPlaylistChanged();
61                int state = itemStatus.getPlaybackState();
62                if (state == MediaItemStatus.PLAYBACK_STATE_FINISHED) {
63                    mCallback.onCompletion();
64                } else if (state == MediaItemStatus.PLAYBACK_STATE_ERROR) {
65                    mCallback.onError();
66                }
67            }
68        }
69
70        @Override
71        public void onSessionStatusChanged(Bundle data,
72                String sessionId, MediaSessionStatus sessionStatus) {
73            logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null);
74            if (mCallback != null) {
75                mCallback.onPlaylistChanged();
76            }
77        }
78
79        @Override
80        public void onSessionChanged(String sessionId) {
81            if (DEBUG) {
82                Log.d(TAG, "onSessionChanged: sessionId=" + sessionId);
83            }
84        }
85    };
86
87    public RemotePlayer(Context context) {
88        mContext = context;
89    }
90
91    @Override
92    public boolean isRemotePlayback() {
93        return true;
94    }
95
96    @Override
97    public boolean isQueuingSupported() {
98        return mClient.isQueuingSupported();
99    }
100
101    @Override
102    public void connect(RouteInfo route) {
103        mRoute = route;
104        mClient = new RemotePlaybackClient(mContext, route);
105        mClient.setStatusCallback(mStatusCallback);
106
107        if (DEBUG) {
108            Log.d(TAG, "connected to: " + route
109                    + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported()
110                    + ", isQueuingSupported: "+ mClient.isQueuingSupported());
111        }
112    }
113
114    @Override
115    public void release() {
116        mClient.release();
117
118        if (DEBUG) {
119            Log.d(TAG, "released.");
120        }
121    }
122
123    // basic playback operations that are always supported
124    @Override
125    public void play(final PlaylistItem item) {
126        if (DEBUG) {
127            Log.d(TAG, "play: item=" + item);
128        }
129        mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
130            @Override
131            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
132                    String itemId, MediaItemStatus itemStatus) {
133                logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus);
134                item.setRemoteItemId(itemId);
135                if (item.getPosition() > 0) {
136                    seekInternal(item);
137                }
138                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
139                    pause();
140                } else {
141                    publishState(STATE_PLAYING);
142                }
143                if (mCallback != null) {
144                    mCallback.onPlaylistChanged();
145                }
146            }
147
148            @Override
149            public void onError(String error, int code, Bundle data) {
150                logError("play: failed", error, code);
151            }
152        });
153    }
154
155    @Override
156    public void seek(final PlaylistItem item) {
157        seekInternal(item);
158    }
159
160    @Override
161    public void getStatus(final PlaylistItem item, final boolean update) {
162        if (!mClient.hasSession() || item.getRemoteItemId() == null) {
163            // if session is not valid or item id not assigend yet.
164            // just return, it's not fatal
165            return;
166        }
167
168        if (DEBUG) {
169            Log.d(TAG, "getStatus: item=" + item + ", update=" + update);
170        }
171        mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() {
172            @Override
173            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
174                    String itemId, MediaItemStatus itemStatus) {
175                logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus);
176                int state = itemStatus.getPlaybackState();
177                if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING
178                        || state == MediaItemStatus.PLAYBACK_STATE_PAUSED
179                        || state == MediaItemStatus.PLAYBACK_STATE_PENDING) {
180                    item.setState(state);
181                    item.setPosition(itemStatus.getContentPosition());
182                    item.setDuration(itemStatus.getContentDuration());
183                    item.setTimestamp(itemStatus.getTimestamp());
184                }
185                if (update && mCallback != null) {
186                    mCallback.onPlaylistReady();
187                }
188            }
189
190            @Override
191            public void onError(String error, int code, Bundle data) {
192                logError("getStatus: failed", error, code);
193                if (update && mCallback != null) {
194                    mCallback.onPlaylistReady();
195                }
196            }
197        });
198    }
199
200    @Override
201    public void pause() {
202        if (!mClient.hasSession()) {
203            // ignore if no session
204            return;
205        }
206        if (DEBUG) {
207            Log.d(TAG, "pause");
208        }
209        mClient.pause(null, new SessionActionCallback() {
210            @Override
211            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
212                logStatus("pause: succeeded", sessionId, sessionStatus, null, null);
213                if (mCallback != null) {
214                    mCallback.onPlaylistChanged();
215                }
216                publishState(STATE_PAUSED);
217            }
218
219            @Override
220            public void onError(String error, int code, Bundle data) {
221                logError("pause: failed", error, code);
222            }
223        });
224    }
225
226    @Override
227    public void resume() {
228        if (!mClient.hasSession()) {
229            // ignore if no session
230            return;
231        }
232        if (DEBUG) {
233            Log.d(TAG, "resume");
234        }
235        mClient.resume(null, new SessionActionCallback() {
236            @Override
237            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
238                logStatus("resume: succeeded", sessionId, sessionStatus, null, null);
239                if (mCallback != null) {
240                    mCallback.onPlaylistChanged();
241                }
242                publishState(STATE_PLAYING);
243            }
244
245            @Override
246            public void onError(String error, int code, Bundle data) {
247                logError("resume: failed", error, code);
248            }
249        });
250    }
251
252    @Override
253    public void stop() {
254        if (!mClient.hasSession()) {
255            // ignore if no session
256            return;
257        }
258        publishState(STATE_IDLE);
259        if (DEBUG) {
260            Log.d(TAG, "stop");
261        }
262        mClient.stop(null, new SessionActionCallback() {
263            @Override
264            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
265                logStatus("stop: succeeded", sessionId, sessionStatus, null, null);
266                if (mClient.isSessionManagementSupported()) {
267                    endSession();
268                }
269                if (mCallback != null) {
270                    mCallback.onPlaylistChanged();
271                }
272            }
273
274            @Override
275            public void onError(String error, int code, Bundle data) {
276                logError("stop: failed", error, code);
277            }
278        });
279    }
280
281    // enqueue & remove are only supported if isQueuingSupported() returns true
282    @Override
283    public void enqueue(final PlaylistItem item) {
284        throwIfQueuingUnsupported();
285
286        if (!mClient.hasSession() && !mEnqueuePending) {
287            mEnqueuePending = true;
288            if (mClient.isSessionManagementSupported()) {
289                startSession(item);
290            } else {
291                enqueueInternal(item);
292            }
293        } else if (mEnqueuePending){
294            mTempQueue.add(item);
295        } else {
296            enqueueInternal(item);
297        }
298    }
299
300    @Override
301    public PlaylistItem remove(String itemId) {
302        throwIfNoSession();
303        throwIfQueuingUnsupported();
304
305        if (DEBUG) {
306            Log.d(TAG, "remove: itemId=" + itemId);
307        }
308        mClient.remove(itemId, null, new ItemActionCallback() {
309            @Override
310            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
311                    String itemId, MediaItemStatus itemStatus) {
312                logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus);
313            }
314
315            @Override
316            public void onError(String error, int code, Bundle data) {
317                logError("remove: failed", error, code);
318            }
319        });
320
321        return null;
322    }
323
324    @Override
325    public void takeSnapshot() {
326        mSnapshot = null;
327
328        Intent intent = new Intent(SampleMediaRouteProvider.ACTION_TAKE_SNAPSHOT);
329        intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);
330
331        if (mRoute != null && mRoute.supportsControlRequest(intent)) {
332            ControlRequestCallback callback = new ControlRequestCallback() {
333                @Override
334                public void onResult(Bundle data) {
335                    if (DEBUG) {
336                        Log.d(TAG, "takeSnapshot: succeeded: data=" + data);
337                    }
338                    if (data != null) {
339                        mSnapshot = data.getParcelable(SampleMediaRouteProvider.EXTRA_SNAPSHOT);
340                    }
341                }
342
343                @Override
344                public void onError(String error, Bundle data) {
345                    Log.d(TAG, "takeSnapshot: failed: error=" + error + ", data=" + data);
346                }
347            };
348
349            mRoute.sendControlRequest(intent, callback);
350        }
351    }
352
353    @Override
354    public Bitmap getSnapshot() {
355        return mSnapshot;
356    }
357
358    private void enqueueInternal(final PlaylistItem item) {
359        throwIfQueuingUnsupported();
360
361        if (DEBUG) {
362            Log.d(TAG, "enqueue: item=" + item);
363        }
364        mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
365            @Override
366            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
367                    String itemId, MediaItemStatus itemStatus) {
368                logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus);
369                item.setRemoteItemId(itemId);
370                if (item.getPosition() > 0) {
371                    seekInternal(item);
372                }
373                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
374                    pause();
375                } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
376                    publishState(STATE_PLAYING);
377                }
378                if (mEnqueuePending) {
379                    mEnqueuePending = false;
380                    for (PlaylistItem item : mTempQueue) {
381                        enqueueInternal(item);
382                    }
383                    mTempQueue.clear();
384                }
385                if (mCallback != null) {
386                    mCallback.onPlaylistChanged();
387                }
388            }
389
390            @Override
391            public void onError(String error, int code, Bundle data) {
392                logError("enqueue: failed", error, code);
393                if (mCallback != null) {
394                    mCallback.onPlaylistChanged();
395                }
396            }
397        });
398    }
399
400    private void seekInternal(final PlaylistItem item) {
401        throwIfNoSession();
402
403        if (DEBUG) {
404            Log.d(TAG, "seek: item=" + item);
405        }
406        mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() {
407           @Override
408           public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
409                   String itemId, MediaItemStatus itemStatus) {
410               logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus);
411               if (mCallback != null) {
412                   mCallback.onPlaylistChanged();
413               }
414           }
415
416           @Override
417           public void onError(String error, int code, Bundle data) {
418               logError("seek: failed", error, code);
419           }
420        });
421    }
422
423    private void startSession(final PlaylistItem item) {
424        mClient.startSession(null, new SessionActionCallback() {
425            @Override
426            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
427                logStatus("startSession: succeeded", sessionId, sessionStatus, null, null);
428                enqueueInternal(item);
429            }
430
431            @Override
432            public void onError(String error, int code, Bundle data) {
433                logError("startSession: failed", error, code);
434            }
435        });
436    }
437
438    private void endSession() {
439        mClient.endSession(null, new SessionActionCallback() {
440            @Override
441            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
442                logStatus("endSession: succeeded", sessionId, sessionStatus, null, null);
443            }
444
445            @Override
446            public void onError(String error, int code, Bundle data) {
447                logError("endSession: failed", error, code);
448            }
449        });
450    }
451
452    private void logStatus(String message,
453            String sessionId, MediaSessionStatus sessionStatus,
454            String itemId, MediaItemStatus itemStatus) {
455        if (DEBUG) {
456            String result = "";
457            if (sessionId != null && sessionStatus != null) {
458                result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus;
459            }
460            if (itemId != null & itemStatus != null) {
461                result += (result.isEmpty() ? "" : ", ")
462                        + "itemId=" + itemId + ", itemStatus=" + itemStatus;
463            }
464            Log.d(TAG, message + ": " + result);
465        }
466    }
467
468    private void logError(String message, String error, int code) {
469        Log.d(TAG, message + ": error=" + error + ", code=" + code);
470    }
471
472    private void throwIfNoSession() {
473        if (!mClient.hasSession()) {
474            throw new IllegalStateException("Session is invalid");
475        }
476    }
477
478    private void throwIfQueuingUnsupported() {
479        if (!isQueuingSupported()) {
480            throw new UnsupportedOperationException("Queuing is unsupported");
481        }
482    }
483}
484