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