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