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.common.SoftPreconditions;
41 import com.android.tv.data.Channel;
42 import com.android.tv.ui.TunableTvView;
43 import com.android.tv.ui.TunableTvView.OnTuneListener;
44 import com.android.tv.util.TvInputManagerHelper;
45 
46 import java.util.Collections;
47 import java.util.List;
48 import java.util.Objects;
49 import 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)
64 public 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 
InputSessionManager(Context context)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
createTvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback)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
releaseTvViewSession(TvViewSession session)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
createRecordingSession(String inputId, String tag, RecordingCallback callback, Handler handler, long endTimeMs)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      */
releaseRecordingSession(RecordingSession session)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
addOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener)132     public void addOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) {
133         mOnTvViewChannelChangeListeners.add(listener);
134     }
135 
136     /**
137      * Removes the {@link OnTvViewChannelChangeListener}.
138      */
139     @MainThread
removeOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener)140     public void removeOnTvViewChannelChangeListener(OnTvViewChannelChangeListener listener) {
141         mOnTvViewChannelChangeListeners.remove(listener);
142     }
143 
144     @MainThread
notifyTvViewChannelChange(Uri channelUri)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
getCurrentTvViewChannelUri()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
getEarliestRecordingSessionEndTimeMs(String inputId)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
getTunedTvViewSessionCount(String inputId)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
isTunedForTvView(Uri channelUri)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 
getTunedRecordingSessionCount(String inputId)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 
isTunedForRecording(Uri channelUri)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 
TvViewSession(TvView tvView, TunableTvView tunableTvView, TvInputCallback callback)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          */
tune(String inputId, Uri channelUri)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          */
tune(Channel channel, Bundle params, OnTuneListener listener)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 
retune()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          */
timeShiftPlay(String inputId, Uri recordedProgramUri)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          */
reset()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 
resetByRecording()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 
RecordingSession(String inputId, String tag, RecordingCallback callback, Handler handler, long endTimeMs)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 
release()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          */
tune(String inputId, Uri channelUri)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          */
startRecording(Uri programHintUri)453         public void startRecording(Uri programHintUri) {
454             mClient.startRecording(programHintUri);
455         }
456 
457         /**
458          * Stops recording.
459          */
stopRecording()460         public void stopRecording() {
461             mClient.stopRecording();
462         }
463 
464         /**
465          * Sets recording session's ending time.
466          */
setEndTimeMs(long endTimeMs)467         public void setEndTimeMs(long endTimeMs) {
468             mEndTimeMs = endTimeMs;
469         }
470 
runOnHandler(Handler handler, Runnable runnable)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 
DelegateTvInputCallback(TvInputCallback delegate)483         DelegateTvInputCallback(TvInputCallback delegate) {
484             mDelegate = delegate;
485         }
486 
487         @Override
onConnectionFailed(String inputId)488         public void onConnectionFailed(String inputId) {
489             mDelegate.onConnectionFailed(inputId);
490         }
491 
492         @Override
onDisconnected(String inputId)493         public void onDisconnected(String inputId) {
494             mDelegate.onDisconnected(inputId);
495         }
496 
497         @Override
onChannelRetuned(String inputId, Uri channelUri)498         public void onChannelRetuned(String inputId, Uri channelUri) {
499             mDelegate.onChannelRetuned(inputId, channelUri);
500         }
501 
502         @Override
onTracksChanged(String inputId, List<TvTrackInfo> tracks)503         public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
504             mDelegate.onTracksChanged(inputId, tracks);
505         }
506 
507         @Override
onTrackSelected(String inputId, int type, String trackId)508         public void onTrackSelected(String inputId, int type, String trackId) {
509             mDelegate.onTrackSelected(inputId, type, trackId);
510         }
511 
512         @Override
onVideoSizeChanged(String inputId, int width, int height)513         public void onVideoSizeChanged(String inputId, int width, int height) {
514             mDelegate.onVideoSizeChanged(inputId, width, height);
515         }
516 
517         @Override
onVideoAvailable(String inputId)518         public void onVideoAvailable(String inputId) {
519             mDelegate.onVideoAvailable(inputId);
520         }
521 
522         @Override
onVideoUnavailable(String inputId, int reason)523         public void onVideoUnavailable(String inputId, int reason) {
524             mDelegate.onVideoUnavailable(inputId, reason);
525         }
526 
527         @Override
onContentAllowed(String inputId)528         public void onContentAllowed(String inputId) {
529             mDelegate.onContentAllowed(inputId);
530         }
531 
532         @Override
onContentBlocked(String inputId, TvContentRating rating)533         public void onContentBlocked(String inputId, TvContentRating rating) {
534             mDelegate.onContentBlocked(inputId, rating);
535         }
536 
537         @Override
onTimeShiftStatusChanged(String inputId, int status)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 {
onTvViewChannelChange(@ullable Uri channelUri)547         void onTvViewChannelChange(@Nullable Uri channelUri);
548     }
549 }
550