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