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