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