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