• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.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 
TunerSessionWorker( Context context, ChannelDataManager channelDataManager, TunerSession tunerSession)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
tune(Uri channelUri)279     public void tune(Uri channelUri) {
280         mHandler.removeCallbacksAndMessages(null);
281         mSourceManager.setHasPendingTune();
282         sendMessage(MSG_TUNE, channelUri);
283     }
284 
285     @MainThread
stopTune()286     public void stopTune() {
287         mHandler.removeCallbacksAndMessages(null);
288         sendMessage(MSG_STOP_TUNE);
289     }
290 
291     /** Sets {@link Surface}. */
292     @MainThread
setSurface(Surface surface)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
setStreamVolume(float volume)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
setCaptionEnabled(boolean captionEnabled)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 
getCurrentChannel()324     public TunerChannel getCurrentChannel() {
325         return mChannel;
326     }
327 
328     @MainThread
getStartPosition()329     public long getStartPosition() {
330         return mBufferStartTimeMs;
331     }
332 
getRecordingPath()333     private String getRecordingPath() {
334         return Uri.parse(mRecordingId).getPath();
335     }
336 
getDurationForRecording(String recordingId)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
getCurrentPosition()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
sendMessage(int messageType)380     public void sendMessage(int messageType) {
381         mHandler.sendEmptyMessage(messageType);
382     }
383 
384     @AnyThread
sendMessage(int messageType, Object object)385     public void sendMessage(int messageType, Object object) {
386         mHandler.obtainMessage(messageType, object).sendToTarget();
387     }
388 
389     @AnyThread
sendMessage(int messageType, int arg1, int arg2, Object object)390     public void sendMessage(int messageType, int arg1, int arg2, Object object) {
391         mHandler.obtainMessage(messageType, arg1, arg2, object).sendToTarget();
392     }
393 
394     @MainThread
release()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
onStateChanged(boolean playWhenReady, int playbackState)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
onError(Exception e)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
onVideoSizeChanged(int width, int height, float pixelWidthHeight)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
onDrawnToSurface(MpegTsPlayer player, Surface surface)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
onSmoothTrickplayForceStopped()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
onAudioUnplayable()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
onEmitCaptionEvent(Cea708Data.CaptionEvent event)517     public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) {
518         mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event);
519     }
520 
521     @Override
onClearCaptionEvent()522     public void onClearCaptionEvent() {
523         mSession.sendUiMessage(TunerSession.MSG_UI_CLEAR_CAPTION_RENDERER);
524     }
525 
526     @Override
onDiscoverCaptionServiceNumber(int serviceNumber)527     public void onDiscoverCaptionServiceNumber(int serviceNumber) {
528         sendMessage(MSG_DISCOVER_CAPTION_SERVICE_NUMBER, serviceNumber);
529     }
530 
531     // ChannelDataManager.ProgramInfoListener
532     @Override
onProgramsArrived(TunerChannel channel, List<EitItem> programs)533     public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) {
534         sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs));
535     }
536 
537     @Override
onChannelArrived(TunerChannel channel)538     public void onChannelArrived(TunerChannel channel) {
539         sendMessage(MSG_UPDATE_CHANNEL_INFO, channel);
540     }
541 
542     @Override
onRescanNeeded()543     public void onRescanNeeded() {
544         mSession.sendUiMessage(TunerSession.MSG_UI_TOAST_RESCAN_NEEDED);
545     }
546 
547     @Override
onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs)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
onBufferStartTimeChanged(long startTimeMs)554     public void onBufferStartTimeChanged(long startTimeMs) {
555         sendMessage(MSG_BUFFER_START_TIME_CHANGED, startTimeMs);
556     }
557 
558     @Override
onBufferStateChanged(boolean available)559     public void onBufferStateChanged(boolean available) {
560         sendMessage(MSG_BUFFER_STATE_CHANGED, available);
561     }
562 
563     @Override
onDiskTooSlow()564     public void onDiskTooSlow() {
565         mTrickplayDisabledByStorageIssue = true;
566         sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
567     }
568 
569     // EventDetector.EventListener
570     @Override
onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime)571     public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
572         mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
573     }
574 
575     @Override
onEventDetected(TunerChannel channel, List<EitItem> items)576     public void onEventDetected(TunerChannel channel, List<EitItem> items) {
577         mChannelDataManager.notifyEventDetected(channel, items);
578     }
579 
580     @Override
onChannelScanDone()581     public void onChannelScanDone() {
582         // do nothing.
583     }
584 
parseChannel(Uri uri)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 
RecordedProgram(Cursor cursor)605         public RecordedProgram(Cursor cursor) {
606             int index = 0;
607             //            mChannelId = cursor.getLong(index++);
608             index++;
609             mDataUri = cursor.getString(index++);
610         }
611 
RecordedProgram(long channelId, String dataUri)612         public RecordedProgram(long channelId, String dataUri) {
613             //            mChannelId = channelId;
614             mDataUri = dataUri;
615         }
616 
onQuery(Cursor c)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 
getDataUri()625         public String getDataUri() {
626             return mDataUri;
627         }
628     }
629 
getRecordedProgram(Uri recordedUri)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 
parseRecording(Uri uri)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
handleMessage(Message msg)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
doSelectTrack(int type, String trackId)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 
setTrickplayEnabledIfNeeded()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 
createPlayer(AudioCapabilities capabilities)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 
startCaptionTrack()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 
stopCaptionTrack()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 
resetTvTracks()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 
updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt)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 
removeTvTracks(int trackType)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 
updateVideoTrack(int width, int height)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 
updateAudioTracks(List<AtscAudioTrack> audioTracks)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 
notifyAudioTracksUpdated()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 
updateCaptionTracks(List<AtscCaptionTrack> captionTracks)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 
updateChannelInfo(TunerChannel channel)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 
stopPlayback(boolean removeChannelDataCallbacks)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 
startPlayback(int playerHashCode)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 
preparePlayback()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 
resetPlayback()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 
prepareTune(TunerChannel channel, String recording)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 
doReschedulePrograms()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 
getTrickPlaySeekIntervalMs()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")
doTrickplayBySeek(int seekPositionMs)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 
doTimeShiftPause()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 
doTimeShiftResume()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 
doTimeShiftSeekTo(long timeMs)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 
doTimeShiftSetPlaybackParams(PlaybackParams params)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 
getCurrentProgram()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 
doParentalControls()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 
doDiscoverCaptionServiceNumber(int serviceNumber)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 
getContentRatingOfCurrentProgramBlocked()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 
updateChannelBlockStatus(boolean channelBlocked, TvContentRating contentRating)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
clearCallbacksAndMessagesSafely()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 
hasEnoughBackwardBuffer()1845     private boolean hasEnoughBackwardBuffer() {
1846         return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS
1847                 >= mBufferStartTimeMs - mRecordStartTimeMs;
1848     }
1849 
notifyVideoUnavailable(final int reason)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 
notifyVideoAvailable()1857     private void notifyVideoAvailable() {
1858         mReportedWeakSignal = false;
1859         if (mSession != null) {
1860             mSession.notifyVideoAvailable();
1861         }
1862     }
1863 }
1864