Home | History | Annotate | Download | only in tvinput
      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.tuner.tvinput;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.ContentUris;
     21 import android.content.Context;
     22 import android.database.Cursor;
     23 import android.media.MediaFormat;
     24 import android.media.PlaybackParams;
     25 import android.media.tv.TvContentRating;
     26 import android.media.tv.TvContract;
     27 import android.media.tv.TvInputManager;
     28 import android.media.tv.TvTrackInfo;
     29 import android.net.Uri;
     30 import android.os.Handler;
     31 import android.os.HandlerThread;
     32 import android.os.Message;
     33 import android.os.SystemClock;
     34 import android.support.annotation.AnyThread;
     35 import android.support.annotation.MainThread;
     36 import android.support.annotation.WorkerThread;
     37 import android.text.Html;
     38 import android.util.Log;
     39 import android.util.Pair;
     40 import android.util.SparseArray;
     41 import android.view.Surface;
     42 import android.view.accessibility.CaptioningManager;
     43 
     44 import com.google.android.exoplayer.audio.AudioCapabilities;
     45 import com.google.android.exoplayer.ExoPlayer;
     46 import com.android.tv.common.SoftPreconditions;
     47 import com.android.tv.common.TvContentRatingCache;
     48 import com.android.tv.tuner.TunerPreferences;
     49 import com.android.tv.tuner.data.Cea708Data;
     50 import com.android.tv.tuner.data.PsipData.EitItem;
     51 import com.android.tv.tuner.data.PsipData.TvTracksInterface;
     52 import com.android.tv.tuner.data.TunerChannel;
     53 import com.android.tv.tuner.data.nano.Channel;
     54 import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
     55 import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
     56 import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
     57 import com.android.tv.tuner.exoplayer.buffer.BufferManager;
     58 import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
     59 import com.android.tv.tuner.exoplayer.MpegTsPlayer;
     60 import com.android.tv.tuner.source.TsDataSource;
     61 import com.android.tv.tuner.source.TsDataSourceManager;
     62 import com.android.tv.tuner.util.StatusTextUtils;
     63 
     64 import java.io.File;
     65 import java.io.FileNotFoundException;
     66 import java.io.IOException;
     67 import java.util.ArrayList;
     68 import java.util.Iterator;
     69 import java.util.List;
     70 import java.util.Objects;
     71 import java.util.concurrent.CountDownLatch;
     72 
     73 /**
     74  * {@link TunerSessionWorker} implements a handler thread which processes TV input jobs
     75  * such as handling {@link ExoPlayer}, managing a tuner device, trickplay, and so on.
     76  */
     77 @WorkerThread
     78 public class TunerSessionWorker implements PlaybackBufferListener,
     79         MpegTsPlayer.VideoEventListener, MpegTsPlayer.Listener, EventDetector.EventListener,
     80         ChannelDataManager.ProgramInfoListener, Handler.Callback {
     81     private static final String TAG = "TunerSessionWorker";
     82     private static final boolean DEBUG = false;
     83     private static final boolean ENABLE_PROFILER = true;
     84     private static final String PLAY_FROM_CHANNEL = "channel";
     85 
     86     // Public messages
     87     public static final int MSG_SELECT_TRACK = 1;
     88     public static final int MSG_UPDATE_CAPTION_TRACK = 2;
     89     public static final int MSG_SET_STREAM_VOLUME = 3;
     90     public static final int MSG_TIMESHIFT_PAUSE = 4;
     91     public static final int MSG_TIMESHIFT_RESUME = 5;
     92     public static final int MSG_TIMESHIFT_SEEK_TO = 6;
     93     public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7;
     94     public static final int MSG_AUDIO_CAPABILITIES_CHANGED = 8;
     95     public static final int MSG_UNBLOCKED_RATING = 9;
     96 
     97     // Private messages
     98     private static final int MSG_TUNE = 1000;
     99     private static final int MSG_RELEASE = 1001;
    100     private static final int MSG_RETRY_PLAYBACK = 1002;
    101     private static final int MSG_START_PLAYBACK = 1003;
    102     private static final int MSG_UPDATE_PROGRAM = 1008;
    103     private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009;
    104     private static final int MSG_UPDATE_CHANNEL_INFO = 1010;
    105     private static final int MSG_TRICKPLAY_BY_SEEK = 1011;
    106     private static final int MSG_SMOOTH_TRICKPLAY_MONITOR = 1012;
    107     private static final int MSG_PARENTAL_CONTROLS = 1015;
    108     private static final int MSG_RESCHEDULE_PROGRAMS = 1016;
    109     private static final int MSG_BUFFER_START_TIME_CHANGED = 1017;
    110     private static final int MSG_CHECK_SIGNAL = 1018;
    111     private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019;
    112     private static final int MSG_RESET_PLAYBACK = 1020;
    113     private static final int MSG_BUFFER_STATE_CHANGED = 1021;
    114     private static final int MSG_PROGRAM_DATA_RESULT = 1022;
    115     private static final int MSG_STOP_TUNE = 1023;
    116     private static final int MSG_SET_SURFACE = 1024;
    117     private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025;
    118 
    119     private static final int TS_PACKET_SIZE = 188;
    120     private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000;
    121     private static final int CHECK_NO_SIGNAL_PERIOD_MS = 500;
    122     private static final int RECOVER_STOPPED_PLAYBACK_PERIOD_MS = 2500;
    123     private static final int PARENTAL_CONTROLS_INTERVAL_MS = 5000;
    124     private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000;
    125     private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000;
    126     private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000;
    127     // The following 3s is defined empirically. This should be larger than 2s considering video
    128     // key frame interval in the TS stream.
    129     private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000;
    130     private static final int PLAYBACK_RETRY_DELAY_MS = 5000;
    131     private static final int MAX_IMMEDIATE_RETRY_COUNT = 5;
    132     private static final long INVALID_TIME = -1;
    133 
    134     // Some examples of the track ids of the audio tracks, "a0", "a1", "a2".
    135     // The number after prefix is being used for indicating a index of the given audio track.
    136     private static final String AUDIO_TRACK_PREFIX = "a";
    137 
    138     // Some examples of the tracks id of the caption tracks, "s1", "s2", "s3".
    139     // The number after prefix is being used for indicating a index of a caption service number
    140     // of the given caption track.
    141     private static final String SUBTITLE_TRACK_PREFIX = "s";
    142     private static final int TRACK_PREFIX_SIZE = 1;
    143     private static final String VIDEO_TRACK_ID = "v";
    144     private static final long BUFFER_UNDERFLOW_BUFFER_MS = 5000;
    145 
    146     // Actual interval would be divided by the speed.
    147     private static final int EXPECTED_KEY_FRAME_INTERVAL_MS = 500;
    148     private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 20;
    149     private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250;
    150 
    151     private final Context mContext;
    152     private final ChannelDataManager mChannelDataManager;
    153     private final TsDataSourceManager mSourceManager;
    154     private volatile Surface mSurface;
    155     private volatile float mVolume = 1.0f;
    156     private volatile boolean mCaptionEnabled;
    157     private volatile MpegTsPlayer mPlayer;
    158     private volatile TunerChannel mChannel;
    159     private volatile Long mRecordingDuration;
    160     private volatile long mRecordStartTimeMs;
    161     private volatile long mBufferStartTimeMs;
    162     private String mRecordingId;
    163     private final Handler mHandler;
    164     private int mRetryCount;
    165     private final ArrayList<TvTrackInfo> mTvTracks;
    166     private final SparseArray<AtscAudioTrack> mAudioTrackMap;
    167     private final SparseArray<AtscCaptionTrack> mCaptionTrackMap;
    168     private AtscCaptionTrack mCaptionTrack;
    169     private PlaybackParams mPlaybackParams = new PlaybackParams();
    170     private boolean mPlayerStarted = false;
    171     private boolean mReportedDrawnToSurface = false;
    172     private boolean mReportedWeakSignal = false;
    173     private EitItem mProgram;
    174     private List<EitItem> mPrograms;
    175     private final TvInputManager mTvInputManager;
    176     private boolean mChannelBlocked;
    177     private TvContentRating mUnblockedContentRating;
    178     private long mLastPositionMs;
    179     private AudioCapabilities mAudioCapabilities;
    180     private final CountDownLatch mReleaseLatch = new CountDownLatch(1);
    181     private long mLastLimitInBytes;
    182     private long mLastPositionInBytes;
    183     private final BufferManager mBufferManager;
    184     private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
    185     private final TunerSession mSession;
    186     private int mPlayerState = ExoPlayer.STATE_IDLE;
    187     private long mPreparingStartTimeMs;
    188     private long mBufferingStartTimeMs;
    189     private long mReadyStartTimeMs;
    190 
    191     public TunerSessionWorker(Context context, ChannelDataManager channelDataManager,
    192                 BufferManager bufferManager, TunerSession tunerSession) {
    193         if (DEBUG) Log.d(TAG, "TunerSessionWorker created");
    194         mContext = context;
    195 
    196         // HandlerThread should be set up before it is registered as a listener in the all other
    197         // components.
    198         HandlerThread handlerThread = new HandlerThread(TAG);
    199         handlerThread.start();
    200         mHandler = new Handler(handlerThread.getLooper(), this);
    201         mSession = tunerSession;
    202         mChannelDataManager = channelDataManager;
    203         mChannelDataManager.setListener(this);
    204         mChannelDataManager.checkDataVersion(mContext);
    205         mSourceManager = TsDataSourceManager.createSourceManager(false);
    206         mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE);
    207         mTvTracks = new ArrayList<>();
    208         mAudioTrackMap = new SparseArray<>();
    209         mCaptionTrackMap = new SparseArray<>();
    210         CaptioningManager captioningManager =
    211                 (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
    212         mCaptionEnabled = captioningManager.isEnabled();
    213         mPlaybackParams.setSpeed(1.0f);
    214         mBufferManager = bufferManager;
    215         mPreparingStartTimeMs = INVALID_TIME;
    216         mBufferingStartTimeMs = INVALID_TIME;
    217         mReadyStartTimeMs = INVALID_TIME;
    218     }
    219 
    220     // Public methods
    221     @MainThread
    222     public void tune(Uri channelUri) {
    223         mHandler.removeCallbacksAndMessages(null);
    224         mSourceManager.setHasPendingTune();
    225         sendMessage(MSG_TUNE, channelUri);
    226     }
    227 
    228     @MainThread
    229     public void stopTune() {
    230         mHandler.removeCallbacksAndMessages(null);
    231         sendMessage(MSG_STOP_TUNE);
    232     }
    233 
    234     /**
    235      * Sets {@link Surface}.
    236      */
    237     @MainThread
    238     public void setSurface(Surface surface) {
    239         if (surface != null && !surface.isValid()) {
    240             Log.w(TAG, "Ignoring invalid surface.");
    241             return;
    242         }
    243         // mSurface is kept even when tune is called right after. But, messages can be deleted by
    244         // tune or updateChannelBlockStatus. So mSurface should be stored here, not through message.
    245         mSurface = surface;
    246         mHandler.sendEmptyMessage(MSG_SET_SURFACE);
    247     }
    248 
    249     /**
    250      * Sets volume.
    251      */
    252     @MainThread
    253     public void setStreamVolume(float volume) {
    254         // mVolume is kept even when tune is called right after. But, messages can be deleted by
    255         // tune or updateChannelBlockStatus. So mVolume is stored here and mPlayer.setVolume will be
    256         // called in MSG_SET_STREAM_VOLUME.
    257         mVolume = volume;
    258         mHandler.sendEmptyMessage(MSG_SET_STREAM_VOLUME);
    259     }
    260 
    261     /**
    262      * Sets if caption is enabled or disabled.
    263      */
    264     @MainThread
    265     public void setCaptionEnabled(boolean captionEnabled) {
    266         // mCaptionEnabled is kept even when tune is called right after. But, messages can be
    267         // deleted by tune or updateChannelBlockStatus. So mCaptionEnabled is stored here and
    268         // start/stopCaptionTrack will be called in MSG_UPDATE_CAPTION_STATUS.
    269         mCaptionEnabled = captionEnabled;
    270         mHandler.sendEmptyMessage(MSG_UPDATE_CAPTION_TRACK);
    271     }
    272 
    273     public TunerChannel getCurrentChannel() {
    274         return mChannel;
    275     }
    276 
    277     @MainThread
    278     public long getStartPosition() {
    279         return mBufferStartTimeMs;
    280     }
    281 
    282 
    283     private String getRecordingPath() {
    284         return Uri.parse(mRecordingId).getPath();
    285     }
    286 
    287     private Long getDurationForRecording(String recordingId) {
    288         try {
    289             DvrStorageManager storageManager =
    290                     new DvrStorageManager(new File(getRecordingPath()), false);
    291             Pair<String, MediaFormat> trackInfo = null;
    292             try {
    293                 trackInfo = storageManager.readTrackInfoFile(false);
    294             } catch (FileNotFoundException e) {
    295             }
    296             if (trackInfo == null) {
    297                 trackInfo = storageManager.readTrackInfoFile(true);
    298             }
    299             Long durationUs = trackInfo.second.getLong(MediaFormat.KEY_DURATION);
    300             // we need duration by milli for trickplay notification.
    301             return durationUs != null ? durationUs / 1000 : null;
    302         } catch (IOException e) {
    303             Log.e(TAG, "meta file for recording was not found: " + recordingId);
    304             return null;
    305         }
    306     }
    307 
    308     @MainThread
    309     public long getCurrentPosition() {
    310         // TODO: More precise time may be necessary.
    311         MpegTsPlayer mpegTsPlayer = mPlayer;
    312         long currentTime = mpegTsPlayer != null
    313                 ? mRecordStartTimeMs + mpegTsPlayer.getCurrentPosition() : mRecordStartTimeMs;
    314         if (mChannel == null && mPlayerState == ExoPlayer.STATE_ENDED) {
    315             currentTime = mRecordingDuration + mRecordStartTimeMs;
    316         }
    317         if (DEBUG) {
    318             long systemCurrentTime = System.currentTimeMillis();
    319             Log.d(TAG, "currentTime = " + currentTime
    320                     + " ; System.currentTimeMillis() = " + systemCurrentTime
    321                     + " ; diff = " + (currentTime - systemCurrentTime));
    322         }
    323         return currentTime;
    324     }
    325 
    326     @AnyThread
    327     public void sendMessage(int messageType) {
    328         mHandler.sendEmptyMessage(messageType);
    329     }
    330 
    331     @AnyThread
    332     public void sendMessage(int messageType, Object object) {
    333         mHandler.obtainMessage(messageType, object).sendToTarget();
    334     }
    335 
    336     @AnyThread
    337     public void sendMessage(int messageType, int arg1, int arg2, Object object) {
    338         mHandler.obtainMessage(messageType, arg1, arg2, object).sendToTarget();
    339     }
    340 
    341     @MainThread
    342     public void release() {
    343         if (DEBUG) Log.d(TAG, "release()");
    344         mChannelDataManager.setListener(null);
    345         mHandler.removeCallbacksAndMessages(null);
    346         mHandler.sendEmptyMessage(MSG_RELEASE);
    347         try {
    348             mReleaseLatch.await();
    349         } catch (InterruptedException e) {
    350             Log.e(TAG, "Couldn't wait for finish of MSG_RELEASE", e);
    351         } finally {
    352             mHandler.getLooper().quitSafely();
    353         }
    354     }
    355 
    356     // MpegTsPlayer.Listener
    357     // Called in the same thread as mHandler.
    358     @Override
    359     public void onStateChanged(boolean playWhenReady, int playbackState) {
    360         if (DEBUG) Log.d(TAG, "ExoPlayer state change: " + playbackState + " " + playWhenReady);
    361         if (playbackState == mPlayerState) {
    362             return;
    363         }
    364         mReadyStartTimeMs = INVALID_TIME;
    365         mPreparingStartTimeMs = INVALID_TIME;
    366         mBufferingStartTimeMs = INVALID_TIME;
    367         if (playbackState == ExoPlayer.STATE_READY) {
    368             if (DEBUG) Log.d(TAG, "ExoPlayer ready");
    369             if (!mPlayerStarted) {
    370                 sendMessage(MSG_START_PLAYBACK, mPlayer);
    371             }
    372             mReadyStartTimeMs = SystemClock.elapsedRealtime();
    373         } else if (playbackState == ExoPlayer.STATE_PREPARING) {
    374             mPreparingStartTimeMs = SystemClock.elapsedRealtime();
    375         } else if (playbackState == ExoPlayer.STATE_BUFFERING) {
    376             mBufferingStartTimeMs = SystemClock.elapsedRealtime();
    377         } else if (playbackState == ExoPlayer.STATE_ENDED) {
    378             // Final status
    379             // notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards.
    380             Log.i(TAG, "Player ended: end of stream");
    381             if (mChannel != null) {
    382                 sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
    383             }
    384         }
    385         mPlayerState = playbackState;
    386     }
    387 
    388     @Override
    389     public void onError(Exception e) {
    390         if (TunerPreferences.getStoreTsStream(mContext)) {
    391             // Crash intentionally to capture the error causing TS file.
    392             Log.e(TAG, "Crash intentionally to capture the error causing TS file. "
    393                     + e.getMessage());
    394             SoftPreconditions.checkState(false);
    395         }
    396         // There maybe some errors that finally raise ExoPlaybackException and will be handled here.
    397         // If we are playing live stream, retrying playback maybe helpful. But for recorded stream,
    398         // retrying playback is not helpful.
    399         if (mChannel != null) {
    400             mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer).sendToTarget();
    401         }
    402     }
    403 
    404     @Override
    405     public void onVideoSizeChanged(int width, int height, float pixelWidthHeight) {
    406         if (mChannel != null && mChannel.hasVideo()) {
    407             updateVideoTrack(width, height);
    408         }
    409         if (mRecordingId != null) {
    410             updateVideoTrack(width, height);
    411         }
    412     }
    413 
    414     @Override
    415     public void onDrawnToSurface(MpegTsPlayer player, Surface surface) {
    416         if (mSurface != null && mPlayerStarted) {
    417             if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE");
    418             mBufferStartTimeMs = mRecordStartTimeMs =
    419                     (mRecordingId != null) ? 0 : System.currentTimeMillis();
    420             notifyVideoAvailable();
    421             mReportedDrawnToSurface = true;
    422 
    423             // If surface is drawn successfully, it means that the playback was brought back
    424             // to normal and therefore, the playback recovery status will be reset through
    425             // setting a zero value to the retry count.
    426             // TODO: Consider audio only channels for detecting playback status changes to
    427             //       be normal.
    428             mRetryCount = 0;
    429             if (mCaptionEnabled && mCaptionTrack != null) {
    430                 startCaptionTrack();
    431             } else {
    432                 stopCaptionTrack();
    433             }
    434             mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
    435         }
    436     }
    437 
    438     @Override
    439     public void onSmoothTrickplayForceStopped() {
    440         if (mPlayer == null || !mHandler.hasMessages(MSG_SMOOTH_TRICKPLAY_MONITOR)) {
    441             return;
    442         }
    443         mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
    444         doTrickplayBySeek((int) mPlayer.getCurrentPosition());
    445     }
    446 
    447     @Override
    448     public void onAudioUnplayable() {
    449         if (mPlayer == null) {
    450             return;
    451         }
    452         Log.i(TAG, "AC3 audio cannot be played due to device limitation");
    453         mSession.sendUiMessage(
    454                 TunerSession.MSG_UI_SHOW_AUDIO_UNPLAYABLE);
    455     }
    456 
    457     // MpegTsPlayer.VideoEventListener
    458     @Override
    459     public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) {
    460         mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event);
    461     }
    462 
    463     @Override
    464     public void onDiscoverCaptionServiceNumber(int serviceNumber) {
    465         sendMessage(MSG_DISCOVER_CAPTION_SERVICE_NUMBER, serviceNumber);
    466     }
    467 
    468     // ChannelDataManager.ProgramInfoListener
    469     @Override
    470     public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) {
    471         sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs));
    472     }
    473 
    474     @Override
    475     public void onChannelArrived(TunerChannel channel) {
    476         sendMessage(MSG_UPDATE_CHANNEL_INFO, channel);
    477     }
    478 
    479     @Override
    480     public void onRescanNeeded() {
    481         mSession.sendUiMessage(TunerSession.MSG_UI_TOAST_RESCAN_NEEDED);
    482     }
    483 
    484     @Override
    485     public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) {
    486         sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs));
    487     }
    488 
    489     // PlaybackBufferListener
    490     @Override
    491     public void onBufferStartTimeChanged(long startTimeMs) {
    492         sendMessage(MSG_BUFFER_START_TIME_CHANGED, startTimeMs);
    493     }
    494 
    495     @Override
    496     public void onBufferStateChanged(boolean available) {
    497         sendMessage(MSG_BUFFER_STATE_CHANGED, available);
    498     }
    499 
    500     @Override
    501     public void onDiskTooSlow() {
    502         sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
    503     }
    504 
    505     // EventDetector.EventListener
    506     @Override
    507     public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
    508         mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
    509     }
    510 
    511     @Override
    512     public void onEventDetected(TunerChannel channel, List<EitItem> items) {
    513         mChannelDataManager.notifyEventDetected(channel, items);
    514     }
    515 
    516     @Override
    517     public void onChannelScanDone() {
    518         // do nothing.
    519     }
    520 
    521     private long parseChannel(Uri uri) {
    522         try {
    523             List<String> paths = uri.getPathSegments();
    524             if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) {
    525                 return ContentUris.parseId(uri);
    526             }
    527         } catch (UnsupportedOperationException | NumberFormatException e) {
    528         }
    529         return -1;
    530     }
    531 
    532     private static class RecordedProgram {
    533         private final long mChannelId;
    534         private final String mDataUri;
    535 
    536         private static final String[] PROJECTION = {
    537             TvContract.Programs.COLUMN_CHANNEL_ID,
    538             TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI,
    539         };
    540 
    541         public RecordedProgram(Cursor cursor) {
    542             int index = 0;
    543             mChannelId = cursor.getLong(index++);
    544             mDataUri = cursor.getString(index++);
    545         }
    546 
    547         public RecordedProgram(long channelId, String dataUri) {
    548             mChannelId = channelId;
    549             mDataUri = dataUri;
    550         }
    551 
    552         public static RecordedProgram onQuery(Cursor c) {
    553             RecordedProgram recording = null;
    554             if (c != null && c.moveToNext()) {
    555                 recording = new RecordedProgram(c);
    556             }
    557             return recording;
    558         }
    559 
    560         public String getDataUri() {
    561             return mDataUri;
    562         }
    563     }
    564 
    565     private RecordedProgram getRecordedProgram(Uri recordedUri) {
    566         ContentResolver resolver = mContext.getContentResolver();
    567         try(Cursor c = resolver.query(recordedUri, RecordedProgram.PROJECTION, null, null, null)) {
    568             if (c != null) {
    569                 RecordedProgram result = RecordedProgram.onQuery(c);
    570                 if (DEBUG) {
    571                     Log.d(TAG, "Finished query for " + this);
    572                 }
    573                 return result;
    574             } else {
    575                 if (c == null) {
    576                     Log.e(TAG, "Unknown query error for " + this);
    577                 } else {
    578                     if (DEBUG) Log.d(TAG, "Canceled query for " + this);
    579                 }
    580                 return null;
    581             }
    582         }
    583     }
    584 
    585     private String parseRecording(Uri uri) {
    586         RecordedProgram recording = getRecordedProgram(uri);
    587         if (recording != null) {
    588             return recording.getDataUri();
    589         }
    590         return null;
    591     }
    592 
    593     @Override
    594     public boolean handleMessage(Message msg) {
    595         switch (msg.what) {
    596             case MSG_TUNE: {
    597                 if (DEBUG) Log.d(TAG, "MSG_TUNE");
    598 
    599                 // When sequential tuning messages arrived, it skips middle tuning messages in order
    600                 // to change to the last requested channel quickly.
    601                 if (mHandler.hasMessages(MSG_TUNE)) {
    602                     return true;
    603                 }
    604                 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
    605                 Uri channelUri = (Uri) msg.obj;
    606                 String recording = null;
    607                 long channelId = parseChannel(channelUri);
    608                 TunerChannel channel = (channelId == -1) ? null
    609                         : mChannelDataManager.getChannel(channelId);
    610                 if (channelId == -1) {
    611                     recording = parseRecording(channelUri);
    612                 }
    613                 if (channel == null && recording == null) {
    614                     Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri);
    615                     stopTune();
    616                     notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
    617                     return true;
    618                 }
    619                 mHandler.removeCallbacksAndMessages(null);
    620                 if (channel != null) {
    621                     mChannelDataManager.requestProgramsData(channel);
    622                 }
    623                 prepareTune(channel, recording);
    624                 // TODO: Need to refactor. notifyContentAllowed() should not be called if parental
    625                 // control is turned on.
    626                 mSession.notifyContentAllowed();
    627                 resetPlayback();
    628                 resetTvTracks();
    629                 mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
    630                         RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
    631                 return true;
    632             }
    633             case MSG_STOP_TUNE: {
    634                 if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE");
    635                 mChannel = null;
    636                 stopPlayback();
    637                 stopCaptionTrack();
    638                 resetTvTracks();
    639                 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
    640                 return true;
    641             }
    642             case MSG_RELEASE: {
    643                 if (DEBUG) Log.d(TAG, "MSG_RELEASE");
    644                 mHandler.removeCallbacksAndMessages(null);
    645                 stopPlayback();
    646                 stopCaptionTrack();
    647                 mSourceManager.release();
    648                 mReleaseLatch.countDown();
    649                 return true;
    650             }
    651             case MSG_RETRY_PLAYBACK: {
    652                 if (mPlayer == msg.obj) {
    653                     Log.i(TAG, "Retrying the playback for channel: " + mChannel);
    654                     mHandler.removeMessages(MSG_RETRY_PLAYBACK);
    655                     // When there is a request of retrying playback, don't reuse TunerHal.
    656                     mSourceManager.setKeepTuneStatus(false);
    657                     mRetryCount++;
    658                     if (DEBUG) {
    659                         Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
    660                     }
    661                     if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
    662                         resetPlayback();
    663                     } else {
    664                         // When it reaches this point, it may be due to an error that occurred in
    665                         // the tuner device. Calling stopPlayback() resets the tuner device
    666                         // to recover from the error.
    667                         stopPlayback();
    668                         stopCaptionTrack();
    669 
    670                         notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
    671 
    672                         // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically chosen
    673                         // value before recovering the playback.
    674                         mHandler.sendEmptyMessageDelayed(MSG_RESET_PLAYBACK,
    675                                 RECOVER_STOPPED_PLAYBACK_PERIOD_MS);
    676                     }
    677                 }
    678                 return true;
    679             }
    680             case MSG_RESET_PLAYBACK: {
    681                 if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK");
    682                 resetPlayback();
    683                 return true;
    684             }
    685             case MSG_START_PLAYBACK: {
    686                 if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK");
    687                 if (mChannel != null || mRecordingId != null) {
    688                     startPlayback(msg.obj);
    689                 }
    690                 return true;
    691             }
    692             case MSG_UPDATE_PROGRAM: {
    693                 if (mChannel != null) {
    694                     EitItem program = (EitItem) msg.obj;
    695                     updateTvTracks(program, false);
    696                     mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
    697                 }
    698                 return true;
    699             }
    700             case MSG_SCHEDULE_OF_PROGRAMS: {
    701                 mHandler.removeMessages(MSG_UPDATE_PROGRAM);
    702                 Pair<TunerChannel, List<EitItem>> pair =
    703                         (Pair<TunerChannel, List<EitItem>>) msg.obj;
    704                 TunerChannel channel = pair.first;
    705                 if (mChannel == null) {
    706                     return true;
    707                 }
    708                 if (mChannel != null && mChannel.compareTo(channel) != 0) {
    709                     return true;
    710                 }
    711                 mPrograms = pair.second;
    712                 EitItem currentProgram = getCurrentProgram();
    713                 if (currentProgram == null) {
    714                     mProgram = null;
    715                 }
    716                 long currentTimeMs = getCurrentPosition();
    717                 if (mPrograms != null) {
    718                     for (EitItem item : mPrograms) {
    719                         if (currentProgram != null && currentProgram.compareTo(item) == 0) {
    720                             if (DEBUG) {
    721                                 Log.d(TAG, "Update current TvTracks " + item);
    722                             }
    723                             if (mProgram != null && mProgram.compareTo(item) == 0) {
    724                                 continue;
    725                             }
    726                             mProgram = item;
    727                             updateTvTracks(item, false);
    728                         } else if (item.getStartTimeUtcMillis() > currentTimeMs) {
    729                             if (DEBUG) {
    730                                 Log.d(TAG, "Update next TvTracks " + item + " "
    731                                         + (item.getStartTimeUtcMillis() - currentTimeMs));
    732                             }
    733                             mHandler.sendMessageDelayed(
    734                                     mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item),
    735                                     item.getStartTimeUtcMillis() - currentTimeMs);
    736                         }
    737                     }
    738                 }
    739                 mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
    740                 return true;
    741             }
    742             case MSG_UPDATE_CHANNEL_INFO: {
    743                 TunerChannel channel = (TunerChannel) msg.obj;
    744                 if (mChannel != null && mChannel.compareTo(channel) == 0) {
    745                     updateChannelInfo(channel);
    746                 }
    747                 return true;
    748             }
    749             case MSG_PROGRAM_DATA_RESULT: {
    750                 TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first;
    751 
    752                 // If there already exists, skip it since real-time data is a top priority,
    753                 if (mChannel != null && mChannel.compareTo(channel) == 0
    754                         && mPrograms == null && mProgram == null) {
    755                     sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj);
    756                 }
    757                 return true;
    758             }
    759             case MSG_TRICKPLAY_BY_SEEK: {
    760                 if (mPlayer == null) {
    761                     return true;
    762                 }
    763                 doTrickplayBySeek(msg.arg1);
    764                 return true;
    765             }
    766             case MSG_SMOOTH_TRICKPLAY_MONITOR: {
    767                 if (mPlayer == null) {
    768                     return true;
    769                 }
    770                 long systemCurrentTime = System.currentTimeMillis();
    771                 long position = getCurrentPosition();
    772                 if (mRecordingId == null) {
    773                     // Checks if the position exceeds the upper bound when forwarding,
    774                     // or exceed the lower bound when rewinding.
    775                     // If the direction is not checked, there can be some issues.
    776                     // (See b/29939781 for more details.)
    777                     if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L)
    778                             || (position < mBufferStartTimeMs && mPlaybackParams.getSpeed() < 0L)) {
    779                         doTimeShiftResume();
    780                         return true;
    781                     }
    782                 } else {
    783                     if (position > mRecordingDuration || position < 0) {
    784                         doTimeShiftPause();
    785                         return true;
    786                     }
    787                 }
    788                 mHandler.sendEmptyMessageDelayed(MSG_SMOOTH_TRICKPLAY_MONITOR,
    789                         TRICKPLAY_MONITOR_INTERVAL_MS);
    790                 return true;
    791             }
    792             case MSG_RESCHEDULE_PROGRAMS: {
    793                 doReschedulePrograms();
    794                 return true;
    795             }
    796             case MSG_PARENTAL_CONTROLS: {
    797                 doParentalControls();
    798                 mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
    799                 mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS,
    800                         PARENTAL_CONTROLS_INTERVAL_MS);
    801                 return true;
    802             }
    803             case MSG_UNBLOCKED_RATING: {
    804                 mUnblockedContentRating = (TvContentRating) msg.obj;
    805                 doParentalControls();
    806                 mHandler.removeMessages(MSG_PARENTAL_CONTROLS);
    807                 mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS,
    808                         PARENTAL_CONTROLS_INTERVAL_MS);
    809                 return true;
    810             }
    811             case MSG_DISCOVER_CAPTION_SERVICE_NUMBER: {
    812                 int serviceNumber = (int) msg.obj;
    813                 doDiscoverCaptionServiceNumber(serviceNumber);
    814                 return true;
    815             }
    816             case MSG_SELECT_TRACK: {
    817                 if (mChannel != null) {
    818                     doSelectTrack(msg.arg1, (String) msg.obj);
    819                 } else if (mRecordingId != null) {
    820                     // TODO : mChannel == null && mRecordingId != null
    821                     Log.d(TAG, "track selected for recording");
    822                 }
    823                 return true;
    824             }
    825             case MSG_UPDATE_CAPTION_TRACK: {
    826                 if (mCaptionEnabled) {
    827                     startCaptionTrack();
    828                 } else {
    829                     stopCaptionTrack();
    830                 }
    831                 return true;
    832             }
    833             case MSG_TIMESHIFT_PAUSE: {
    834                 if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE");
    835                 if (mPlayer == null) {
    836                     return true;
    837                 }
    838                 doTimeShiftPause();
    839                 return true;
    840             }
    841             case MSG_TIMESHIFT_RESUME: {
    842                 if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME");
    843                 if (mPlayer == null) {
    844                     return true;
    845                 }
    846                 doTimeShiftResume();
    847                 return true;
    848             }
    849             case MSG_TIMESHIFT_SEEK_TO: {
    850                 long position = (long) msg.obj;
    851                 if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + position + ")");
    852                 if (mPlayer == null) {
    853                     return true;
    854                 }
    855                 doTimeShiftSeekTo(position);
    856                 return true;
    857             }
    858             case MSG_TIMESHIFT_SET_PLAYBACKPARAMS: {
    859                 if (mPlayer == null) {
    860                     return true;
    861                 }
    862                 doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj);
    863                 return true;
    864             }
    865             case MSG_AUDIO_CAPABILITIES_CHANGED: {
    866                 AudioCapabilities capabilities = (AudioCapabilities) msg.obj;
    867                 if (DEBUG) {
    868                     Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + capabilities);
    869                 }
    870                 if (capabilities == null) {
    871                     return true;
    872                 }
    873                 if (!capabilities.equals(mAudioCapabilities)) {
    874                     // HDMI supported encodings are changed. restart player.
    875                     mAudioCapabilities = capabilities;
    876                     resetPlayback();
    877                 }
    878                 return true;
    879             }
    880             case MSG_SET_STREAM_VOLUME: {
    881                 if (mPlayer != null && mPlayer.isPlaying()) {
    882                     mPlayer.setVolume(mVolume);
    883                 }
    884                 return true;
    885             }
    886             case MSG_BUFFER_START_TIME_CHANGED: {
    887                 if (mPlayer == null) {
    888                     return true;
    889                 }
    890                 mBufferStartTimeMs = (long) msg.obj;
    891                 if (!hasEnoughBackwardBuffer()
    892                         && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) {
    893                     mPlayer.setPlayWhenReady(true);
    894                     mPlayer.setAudioTrack(true);
    895                     mPlaybackParams.setSpeed(1.0f);
    896                 }
    897                 return true;
    898             }
    899             case MSG_BUFFER_STATE_CHANGED: {
    900                 boolean available = (boolean) msg.obj;
    901                 mSession.notifyTimeShiftStatusChanged(available
    902                         ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
    903                         : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
    904                 return true;
    905             }
    906             case MSG_CHECK_SIGNAL: {
    907                 if (mChannel == null || mPlayer == null) {
    908                     return true;
    909                 }
    910                 TsDataSource source = mPlayer.getDataSource();
    911                 long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
    912                 long positionInBytes = source != null ? source.getLastReadPosition() : 0L;
    913                 if (TunerDebug.ENABLED) {
    914                     TunerDebug.calculateDiff();
    915                     mSession.sendUiMessage(TunerSession.MSG_UI_SET_STATUS_TEXT,
    916                             Html.fromHtml(
    917                                     StatusTextUtils.getStatusWarningInHTML(
    918                                             (limitInBytes - mLastLimitInBytes)
    919                                                     / TS_PACKET_SIZE,
    920                                             TunerDebug.getVideoFrameDrop(),
    921                                             TunerDebug.getBytesInQueue(),
    922                                             TunerDebug.getAudioPositionUs(),
    923                                             TunerDebug.getAudioPositionUsRate(),
    924                                             TunerDebug.getAudioPtsUs(),
    925                                             TunerDebug.getAudioPtsUsRate(),
    926                                             TunerDebug.getVideoPtsUs(),
    927                                             TunerDebug.getVideoPtsUsRate()
    928                                     )));
    929                 }
    930                 if (DEBUG) {
    931                     Log.d(TAG, String.format("MSG_CHECK_SIGNAL position: %d, limit: %d",
    932                             positionInBytes, limitInBytes));
    933                 }
    934                 mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
    935                 long currentTime = SystemClock.elapsedRealtime();
    936                 boolean noBufferRead = positionInBytes == mLastPositionInBytes
    937                         && limitInBytes == mLastLimitInBytes;
    938                 boolean isBufferingTooLong = mBufferingStartTimeMs != INVALID_TIME
    939                         && currentTime - mBufferingStartTimeMs
    940                                 > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
    941                 boolean isPreparingTooLong = mPreparingStartTimeMs != INVALID_TIME
    942                         && currentTime - mPreparingStartTimeMs
    943                         > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
    944                 boolean isWeakSignal = source != null
    945                         && mChannel.getType() == Channel.TYPE_TUNER
    946                         && (noBufferRead || isBufferingTooLong || isPreparingTooLong);
    947                 if (isWeakSignal && !mReportedWeakSignal) {
    948                     if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
    949                         mHandler.sendMessageDelayed(mHandler.obtainMessage(
    950                                 MSG_RETRY_PLAYBACK, mPlayer), PLAYBACK_RETRY_DELAY_MS);
    951                     }
    952                     if (mPlayer != null) {
    953                         mPlayer.setAudioTrack(false);
    954                     }
    955                     notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
    956                 } else if (!isWeakSignal && mReportedWeakSignal) {
    957                     boolean isPlaybackStable = mReadyStartTimeMs != INVALID_TIME
    958                             && currentTime - mReadyStartTimeMs
    959                                     > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
    960                     if (!isPlaybackStable) {
    961                         // Wait until playback becomes stable.
    962                     } else if (mReportedDrawnToSurface) {
    963                         mHandler.removeMessages(MSG_RETRY_PLAYBACK);
    964                         notifyVideoAvailable();
    965                         mPlayer.setAudioTrack(true);
    966                     }
    967                 }
    968                 mLastLimitInBytes = limitInBytes;
    969                 mLastPositionInBytes = positionInBytes;
    970                 mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
    971                 return true;
    972             }
    973             case MSG_SET_SURFACE: {
    974                 if (mPlayer != null) {
    975                     mPlayer.setSurface(mSurface);
    976                 } else {
    977                     // TODO: Since surface is dynamically set, we can remove the dependency of
    978                     // playback start on mSurface nullity.
    979                     resetPlayback();
    980                 }
    981                 return true;
    982             }
    983             case MSG_NOTIFY_AUDIO_TRACK_UPDATED: {
    984                 notifyAudioTracksUpdated();
    985                 return true;
    986             }
    987             default: {
    988                 Log.w(TAG, "Unhandled message code: " + msg.what);
    989                 return false;
    990             }
    991         }
    992     }
    993 
    994     // Private methods
    995     private void doSelectTrack(int type, String trackId) {
    996         int numTrackId = trackId != null
    997                 ? Integer.parseInt(trackId.substring(TRACK_PREFIX_SIZE)) : -1;
    998         if (type == TvTrackInfo.TYPE_AUDIO) {
    999             if (trackId == null) {
   1000                 return;
   1001             }
   1002             AtscAudioTrack audioTrack = mAudioTrackMap.get(numTrackId);
   1003             if (audioTrack == null) {
   1004                 return;
   1005             }
   1006             int oldAudioPid = mChannel.getAudioPid();
   1007             mChannel.selectAudioTrack(audioTrack.index);
   1008             int newAudioPid = mChannel.getAudioPid();
   1009             if (oldAudioPid != newAudioPid) {
   1010                 mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, audioTrack.index);
   1011             }
   1012             mSession.notifyTrackSelected(type, trackId);
   1013         } else if (type == TvTrackInfo.TYPE_SUBTITLE) {
   1014             if (trackId == null) {
   1015                 mSession.notifyTrackSelected(type, null);
   1016                 mCaptionTrack = null;
   1017                 stopCaptionTrack();
   1018                 return;
   1019             }
   1020             for (TvTrackInfo track : mTvTracks) {
   1021                 if (track.getId().equals(trackId)) {
   1022                     // The service number of the caption service is used for track id of a
   1023                     // subtitle track. Passes the following track id on to TsParser.
   1024                     mSession.notifyTrackSelected(type, trackId);
   1025                     mCaptionTrack = mCaptionTrackMap.get(numTrackId);
   1026                     startCaptionTrack();
   1027                     return;
   1028                 }
   1029             }
   1030         }
   1031     }
   1032 
   1033     private MpegTsPlayer createPlayer(AudioCapabilities capabilities, BufferManager bufferManager) {
   1034         if (capabilities == null) {
   1035             Log.w(TAG, "No Audio Capabilities");
   1036         }
   1037 
   1038         MpegTsPlayer player = new MpegTsPlayer(
   1039                 new MpegTsRendererBuilder(mContext, bufferManager, this),
   1040                 mHandler, mSourceManager, capabilities, this);
   1041         Log.i(TAG, "Passthrough AC3 renderer");
   1042         if (DEBUG) Log.d(TAG, "ExoPlayer created");
   1043         return player;
   1044     }
   1045 
   1046     private void startCaptionTrack() {
   1047         if (mCaptionEnabled && mCaptionTrack != null) {
   1048             mSession.sendUiMessage(
   1049                     TunerSession.MSG_UI_START_CAPTION_TRACK, mCaptionTrack);
   1050             if (mPlayer != null) {
   1051                 mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber);
   1052             }
   1053         }
   1054     }
   1055 
   1056     private void stopCaptionTrack() {
   1057         if (mPlayer != null) {
   1058             mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
   1059         }
   1060         mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK);
   1061     }
   1062 
   1063     private void resetTvTracks() {
   1064         mTvTracks.clear();
   1065         mAudioTrackMap.clear();
   1066         mCaptionTrackMap.clear();
   1067         mSession.sendUiMessage(TunerSession.MSG_UI_RESET_CAPTION_TRACK);
   1068         mSession.notifyTracksChanged(mTvTracks);
   1069     }
   1070 
   1071     private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) {
   1072         if (DEBUG) {
   1073             Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
   1074         }
   1075         List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
   1076         List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
   1077         // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio
   1078         // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio
   1079         // track info in PMT more and use info in EIT only when we have nothing.
   1080         if (audioTracks != null && !audioTracks.isEmpty()
   1081                 && (mChannel.getAudioTracks() == null || fromPmt)) {
   1082             updateAudioTracks(audioTracks);
   1083         }
   1084         if (captionTracks == null || captionTracks.isEmpty()) {
   1085             if (tvTracksInterface.hasCaptionTrack()) {
   1086                 updateCaptionTracks(captionTracks);
   1087             }
   1088         } else {
   1089             updateCaptionTracks(captionTracks);
   1090         }
   1091     }
   1092 
   1093     private void removeTvTracks(int trackType) {
   1094         Iterator<TvTrackInfo> iterator = mTvTracks.iterator();
   1095         while (iterator.hasNext()) {
   1096             TvTrackInfo tvTrackInfo = iterator.next();
   1097             if (tvTrackInfo.getType() == trackType) {
   1098                 iterator.remove();
   1099             }
   1100         }
   1101     }
   1102 
   1103     private void updateVideoTrack(int width, int height) {
   1104         removeTvTracks(TvTrackInfo.TYPE_VIDEO);
   1105         mTvTracks.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID)
   1106                 .setVideoWidth(width).setVideoHeight(height).build());
   1107         mSession.notifyTracksChanged(mTvTracks);
   1108         mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID);
   1109     }
   1110 
   1111     private void updateAudioTracks(List<AtscAudioTrack> audioTracks) {
   1112         if (DEBUG) {
   1113             Log.d(TAG, "Update AudioTracks " + audioTracks);
   1114         }
   1115         mAudioTrackMap.clear();
   1116         if (audioTracks != null) {
   1117             int index = 0;
   1118             for (AtscAudioTrack audioTrack : audioTracks) {
   1119                 audioTrack.index = index;
   1120                 mAudioTrackMap.put(index, audioTrack);
   1121                 ++index;
   1122             }
   1123         }
   1124         mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED);
   1125     }
   1126 
   1127     private void notifyAudioTracksUpdated() {
   1128         if (mPlayer == null) {
   1129             // Audio tracks will be updated later once player initialization is done.
   1130             return;
   1131         }
   1132         int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO);
   1133         removeTvTracks(TvTrackInfo.TYPE_AUDIO);
   1134         for (int i = 0; i < audioTrackCount; i++) {
   1135             AtscAudioTrack audioTrack = mAudioTrackMap.get(i);
   1136             if (audioTrack == null) {
   1137                 continue;
   1138             }
   1139             String language = audioTrack.language;
   1140             if (language == null && mChannel.getAudioTracks() != null
   1141                     && mChannel.getAudioTracks().size() == mAudioTrackMap.size()) {
   1142                 // If a language is not present, use a language field in PMT section parsed.
   1143                 language = mChannel.getAudioTracks().get(i).language;
   1144             }
   1145             // Save the index to the audio track.
   1146             // Later, when an audio track is selected, both the audio pid and its audio stream
   1147             // type reside in the selected index position of the tuner channel's audio data.
   1148             audioTrack.index = i;
   1149             TvTrackInfo.Builder builder = new TvTrackInfo.Builder(
   1150                     TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i);
   1151             builder.setLanguage(language);
   1152             builder.setAudioChannelCount(audioTrack.channelCount);
   1153             builder.setAudioSampleRate(audioTrack.sampleRate);
   1154             TvTrackInfo track = builder.build();
   1155             mTvTracks.add(track);
   1156         }
   1157         mSession.notifyTracksChanged(mTvTracks);
   1158     }
   1159 
   1160     private void updateCaptionTracks(List<AtscCaptionTrack> captionTracks) {
   1161         if (DEBUG) {
   1162             Log.d(TAG, "Update CaptionTrack " + captionTracks);
   1163         }
   1164         removeTvTracks(TvTrackInfo.TYPE_SUBTITLE);
   1165         mCaptionTrackMap.clear();
   1166         if (captionTracks != null) {
   1167             for (AtscCaptionTrack captionTrack : captionTracks) {
   1168                 if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) {
   1169                     continue;
   1170                 }
   1171                 String language = captionTrack.language;
   1172 
   1173                 // The service number of the caption service is used for track id of a subtitle.
   1174                 // Later, when a subtitle is chosen, track id will be passed on to TsParser.
   1175                 TvTrackInfo.Builder builder =
   1176                         new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE,
   1177                                 SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber);
   1178                 builder.setLanguage(language);
   1179                 mTvTracks.add(builder.build());
   1180                 mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack);
   1181             }
   1182         }
   1183         mSession.notifyTracksChanged(mTvTracks);
   1184     }
   1185 
   1186     private void updateChannelInfo(TunerChannel channel) {
   1187         if (DEBUG) {
   1188             Log.d(TAG, String.format("Channel Info (old) videoPid: %d audioPid: %d " +
   1189                     "audioSize: %d", mChannel.getVideoPid(), mChannel.getAudioPid(),
   1190                     mChannel.getAudioPids().size()));
   1191         }
   1192 
   1193         // The list of the audio tracks resided in a channel is often changed depending on a
   1194         // program being on the air. So, we should update the streaming PIDs and types of the
   1195         // tuned channel according to the newly received channel data.
   1196         int oldVideoPid = mChannel.getVideoPid();
   1197         int oldAudioPid = mChannel.getAudioPid();
   1198         List<Integer> audioPids = channel.getAudioPids();
   1199         List<Integer> audioStreamTypes = channel.getAudioStreamTypes();
   1200         int size = audioPids.size();
   1201         mChannel.setVideoPid(channel.getVideoPid());
   1202         mChannel.setAudioPids(audioPids);
   1203         mChannel.setAudioStreamTypes(audioStreamTypes);
   1204         updateTvTracks(channel, true);
   1205         int index = audioPids.isEmpty() ? -1 : 0;
   1206         for (int i = 0; i < size; ++i) {
   1207             if (audioPids.get(i) == oldAudioPid) {
   1208                 index = i;
   1209                 break;
   1210             }
   1211         }
   1212         mChannel.selectAudioTrack(index);
   1213         mSession.notifyTrackSelected(TvTrackInfo.TYPE_AUDIO,
   1214                 index == -1 ? null : AUDIO_TRACK_PREFIX + index);
   1215 
   1216         // Reset playback if there is a change in the listening streaming PIDs.
   1217         if (oldVideoPid != mChannel.getVideoPid()
   1218                 || oldAudioPid != mChannel.getAudioPid()) {
   1219             // TODO: Implement a switching between tracks more smoothly.
   1220             resetPlayback();
   1221         }
   1222         if (DEBUG) {
   1223             Log.d(TAG, String.format("Channel Info (new) videoPid: %d audioPid: %d " +
   1224                     " audioSize: %d", mChannel.getVideoPid(), mChannel.getAudioPid(),
   1225                     mChannel.getAudioPids().size()));
   1226         }
   1227     }
   1228 
   1229     private void stopPlayback() {
   1230         mChannelDataManager.removeAllCallbacksAndMessages();
   1231         if (mPlayer != null) {
   1232             mPlayer.setPlayWhenReady(false);
   1233             mPlayer.release();
   1234             mPlayer = null;
   1235             mPlayerState = ExoPlayer.STATE_IDLE;
   1236             mPlaybackParams.setSpeed(1.0f);
   1237             mPlayerStarted = false;
   1238             mReportedDrawnToSurface = false;
   1239             mPreparingStartTimeMs = INVALID_TIME;
   1240             mBufferingStartTimeMs = INVALID_TIME;
   1241             mReadyStartTimeMs = INVALID_TIME;
   1242             mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE);
   1243             mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE);
   1244         }
   1245     }
   1246 
   1247     private void startPlayback(Object playerObj) {
   1248         // TODO: provide hasAudio()/hasVideo() for play recordings.
   1249         if (mPlayer == null || mPlayer != playerObj) {
   1250             return;
   1251         }
   1252         if (mChannel != null && !mChannel.hasAudio()) {
   1253             if (DEBUG) Log.d(TAG, "Channel " + mChannel + " does not have audio.");
   1254             // Playbacks with video-only stream have not been tested yet.
   1255             // No video-only channel has been found.
   1256             notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
   1257             return;
   1258         }
   1259         if (mChannel != null && ((mChannel.hasAudio() && !mPlayer.hasAudio())
   1260                 || (mChannel.hasVideo() && !mPlayer.hasVideo()))) {
   1261             // Tracks haven't been detected in the extractor. Try again.
   1262             sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
   1263             return;
   1264         }
   1265         // Since mSurface is volatile, we define a local variable surface to keep the same value
   1266         // inside this method.
   1267         Surface surface = mSurface;
   1268         if (surface != null && !mPlayerStarted) {
   1269             mPlayer.setSurface(surface);
   1270             mPlayer.setPlayWhenReady(true);
   1271             mPlayer.setVolume(mVolume);
   1272             if (mChannel != null && !mChannel.hasVideo() && mChannel.hasAudio()) {
   1273                 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY);
   1274             } else if (!mReportedWeakSignal) {
   1275                 // Doesn't show buffering during weak signal.
   1276                 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING);
   1277             }
   1278             mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
   1279             mPlayerStarted = true;
   1280         }
   1281     }
   1282 
   1283     private void preparePlayback() {
   1284         SoftPreconditions.checkState(mPlayer == null);
   1285         if (mChannel == null && mRecordingId == null) {
   1286             return;
   1287         }
   1288         mSourceManager.setKeepTuneStatus(true);
   1289         BufferManager bufferManager = mChannel != null ? mBufferManager : new BufferManager(
   1290                 new DvrStorageManager(new File(getRecordingPath()), false));
   1291         MpegTsPlayer player = createPlayer(mAudioCapabilities, bufferManager);
   1292         player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
   1293         player.setVideoEventListener(this);
   1294         player.setCaptionServiceNumber(mCaptionTrack != null ?
   1295                 mCaptionTrack.serviceNumber : Cea708Data.EMPTY_SERVICE_NUMBER);
   1296         if (!player.prepare(mContext, mChannel, this)) {
   1297             mSourceManager.setKeepTuneStatus(false);
   1298             player.release();
   1299             if (!mHandler.hasMessages(MSG_TUNE)) {
   1300                 // When prepare failed, there may be some errors related to hardware. In that
   1301                 // case, retry playback immediately may not help.
   1302                 notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
   1303                 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer),
   1304                         PLAYBACK_RETRY_DELAY_MS);
   1305             }
   1306         } else {
   1307             mPlayer = player;
   1308             mPlayerStarted = false;
   1309             mHandler.removeMessages(MSG_CHECK_SIGNAL);
   1310             mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
   1311         }
   1312     }
   1313 
   1314     private void resetPlayback() {
   1315         long timestamp, oldTimestamp;
   1316         timestamp = SystemClock.elapsedRealtime();
   1317         stopPlayback();
   1318         stopCaptionTrack();
   1319         if (ENABLE_PROFILER) {
   1320             oldTimestamp = timestamp;
   1321             timestamp = SystemClock.elapsedRealtime();
   1322             Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms");
   1323         }
   1324         if (mChannelBlocked || mSurface == null) {
   1325             return;
   1326         }
   1327         preparePlayback();
   1328     }
   1329 
   1330     private void prepareTune(TunerChannel channel, String recording) {
   1331         mChannelBlocked = false;
   1332         mUnblockedContentRating = null;
   1333         mRetryCount = 0;
   1334         mChannel = channel;
   1335         mRecordingId = recording;
   1336         mRecordingDuration = recording != null ? getDurationForRecording(recording) : null;
   1337         mProgram = null;
   1338         mPrograms = null;
   1339         mBufferStartTimeMs = mRecordStartTimeMs =
   1340                 (mRecordingId != null) ? 0 : System.currentTimeMillis();
   1341         mLastPositionMs = 0;
   1342         mCaptionTrack = null;
   1343         mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
   1344     }
   1345 
   1346     private void doReschedulePrograms() {
   1347         long currentPositionMs = getCurrentPosition();
   1348         long forwardDifference = Math.abs(currentPositionMs - mLastPositionMs
   1349                 - RESCHEDULE_PROGRAMS_INTERVAL_MS);
   1350         mLastPositionMs = currentPositionMs;
   1351 
   1352         // A gap is measured as the time difference between previous and next current position
   1353         // periodically. If the gap has a significant difference with an interval of a period,
   1354         // this means that there is a change of playback status and the programs of the current
   1355         // channel should be rescheduled to new playback timeline.
   1356         if (forwardDifference > RESCHEDULE_PROGRAMS_TOLERANCE_MS) {
   1357             if (DEBUG) {
   1358                 Log.d(TAG, "reschedule programs size:"
   1359                         + (mPrograms != null ? mPrograms.size() : 0) + " current program: "
   1360                         + getCurrentProgram());
   1361             }
   1362             mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms))
   1363                     .sendToTarget();
   1364         }
   1365         mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS);
   1366         mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
   1367                 RESCHEDULE_PROGRAMS_INTERVAL_MS);
   1368     }
   1369 
   1370     private int getTrickPlaySeekIntervalMs() {
   1371         return Math.max(EXPECTED_KEY_FRAME_INTERVAL_MS / (int) Math.abs(mPlaybackParams.getSpeed()),
   1372                 MIN_TRICKPLAY_SEEK_INTERVAL_MS);
   1373     }
   1374 
   1375     private void doTrickplayBySeek(int seekPositionMs) {
   1376         mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
   1377         if (mPlaybackParams.getSpeed() == 1.0f || !mPlayer.isPrepared()) {
   1378             return;
   1379         }
   1380         if (seekPositionMs < mBufferStartTimeMs - mRecordStartTimeMs) {
   1381             if (mPlaybackParams.getSpeed() > 1.0f) {
   1382                 // If fast forwarding, the seekPositionMs can be out of the buffered range
   1383                 // because of chuck evictions.
   1384                 seekPositionMs = (int) (mBufferStartTimeMs - mRecordStartTimeMs);
   1385             } else {
   1386                 mPlayer.seekTo(mBufferStartTimeMs - mRecordStartTimeMs);
   1387                 mPlaybackParams.setSpeed(1.0f);
   1388                 mPlayer.setAudioTrack(true);
   1389                 return;
   1390             }
   1391         } else if (seekPositionMs > System.currentTimeMillis() - mRecordStartTimeMs) {
   1392             mPlayer.seekTo(System.currentTimeMillis() - mRecordStartTimeMs);
   1393             mPlaybackParams.setSpeed(1.0f);
   1394             mPlayer.setAudioTrack(true);
   1395             return;
   1396         }
   1397 
   1398         long delayForNextSeek = getTrickPlaySeekIntervalMs();
   1399         if (!mPlayer.isBuffering()) {
   1400             mPlayer.seekTo(seekPositionMs);
   1401         } else {
   1402             delayForNextSeek = MIN_TRICKPLAY_SEEK_INTERVAL_MS;
   1403         }
   1404         seekPositionMs += mPlaybackParams.getSpeed() * delayForNextSeek;
   1405         mHandler.sendMessageDelayed(mHandler.obtainMessage(
   1406                 MSG_TRICKPLAY_BY_SEEK, seekPositionMs, 0), delayForNextSeek);
   1407     }
   1408 
   1409     private void doTimeShiftPause() {
   1410         mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
   1411         mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
   1412         if (!hasEnoughBackwardBuffer()) {
   1413             return;
   1414         }
   1415         mPlaybackParams.setSpeed(1.0f);
   1416         mPlayer.setPlayWhenReady(false);
   1417         mPlayer.setAudioTrack(true);
   1418     }
   1419 
   1420     private void doTimeShiftResume() {
   1421         mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
   1422         mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
   1423         mPlaybackParams.setSpeed(1.0f);
   1424         mPlayer.setPlayWhenReady(true);
   1425         mPlayer.setAudioTrack(true);
   1426     }
   1427 
   1428     private void doTimeShiftSeekTo(long timeMs) {
   1429         mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
   1430         mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
   1431         mPlayer.seekTo((int) (timeMs - mRecordStartTimeMs));
   1432     }
   1433 
   1434     private void doTimeShiftSetPlaybackParams(PlaybackParams params) {
   1435         if (!hasEnoughBackwardBuffer() && params.getSpeed() < 1.0f) {
   1436             return;
   1437         }
   1438         mPlaybackParams = params;
   1439         float speed = mPlaybackParams.getSpeed();
   1440         if (speed == 1.0f) {
   1441             mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
   1442             mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
   1443             doTimeShiftResume();
   1444         } else if (mPlayer.supportSmoothTrickPlay(speed)) {
   1445             mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK);
   1446             mPlayer.setAudioTrack(false);
   1447             mPlayer.startSmoothTrickplay(mPlaybackParams);
   1448             mHandler.sendEmptyMessageDelayed(MSG_SMOOTH_TRICKPLAY_MONITOR,
   1449                     TRICKPLAY_MONITOR_INTERVAL_MS);
   1450         } else {
   1451             mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR);
   1452             if (!mHandler.hasMessages(MSG_TRICKPLAY_BY_SEEK)) {
   1453                 mPlayer.setAudioTrack(false);
   1454                 mPlayer.setPlayWhenReady(false);
   1455                 // Initiate trickplay
   1456                 mHandler.sendMessage(mHandler.obtainMessage(MSG_TRICKPLAY_BY_SEEK,
   1457                         (int) (mPlayer.getCurrentPosition()
   1458                                 + speed * getTrickPlaySeekIntervalMs()), 0));
   1459             }
   1460         }
   1461     }
   1462 
   1463     private EitItem getCurrentProgram() {
   1464         if (mPrograms == null || mPrograms.isEmpty()) {
   1465             return null;
   1466         }
   1467         if (mChannel.getType() == Channel.TYPE_FILE) {
   1468             // For the playback from the local file, we use the first one from the given program.
   1469             EitItem first = mPrograms.get(0);
   1470             if (first != null && (mProgram == null
   1471                     || first.getStartTimeUtcMillis() < mProgram.getStartTimeUtcMillis())) {
   1472                 return first;
   1473             }
   1474             return null;
   1475         }
   1476         long currentTimeMs = getCurrentPosition();
   1477         for (EitItem item : mPrograms) {
   1478             if (item.getStartTimeUtcMillis() <= currentTimeMs
   1479                     && item.getEndTimeUtcMillis() >= currentTimeMs) {
   1480                 return item;
   1481             }
   1482         }
   1483         return null;
   1484     }
   1485 
   1486     private void doParentalControls() {
   1487         boolean isParentalControlsEnabled = mTvInputManager.isParentalControlsEnabled();
   1488         if (isParentalControlsEnabled) {
   1489             TvContentRating blockContentRating = getContentRatingOfCurrentProgramBlocked();
   1490             if (DEBUG) {
   1491                 if (blockContentRating != null) {
   1492                     Log.d(TAG, "Check parental controls: blocked by content rating - "
   1493                             + blockContentRating);
   1494                 } else {
   1495                     Log.d(TAG, "Check parental controls: available");
   1496                 }
   1497             }
   1498             updateChannelBlockStatus(blockContentRating != null, blockContentRating);
   1499         } else {
   1500             if (DEBUG) {
   1501                 Log.d(TAG, "Check parental controls: available");
   1502             }
   1503             updateChannelBlockStatus(false, null);
   1504         }
   1505     }
   1506 
   1507     private void doDiscoverCaptionServiceNumber(int serviceNumber) {
   1508         int index = mCaptionTrackMap.indexOfKey(serviceNumber);
   1509         if (index < 0) {
   1510             AtscCaptionTrack captionTrack = new AtscCaptionTrack();
   1511             captionTrack.serviceNumber = serviceNumber;
   1512             captionTrack.wideAspectRatio = false;
   1513             captionTrack.easyReader = false;
   1514             mCaptionTrackMap.put(serviceNumber, captionTrack);
   1515             mTvTracks.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE,
   1516                     SUBTITLE_TRACK_PREFIX + serviceNumber).build());
   1517             mSession.notifyTracksChanged(mTvTracks);
   1518         }
   1519     }
   1520 
   1521     private TvContentRating getContentRatingOfCurrentProgramBlocked() {
   1522         EitItem currentProgram = getCurrentProgram();
   1523         if (currentProgram == null) {
   1524             return null;
   1525         }
   1526         TvContentRating[] ratings = mTvContentRatingCache
   1527                 .getRatings(currentProgram.getContentRating());
   1528         if (ratings == null) {
   1529             return null;
   1530         }
   1531         for (TvContentRating rating : ratings) {
   1532             if (!Objects.equals(mUnblockedContentRating, rating) && mTvInputManager
   1533                     .isRatingBlocked(rating)) {
   1534                 return rating;
   1535             }
   1536         }
   1537         return null;
   1538     }
   1539 
   1540     private void updateChannelBlockStatus(boolean channelBlocked,
   1541             TvContentRating contentRating) {
   1542         if (mChannelBlocked == channelBlocked) {
   1543             return;
   1544         }
   1545         mChannelBlocked = channelBlocked;
   1546         if (mChannelBlocked) {
   1547             mHandler.removeCallbacksAndMessages(null);
   1548             stopPlayback();
   1549             resetTvTracks();
   1550             if (contentRating != null) {
   1551                 mSession.notifyContentBlocked(contentRating);
   1552             }
   1553             mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
   1554         } else {
   1555             mHandler.removeCallbacksAndMessages(null);
   1556             resetPlayback();
   1557             mSession.notifyContentAllowed();
   1558             mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
   1559                     RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
   1560             mHandler.removeMessages(MSG_CHECK_SIGNAL);
   1561             mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS);
   1562         }
   1563     }
   1564 
   1565     private boolean hasEnoughBackwardBuffer() {
   1566         return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS
   1567                 >= mBufferStartTimeMs - mRecordStartTimeMs;
   1568     }
   1569 
   1570     private void notifyVideoUnavailable(final int reason) {
   1571         mReportedWeakSignal = (reason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
   1572         if (mSession != null) {
   1573             mSession.notifyVideoUnavailable(reason);
   1574         }
   1575     }
   1576 
   1577     private void notifyVideoAvailable() {
   1578         mReportedWeakSignal = false;
   1579         if (mSession != null) {
   1580             mSession.notifyVideoAvailable();
   1581         }
   1582     }
   1583 }
   1584