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