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