• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.tv;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.media.tv.TvContentRating;
22 import android.media.tv.TvInputInfo;
23 import android.media.tv.TvRecordingClient;
24 import android.media.tv.TvRecordingClient.RecordingCallback;
25 import android.media.tv.TvTrackInfo;
26 import android.media.tv.TvView;
27 import android.media.tv.TvView.TvInputCallback;
28 import android.net.Uri;
29 import android.os.Build;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Looper;
33 import android.support.annotation.MainThread;
34 import android.support.annotation.NonNull;
35 import android.support.annotation.Nullable;
36 import android.text.TextUtils;
37 import android.util.ArraySet;
38 import android.util.Log;
39 
40 import com.android.tv.data.Channel;
41 import com.android.tv.ui.TunableTvView;
42 import com.android.tv.ui.TunableTvView.OnTuneListener;
43 import com.android.tv.util.TvInputManagerHelper;
44 
45 import java.util.Collections;
46 import java.util.List;
47 import java.util.Objects;
48 import 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)
63 public 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 
InputSessionManager(Context context)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
createTvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback)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
releaseTvViewSession(TvViewSession session)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
createRecordingSession(String inputId, String tag, RecordingCallback callback, Handler handler, long endTimeMs)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      */
releaseRecordingSession(RecordingSession session)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
addOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener)139     public void addOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) {
140         mOnTvViewChannelChangeListeners.add(listener);
141     }
142 
143     /**
144      * Removes the {@link OnTvViewChannelChangeListener}.
145      */
146     @MainThread
removeOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener)147     public void removeOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) {
148         mOnTvViewChannelChangeListeners.remove(listener);
149     }
150 
151     @MainThread
notifyTvViewChannelChange(Uri channelUri)152     void notifyTvViewChannelChange(Uri channelUri) {
153         for (OnTvViewChannelChangeListener l : mOnTvViewChannelChangeListeners) {
154             l.onTvViewChannelChange(channelUri);
155         }
156     }
157 
158     /** Adds the {@link OnRecordingSessionChangeListener}. */
addOnRecordingSessionChangeListener(OnRecordingSessionChangeListener listener)159     public void addOnRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) {
160         mOnRecordingSessionChangeListeners.add(listener);
161     }
162 
163     /** Removes the {@link OnRecordingSessionChangeListener}. */
removeRecordingSessionChangeListener(OnRecordingSessionChangeListener listener)164     public void removeRecordingSessionChangeListener(OnRecordingSessionChangeListener listener) {
165         mOnRecordingSessionChangeListeners.remove(listener);
166     }
167 
168     /** Returns the current {@link TvView} channel. */
169     @MainThread
getCurrentTvViewChannelUri()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
getEarliestRecordingSessionEndTimeMs(String inputId)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
getTunedTvViewSessionCount(String inputId)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
isTunedForTvView(Uri channelUri)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 
getTunedRecordingSessionCount(String inputId)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 
isTunedForRecording(Uri channelUri)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 
TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback)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: connection 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          */
tune(String inputId, Uri channelUri)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          */
tune(Channel channel, Bundle params, OnTuneListener listener)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 
retune()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          */
timeShiftPlay(String inputId, Uri recordedProgramUri)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          */
reset()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 
resetByRecording()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 
RecordingSession(String inputId, String tag, RecordingCallback callback, Handler handler, long endTimeMs)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 
release()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          */
tune(String inputId, Uri channelUri)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          */
startRecording(Uri programHintUri)468         public void startRecording(Uri programHintUri) {
469             mClient.startRecording(programHintUri);
470         }
471 
472         /**
473          * Stops recording.
474          */
stopRecording()475         public void stopRecording() {
476             mClient.stopRecording();
477         }
478 
479         /**
480          * Sets recording session's ending time.
481          */
setEndTimeMs(long endTimeMs)482         public void setEndTimeMs(long endTimeMs) {
483             mEndTimeMs = endTimeMs;
484         }
485 
runOnHandler(Handler handler, Runnable runnable)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 
DelegateTvInputCallback(TvInputCallback delegate)498         DelegateTvInputCallback(TvInputCallback delegate) {
499             mDelegate = delegate;
500         }
501 
502         @Override
onConnectionFailed(String inputId)503         public void onConnectionFailed(String inputId) {
504             mDelegate.onConnectionFailed(inputId);
505         }
506 
507         @Override
onDisconnected(String inputId)508         public void onDisconnected(String inputId) {
509             mDelegate.onDisconnected(inputId);
510         }
511 
512         @Override
onChannelRetuned(String inputId, Uri channelUri)513         public void onChannelRetuned(String inputId, Uri channelUri) {
514             mDelegate.onChannelRetuned(inputId, channelUri);
515         }
516 
517         @Override
onTracksChanged(String inputId, List<TvTrackInfo> tracks)518         public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
519             mDelegate.onTracksChanged(inputId, tracks);
520         }
521 
522         @Override
onTrackSelected(String inputId, int type, String trackId)523         public void onTrackSelected(String inputId, int type, String trackId) {
524             mDelegate.onTrackSelected(inputId, type, trackId);
525         }
526 
527         @Override
onVideoSizeChanged(String inputId, int width, int height)528         public void onVideoSizeChanged(String inputId, int width, int height) {
529             mDelegate.onVideoSizeChanged(inputId, width, height);
530         }
531 
532         @Override
onVideoAvailable(String inputId)533         public void onVideoAvailable(String inputId) {
534             mDelegate.onVideoAvailable(inputId);
535         }
536 
537         @Override
onVideoUnavailable(String inputId, int reason)538         public void onVideoUnavailable(String inputId, int reason) {
539             mDelegate.onVideoUnavailable(inputId, reason);
540         }
541 
542         @Override
onContentAllowed(String inputId)543         public void onContentAllowed(String inputId) {
544             mDelegate.onContentAllowed(inputId);
545         }
546 
547         @Override
onContentBlocked(String inputId, TvContentRating rating)548         public void onContentBlocked(String inputId, TvContentRating rating) {
549             mDelegate.onContentBlocked(inputId, rating);
550         }
551 
552         @Override
onTimeShiftStatusChanged(String inputId, int status)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 {
onTvViewChannelChange(@ullable Uri channelUri)562         void onTvViewChannelChange(@Nullable Uri channelUri);
563     }
564 
565     /** Called when recording session is created or destroyed. */
566     public interface OnRecordingSessionChangeListener {
onRecordingSessionChange(boolean create, int count)567         void onRecordingSessionChange(boolean create, int count);
568     }
569 }
570