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