InputSessionManager.java revision d41f0075a7d2ea826204e81fcec57d0aa57171a9
1/*
2 * Copyright (C) 2015 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.android.tv;
18
19import android.annotation.TargetApi;
20import android.content.Context;
21import android.media.tv.TvContentRating;
22import android.media.tv.TvInputInfo;
23import android.media.tv.TvRecordingClient;
24import android.media.tv.TvRecordingClient.RecordingCallback;
25import android.media.tv.TvTrackInfo;
26import android.media.tv.TvView;
27import android.media.tv.TvView.TvInputCallback;
28import android.net.Uri;
29import android.os.Build;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.Looper;
33import android.support.annotation.MainThread;
34import android.support.annotation.NonNull;
35import android.support.annotation.Nullable;
36import android.text.TextUtils;
37import android.util.ArraySet;
38import android.util.Log;
39
40import com.android.tv.common.SoftPreconditions;
41import com.android.tv.data.Channel;
42import com.android.tv.ui.TunableTvView;
43import com.android.tv.ui.TunableTvView.OnTuneListener;
44import com.android.tv.util.TvInputManagerHelper;
45
46import java.util.Collections;
47import java.util.List;
48import java.util.Objects;
49import java.util.Set;
50
51/**
52 * Manages input sessions.
53 * Responsible for:
54 * <ul>
55 *     <li>Manage {@link TvView} sessions and recording sessions</li>
56 *     <li>Manage capabilities (conflict)</li>
57 * </ul>
58 * <p>
59 * As TvView's methods should be called on the main thread and the {@link RecordingSession} should
60 * look at the state of the {@link TvViewSession} when it calls the framework methods, the framework
61 * calls in RecordingSession are made on the main thread not to introduce the multi-thread problems.
62 */
63@TargetApi(Build.VERSION_CODES.N)
64public class InputSessionManager {
65    private static final String TAG = "InputSessionManager";
66    private static final boolean DEBUG = false;
67
68    private final Context mContext;
69    private final TvInputManagerHelper mInputManager;
70    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
71    private final Set<TvViewSession> mTvViewSessions = new ArraySet<>();
72    private final Set<RecordingSession> mRecordingSessions =
73            Collections.synchronizedSet(new ArraySet<>());
74    private final Set<OnTvViewChannelChangeListener> mOnTvViewChannelChangeListeners =
75            new ArraySet<>();
76
77    public InputSessionManager(Context context) {
78        mContext = context.getApplicationContext();
79        mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper();
80    }
81
82    /**
83     * Creates the session for {@link TvView}.
84     * <p>
85     * Do not call {@link TvView#setCallback} after the session is created.
86     */
87    @MainThread
88    @NonNull
89    public TvViewSession createTvViewSession(TvView tvView, TunableTvView tunableTvView,
90            TvInputCallback callback) {
91        TvViewSession session = new TvViewSession(tvView, tunableTvView, callback);
92        mTvViewSessions.add(session);
93        if (DEBUG) Log.d(TAG, "TvView session created: " + session);
94        return session;
95    }
96
97    /**
98     * Releases the {@link TvView} session.
99     */
100    @MainThread
101    public void releaseTvViewSession(TvViewSession session) {
102        mTvViewSessions.remove(session);
103        session.reset();
104        if (DEBUG) Log.d(TAG, "TvView session released: " + session);
105    }
106
107    /**
108     * Creates the session for recording.
109     */
110    @NonNull
111    public RecordingSession createRecordingSession(String inputId, String tag,
112            RecordingCallback callback, Handler handler, long endTimeMs) {
113        RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs);
114        mRecordingSessions.add(session);
115        if (DEBUG) Log.d(TAG, "Recording session created: " + session);
116        return session;
117    }
118
119    /**
120     * Releases the recording session.
121     */
122    public void releaseRecordingSession(RecordingSession session) {
123        mRecordingSessions.remove(session);
124        session.release();
125        if (DEBUG) Log.d(TAG, "Recording session released: " + session);
126    }
127
128    /**
129     * Adds the {@link OnTvViewChannelChangeListener}.
130     */
131    @MainThread
132    public void addOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) {
133        mOnTvViewChannelChangeListeners.add(listener);
134    }
135
136    /**
137     * Removes the {@link OnTvViewChannelChangeListener}.
138     */
139    @MainThread
140    public void removeOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) {
141        mOnTvViewChannelChangeListeners.remove(listener);
142    }
143
144    @MainThread
145    void notifyTvViewChannelChange(Uri channelUri) {
146        for (OnTvViewChannelChangeListener l : mOnTvViewChannelChangeListeners) {
147            l.onTvViewChannelChange(channelUri);
148        }
149    }
150
151    /**
152     * Returns the current {@link TvView} channel.
153     */
154    @MainThread
155    public Uri getCurrentTvViewChannelUri() {
156        for (TvViewSession session : mTvViewSessions) {
157            if (session.mTuned) {
158                return session.mChannelUri;
159            }
160        }
161        return null;
162    }
163
164    /**
165     * Retruns the earliest end time of recording sessions in progress of the certain TV input.
166     */
167    @MainThread
168    public Long getEarliestRecordingSessionEndTimeMs(String inputId) {
169        long timeMs = Long.MAX_VALUE;
170        synchronized (mRecordingSessions) {
171            for (RecordingSession session : mRecordingSessions) {
172                if (session.mTuned && TextUtils.equals(inputId, session.mInputId)) {
173                    if (session.mEndTimeMs < timeMs) {
174                        timeMs = session.mEndTimeMs;
175                    }
176                }
177            }
178        }
179        return timeMs == Long.MAX_VALUE ? null : timeMs;
180    }
181
182    @MainThread
183    int getTunedTvViewSessionCount(String inputId) {
184        int tunedCount = 0;
185        for (TvViewSession session : mTvViewSessions) {
186            if (session.mTuned && Objects.equals(inputId, session.mInputId)) {
187                ++tunedCount;
188            }
189        }
190        return tunedCount;
191    }
192
193    @MainThread
194    boolean isTunedForTvView(Uri channelUri) {
195        for (TvViewSession session : mTvViewSessions) {
196            if (session.mTuned && Objects.equals(channelUri, session.mChannelUri)) {
197                return true;
198            }
199        }
200        return false;
201    }
202
203    int getTunedRecordingSessionCount(String inputId) {
204        synchronized (mRecordingSessions) {
205            int tunedCount = 0;
206            for (RecordingSession session : mRecordingSessions) {
207                if (session.mTuned && Objects.equals(inputId, session.mInputId)) {
208                    ++tunedCount;
209                }
210            }
211            return tunedCount;
212        }
213    }
214
215    boolean isTunedForRecording(Uri channelUri) {
216        synchronized (mRecordingSessions) {
217            for (RecordingSession session : mRecordingSessions) {
218                if (session.mTuned && Objects.equals(channelUri, session.mChannelUri)) {
219                    return true;
220                }
221            }
222            return false;
223        }
224    }
225
226    /**
227     * The session for {@link TvView}.
228     * <p>
229     * The methods which create or release session for the TV input should be called through this
230     * session.
231     */
232    @MainThread
233    public class TvViewSession {
234        private final TvView mTvView;
235        private final TunableTvView mTunableTvView;
236        private final TvInputCallback mCallback;
237        private Channel mChannel;
238        private String mInputId;
239        private Uri mChannelUri;
240        private Bundle mParams;
241        private OnTuneListener mOnTuneListener;
242        private boolean mTuned;
243        private boolean mNeedToBeRetuned;
244
245        TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback) {
246            mTvView = tvView;
247            mTunableTvView = tunableTvView;
248            mCallback = callback;
249            mTvView.setCallback(new DelegateTvInputCallback(mCallback) {
250                @Override
251                public void onConnectionFailed(String inputId) {
252                    if (DEBUG) Log.d(TAG, "TvViewSession: commection failed");
253                    mTuned = false;
254                    mNeedToBeRetuned = false;
255                    super.onConnectionFailed(inputId);
256                    notifyTvViewChannelChange(null);
257                }
258
259                @Override
260                public void onDisconnected(String inputId) {
261                    if (DEBUG) Log.d(TAG, "TvViewSession: disconnected");
262                    mTuned = false;
263                    mNeedToBeRetuned = false;
264                    super.onDisconnected(inputId);
265                    notifyTvViewChannelChange(null);
266                }
267            });
268        }
269
270        /**
271         * Tunes to the channel.
272         * <p>
273         * As this is called only for the warming up, there's no need to be retuned.
274         */
275        public void tune(String inputId, Uri channelUri) {
276            if (DEBUG) {
277                Log.d(TAG, "warm-up tune: {input=" + inputId + ", channelUri=" + channelUri + "}");
278            }
279            mInputId = inputId;
280            mChannelUri = channelUri;
281            mTuned = true;
282            mNeedToBeRetuned = false;
283            mTvView.tune(inputId, channelUri);
284            notifyTvViewChannelChange(channelUri);
285        }
286
287        /**
288         * Tunes to the channel.
289         */
290        public void tune(Channel channel, Bundle params, OnTuneListener listener) {
291            if (DEBUG) {
292                Log.d(TAG, "tune: {session=" + this + ", channel=" + channel + ", params=" + params
293                        + ", listener=" + listener + ", mTuned=" + mTuned + "}");
294            }
295            mChannel = channel;
296            mInputId = channel.getInputId();
297            mChannelUri = channel.getUri();
298            mParams = params;
299            mOnTuneListener = listener;
300            TvInputInfo input = mInputManager.getTvInputInfo(mInputId);
301            if (input == null || (input.canRecord() && !isTunedForRecording(mChannelUri)
302                    && getTunedRecordingSessionCount(mInputId) >= input.getTunerCount())) {
303                if (DEBUG) {
304                    if (input == null) {
305                        Log.d(TAG, "Can't find input for input ID: " + mInputId);
306                    } else {
307                        Log.d(TAG, "No more tuners to tune for input: " + input);
308                    }
309                }
310                mCallback.onConnectionFailed(mInputId);
311                // Release the previous session to not to hold the unnecessary session.
312                resetByRecording();
313                return;
314            }
315            mTuned = true;
316            mNeedToBeRetuned = false;
317            mTvView.tune(mInputId, mChannelUri, params);
318            notifyTvViewChannelChange(mChannelUri);
319        }
320
321        void retune() {
322            if (DEBUG) Log.d(TAG, "Retune requested.");
323            if (mNeedToBeRetuned) {
324                if (DEBUG) Log.d(TAG, "Retuning: {channel=" + mChannel + "}");
325                mTunableTvView.tuneTo(mChannel, mParams, mOnTuneListener);
326                mNeedToBeRetuned = false;
327            }
328        }
329
330        /**
331         * Plays a given recorded TV program.
332         *
333         * @see TvView#timeShiftPlay
334         */
335        public void timeShiftPlay(String inputId, Uri recordedProgramUri) {
336            mTuned = false;
337            mNeedToBeRetuned = false;
338            mTvView.timeShiftPlay(inputId, recordedProgramUri);
339            notifyTvViewChannelChange(null);
340        }
341
342        /**
343         * Resets this TvView.
344         */
345        public void reset() {
346            if (DEBUG) Log.d(TAG, "Reset TvView session");
347            mTuned = false;
348            mTvView.reset();
349            mNeedToBeRetuned = false;
350            notifyTvViewChannelChange(null);
351        }
352
353        void resetByRecording() {
354            mCallback.onVideoUnavailable(mInputId,
355                    TunableTvView.VIDEO_UNAVAILABLE_REASON_NO_RESOURCE);
356            if (mTuned) {
357                if (DEBUG) Log.d(TAG, "Reset TvView session by recording");
358                mTunableTvView.resetByRecording();
359                reset();
360            }
361            mNeedToBeRetuned = true;
362        }
363    }
364
365    /**
366     * The session for recording.
367     * <p>
368     * The caller is responsible for releasing the session when the error occurs.
369     */
370    public class RecordingSession {
371        private final String mInputId;
372        private Uri mChannelUri;
373        private final RecordingCallback mCallback;
374        private final Handler mHandler;
375        private volatile long mEndTimeMs;
376        private TvRecordingClient mClient;
377        private boolean mTuned;
378
379        RecordingSession(String inputId, String tag, RecordingCallback callback,
380                Handler handler, long endTimeMs) {
381            mInputId = inputId;
382            mCallback = callback;
383            mHandler = handler;
384            mClient = new TvRecordingClient(mContext, tag, callback, handler);
385            mEndTimeMs = endTimeMs;
386        }
387
388        void release() {
389            if (DEBUG) Log.d(TAG, "Release of recording session requested.");
390            runOnHandler(mMainThreadHandler, new Runnable() {
391                @Override
392                public void run() {
393                    if (DEBUG) Log.d(TAG, "Releasing of recording session.");
394                    mTuned = false;
395                    mClient.release();
396                    mClient = null;
397                    for (TvViewSession session : mTvViewSessions) {
398                        if (DEBUG) {
399                            Log.d(TAG, "Finding TvView sessions for retune: {tuned="
400                                    + session.mTuned + ", inputId=" + session.mInputId
401                                    + ", session=" + session + "}");
402                        }
403                        if (!session.mTuned && Objects.equals(session.mInputId, mInputId)) {
404                            session.retune();
405                            break;
406                        }
407                    }
408                }
409            });
410        }
411
412        /**
413         * Tunes to the channel for recording.
414         */
415        public void tune(String inputId, Uri channelUri) {
416            runOnHandler(mMainThreadHandler, new Runnable() {
417                @Override
418                public void run() {
419                    int tunedRecordingSessionCount = getTunedRecordingSessionCount(inputId);
420                    TvInputInfo input = mInputManager.getTvInputInfo(inputId);
421                    if (input == null || !input.canRecord()
422                            || input.getTunerCount() <= tunedRecordingSessionCount) {
423                        runOnHandler(mHandler, new Runnable() {
424                            @Override
425                            public void run() {
426                                mCallback.onConnectionFailed(inputId);
427                            }
428                        });
429                        return;
430                    }
431                    mTuned = true;
432                    int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId);
433                    if (!isTunedForTvView(channelUri) && tunedTuneSessionCount > 0
434                            && tunedRecordingSessionCount + tunedTuneSessionCount
435                                    >= input.getTunerCount()) {
436                        for (TvViewSession session : mTvViewSessions) {
437                            if (session.mTuned && Objects.equals(session.mInputId, inputId)
438                                    && !isTunedForRecording(session.mChannelUri)) {
439                                session.resetByRecording();
440                                break;
441                            }
442                        }
443                    }
444                    mChannelUri = channelUri;
445                    mClient.tune(inputId, channelUri);
446                }
447            });
448        }
449
450        /**
451         * Starts recording.
452         */
453        public void startRecording(Uri programHintUri) {
454            mClient.startRecording(programHintUri);
455        }
456
457        /**
458         * Stops recording.
459         */
460        public void stopRecording() {
461            mClient.stopRecording();
462        }
463
464        /**
465         * Sets recording session's ending time.
466         */
467        public void setEndTimeMs(long endTimeMs) {
468            mEndTimeMs = endTimeMs;
469        }
470
471        private void runOnHandler(Handler handler, Runnable runnable) {
472            if (Looper.myLooper() == handler.getLooper()) {
473                runnable.run();
474            } else {
475                handler.post(runnable);
476            }
477        }
478    }
479
480    private static class DelegateTvInputCallback extends TvInputCallback {
481        private final TvInputCallback mDelegate;
482
483        DelegateTvInputCallback(TvInputCallback delegate) {
484            mDelegate = delegate;
485        }
486
487        @Override
488        public void onConnectionFailed(String inputId) {
489            mDelegate.onConnectionFailed(inputId);
490        }
491
492        @Override
493        public void onDisconnected(String inputId) {
494            mDelegate.onDisconnected(inputId);
495        }
496
497        @Override
498        public void onChannelRetuned(String inputId, Uri channelUri) {
499            mDelegate.onChannelRetuned(inputId, channelUri);
500        }
501
502        @Override
503        public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
504            mDelegate.onTracksChanged(inputId, tracks);
505        }
506
507        @Override
508        public void onTrackSelected(String inputId, int type, String trackId) {
509            mDelegate.onTrackSelected(inputId, type, trackId);
510        }
511
512        @Override
513        public void onVideoSizeChanged(String inputId, int width, int height) {
514            mDelegate.onVideoSizeChanged(inputId, width, height);
515        }
516
517        @Override
518        public void onVideoAvailable(String inputId) {
519            mDelegate.onVideoAvailable(inputId);
520        }
521
522        @Override
523        public void onVideoUnavailable(String inputId, int reason) {
524            mDelegate.onVideoUnavailable(inputId, reason);
525        }
526
527        @Override
528        public void onContentAllowed(String inputId) {
529            mDelegate.onContentAllowed(inputId);
530        }
531
532        @Override
533        public void onContentBlocked(String inputId, TvContentRating rating) {
534            mDelegate.onContentBlocked(inputId, rating);
535        }
536
537        @Override
538        public void onTimeShiftStatusChanged(String inputId, int status) {
539            mDelegate.onTimeShiftStatusChanged(inputId, status);
540        }
541    }
542
543    /**
544     * Called when the {@link TvView} channel is changed.
545     */
546    public interface OnTvViewChannelChangeListener {
547        void onTvViewChannelChange(@Nullable Uri channelUri);
548    }
549}
550