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.tv.tuner.tvinput;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.database.Cursor;
24import android.media.tv.TvContract;
25import android.media.tv.TvInputManager;
26import android.net.Uri;
27import android.os.AsyncTask;
28import android.os.Handler;
29import android.os.HandlerThread;
30import android.os.Message;
31import android.support.annotation.IntDef;
32import android.support.annotation.MainThread;
33import android.support.annotation.Nullable;
34import android.util.Log;
35
36import android.util.Pair;
37import com.google.android.exoplayer.C;
38import com.android.tv.TvApplication;
39import com.android.tv.common.SoftPreconditions;
40import com.android.tv.common.recording.RecordingCapability;
41import com.android.tv.dvr.DvrStorageStatusManager;
42import com.android.tv.dvr.data.RecordedProgram;
43import com.android.tv.tuner.DvbDeviceAccessor;
44import com.android.tv.tuner.data.PsipData;
45import com.android.tv.tuner.data.PsipData.EitItem;
46import com.android.tv.tuner.data.TunerChannel;
47import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
48import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor;
49import com.android.tv.tuner.exoplayer.SampleExtractor;
50import com.android.tv.tuner.exoplayer.buffer.BufferManager;
51import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
52import com.android.tv.tuner.source.TsDataSource;
53import com.android.tv.tuner.source.TsDataSourceManager;
54import com.android.tv.util.Utils;
55
56import java.io.File;
57import java.io.IOException;
58import java.lang.annotation.Retention;
59import java.lang.annotation.RetentionPolicy;
60import java.util.ArrayList;
61import java.util.List;
62import java.util.Locale;
63import java.util.Random;
64import java.util.concurrent.TimeUnit;
65
66/**
67 * Implements a DVR feature.
68 */
69public class TunerRecordingSessionWorker implements PlaybackBufferListener,
70        EventDetector.EventListener, SampleExtractor.OnCompletionListener,
71        Handler.Callback {
72    private static final String TAG = "TunerRecordingSessionW";
73    private static final boolean DEBUG = false;
74
75    private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS
76            + ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", "
77            + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS;
78    private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
79    private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
80    private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
81    private static final long PREPARE_RECORDER_POLL_MS = 50;
82    private static final int MSG_TUNE = 1;
83    private static final int MSG_START_RECORDING = 2;
84    private static final int MSG_PREPARE_RECODER = 3;
85    private static final int MSG_STOP_RECORDING = 4;
86    private static final int MSG_MONITOR_STORAGE_STATUS = 5;
87    private static final int MSG_RELEASE = 6;
88    private static final int MSG_UPDATE_CC_INFO = 7;
89    private final RecordingCapability mCapabilities;
90
91    public RecordingCapability getCapabilities() {
92        return mCapabilities;
93    }
94
95    @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING})
96    @Retention(RetentionPolicy.SOURCE)
97    public @interface DvrSessionState {}
98    private static final int STATE_IDLE = 1;
99    private static final int STATE_TUNING = 2;
100    private static final int STATE_TUNED = 3;
101    private static final int STATE_RECORDING = 4;
102
103    private static final long CHANNEL_ID_NONE = -1;
104    private static final int MAX_TUNING_RETRY = 6;
105
106    private final Context mContext;
107    private final ChannelDataManager mChannelDataManager;
108    private final DvrStorageStatusManager mDvrStorageStatusManager;
109    private final Handler mHandler;
110    private final TsDataSourceManager mSourceManager;
111    private final Random mRandom = new Random();
112
113    private TsDataSource mTunerSource;
114    private TunerChannel mChannel;
115    private File mStorageDir;
116    private long mRecordStartTime;
117    private long mRecordEndTime;
118    private boolean mRecorderRunning;
119    private SampleExtractor mRecorder;
120    private final TunerRecordingSession mSession;
121    @DvrSessionState private int mSessionState = STATE_IDLE;
122    private final String mInputId;
123    private Uri mProgramUri;
124
125    private PsipData.EitItem mCurrenProgram;
126    private List<AtscCaptionTrack> mCaptionTracks;
127    private DvrStorageManager mDvrStorageManager;
128
129    public TunerRecordingSessionWorker(Context context, String inputId,
130            ChannelDataManager dataManager, TunerRecordingSession session) {
131        mRandom.setSeed(System.nanoTime());
132        mContext = context;
133        HandlerThread handlerThread = new HandlerThread(TAG);
134        handlerThread.start();
135        mHandler = new Handler(handlerThread.getLooper(), this);
136        mDvrStorageStatusManager =
137                TvApplication.getSingletons(context).getDvrStorageStatusManager();
138        mChannelDataManager = dataManager;
139        mChannelDataManager.checkDataVersion(context);
140        mSourceManager = TsDataSourceManager.createSourceManager(true);
141        mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId);
142        mInputId = inputId;
143        if (DEBUG) Log.d(TAG, mCapabilities.toString());
144        mSession = session;
145    }
146
147    // PlaybackBufferListener
148    @Override
149    public void onBufferStartTimeChanged(long startTimeMs) { }
150
151    @Override
152    public void onBufferStateChanged(boolean available) { }
153
154    @Override
155    public void onDiskTooSlow() { }
156
157    // EventDetector.EventListener
158    @Override
159    public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
160        if (mChannel == null || mChannel.compareTo(channel) != 0) {
161            return;
162        }
163        mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
164    }
165
166    @Override
167    public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
168        if (mChannel == null || mChannel.compareTo(channel) != 0) {
169            return;
170        }
171        mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget();
172        mChannelDataManager.notifyEventDetected(channel, items);
173    }
174
175    @Override
176    public void onChannelScanDone() {
177        // do nothing.
178    }
179
180    // SampleExtractor.OnCompletionListener
181    @Override
182    public void onCompletion(boolean success, long lastExtractedPositionUs) {
183        onRecordingResult(success, lastExtractedPositionUs);
184        reset();
185    }
186
187    /**
188     * Tunes to {@code channelUri}.
189     */
190    @MainThread
191    public void tune(Uri channelUri) {
192        mHandler.removeCallbacksAndMessages(null);
193        mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget();
194    }
195
196    /**
197     * Starts recording.
198     */
199    @MainThread
200    public void startRecording(@Nullable Uri programUri) {
201        mHandler.obtainMessage(MSG_START_RECORDING, programUri).sendToTarget();
202    }
203
204    /**
205     * Stops recording.
206     */
207    @MainThread
208    public void stopRecording() {
209        mHandler.sendEmptyMessage(MSG_STOP_RECORDING);
210    }
211
212    /**
213     * Releases all resources.
214     */
215    @MainThread
216    public void release() {
217        mHandler.removeCallbacksAndMessages(null);
218        mHandler.sendEmptyMessage(MSG_RELEASE);
219    }
220
221    @Override
222    public boolean handleMessage(Message msg) {
223        switch (msg.what) {
224            case MSG_TUNE: {
225                Uri channelUri = (Uri) msg.obj;
226                int retryCount = msg.arg1;
227                if (DEBUG) Log.d(TAG, "Tune to " + channelUri);
228                if (doTune(channelUri)) {
229                    if (mSessionState == STATE_TUNED) {
230                        mSession.onTuned(channelUri);
231                    } else {
232                        Log.w(TAG, "Tuner stream cannot be created due to resource shortage.");
233                        if (retryCount < MAX_TUNING_RETRY) {
234                            Message tuneMsg =
235                                    mHandler.obtainMessage(MSG_TUNE, retryCount + 1, 0, channelUri);
236                            mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS);
237                        } else {
238                            mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
239                            reset();
240                        }
241                    }
242                }
243                return true;
244            }
245            case MSG_START_RECORDING: {
246                if (DEBUG) Log.d(TAG, "Start recording");
247                if (!doStartRecording((Uri) msg.obj)) {
248                    reset();
249                }
250                return true;
251            }
252            case MSG_PREPARE_RECODER: {
253                if (DEBUG) Log.d(TAG, "Preparing recorder");
254                if (!mRecorderRunning) {
255                    return true;
256                }
257                try {
258                    if (!mRecorder.prepare()) {
259                        mHandler.sendEmptyMessageDelayed(MSG_PREPARE_RECODER,
260                                PREPARE_RECORDER_POLL_MS);
261                    }
262                } catch (IOException e) {
263                    Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor");
264                    mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
265                    reset();
266                }
267                return true;
268            }
269            case MSG_STOP_RECORDING: {
270                if (DEBUG) Log.d(TAG, "Stop recording");
271                if (mSessionState != STATE_RECORDING) {
272                    mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
273                    reset();
274                    return true;
275                }
276                if (mRecorderRunning) {
277                    stopRecorder();
278                }
279                return true;
280            }
281            case MSG_MONITOR_STORAGE_STATUS: {
282                if (mSessionState != STATE_RECORDING) {
283                    return true;
284                }
285                if (!mDvrStorageStatusManager.isStorageSufficient()) {
286                    if (mRecorderRunning) {
287                        stopRecorder();
288                    }
289                    new DeleteRecordingTask().execute(mStorageDir);
290                    mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
291                    reset();
292                } else {
293                    mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS,
294                            STORAGE_MONITOR_INTERVAL_MS);
295                }
296                return true;
297            }
298            case MSG_RELEASE: {
299                // Since release was requested, current recording will be cancelled
300                // without notification.
301                reset();
302                mSourceManager.release();
303                mHandler.removeCallbacksAndMessages(null);
304                mHandler.getLooper().quitSafely();
305                return true;
306            }
307            case MSG_UPDATE_CC_INFO: {
308                Pair<TunerChannel, List<EitItem>> pair =
309                        (Pair<TunerChannel, List<EitItem>>) msg.obj;
310                updateCaptionTracks(pair.first, pair.second);
311                return true;
312            }
313        }
314        return false;
315    }
316
317    @Nullable
318    private TunerChannel getChannel(Uri channelUri) {
319        if (channelUri == null) {
320            return null;
321        }
322        long channelId;
323        try {
324            channelId = ContentUris.parseId(channelUri);
325        } catch (UnsupportedOperationException | NumberFormatException e) {
326            channelId = CHANNEL_ID_NONE;
327        }
328        return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId);
329    }
330
331    private String getStorageKey() {
332        long prefix = System.currentTimeMillis();
333        int suffix = mRandom.nextInt();
334        return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix);
335    }
336
337    private void reset() {
338        if (mRecorder != null) {
339            mRecorder.release();
340            mRecorder = null;
341        }
342        if (mTunerSource != null) {
343            mSourceManager.releaseDataSource(mTunerSource);
344            mTunerSource = null;
345        }
346        mDvrStorageManager = null;
347        mSessionState = STATE_IDLE;
348        mRecorderRunning = false;
349    }
350
351    private boolean doTune(Uri channelUri) {
352        if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) {
353            mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
354            Log.e(TAG, "Tuning was requested from wrong status.");
355            return false;
356        }
357        mChannel = getChannel(channelUri);
358        if (mChannel == null) {
359            mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
360            Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel);
361            return false;
362        } else if (mChannel.isRecordingProhibited()) {
363            mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
364            Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel);
365            return false;
366        }
367        if (!mDvrStorageStatusManager.isStorageSufficient()) {
368            mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
369            Log.w(TAG, "Tuning failed due to insufficient storage.");
370            return false;
371        }
372        mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this);
373        if (mTunerSource == null) {
374            // Retry tuning in this case.
375            mSessionState = STATE_TUNING;
376            return true;
377        }
378        mSessionState = STATE_TUNED;
379        return true;
380    }
381
382    private boolean doStartRecording(@Nullable Uri programUri) {
383        if (mSessionState != STATE_TUNED) {
384            mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
385            Log.e(TAG, "Recording session status abnormal");
386            return false;
387        }
388        mStorageDir = mDvrStorageStatusManager.isStorageSufficient() ?
389                new File(mDvrStorageStatusManager.getRecordingRootDataDirectory(),
390                        getStorageKey()) : null;
391        if (mStorageDir == null) {
392            mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
393            Log.w(TAG, "Failed to start recording due to insufficient storage.");
394            return false;
395        }
396        // Since tuning might be happened a while ago, shifts the start position of tuned source.
397        mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition());
398        mRecordStartTime = System.currentTimeMillis();
399        mDvrStorageManager = new DvrStorageManager(mStorageDir, true);
400        mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource,
401                new BufferManager(mDvrStorageManager), this, true);
402        mRecorder.setOnCompletionListener(this, mHandler);
403        mProgramUri = programUri;
404        mSessionState = STATE_RECORDING;
405        mRecorderRunning = true;
406        mHandler.sendEmptyMessage(MSG_PREPARE_RECODER);
407        mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
408        mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS,
409                STORAGE_MONITOR_INTERVAL_MS);
410        return true;
411    }
412
413    private void stopRecorder() {
414        // Do not change session status.
415        if (mRecorder != null) {
416            mRecorder.release();
417            mRecordEndTime = System.currentTimeMillis();
418            mRecorder = null;
419        }
420        mRecorderRunning = false;
421        mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
422        Log.i(TAG, "Recording stopped");
423    }
424
425    private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) {
426        if (mChannel == null || channel == null || mChannel.compareTo(channel) != 0
427                || items == null || items.isEmpty()) {
428            return;
429        }
430        PsipData.EitItem currentProgram = getCurrentProgram(items);
431        if (currentProgram == null || !currentProgram.hasCaptionTrack()
432                || mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0) {
433            return;
434        }
435        mCurrenProgram = currentProgram;
436        mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks());
437        if (DEBUG) {
438            Log.d(TAG, "updated " + mCaptionTracks.size() + " caption tracks for "
439                    + currentProgram);
440        }
441    }
442
443    private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) {
444        for (PsipData.EitItem item : items) {
445            if (mRecordStartTime >= item.getStartTimeUtcMillis()
446                    && mRecordStartTime < item.getEndTimeUtcMillis()) {
447                return item;
448            }
449        }
450        return null;
451    }
452
453    private static class Program {
454        private final long mChannelId;
455        private final String mTitle;
456        private String mSeriesId;
457        private final String mSeasonTitle;
458        private final String mEpisodeTitle;
459        private final String mSeasonNumber;
460        private final String mEpisodeNumber;
461        private final String mDescription;
462        private final String mPosterArtUri;
463        private final String mThumbnailUri;
464        private final String mCanonicalGenres;
465        private final String mContentRatings;
466        private final long mStartTimeUtcMillis;
467        private final long mEndTimeUtcMillis;
468        private final int mVideoWidth;
469        private final int mVideoHeight;
470        private final byte[] mInternalProviderData;
471
472        private static final String[] PROJECTION = {
473                TvContract.Programs.COLUMN_CHANNEL_ID,
474                TvContract.Programs.COLUMN_TITLE,
475                TvContract.Programs.COLUMN_SEASON_TITLE,
476                TvContract.Programs.COLUMN_EPISODE_TITLE,
477                TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
478                TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
479                TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
480                TvContract.Programs.COLUMN_POSTER_ART_URI,
481                TvContract.Programs.COLUMN_THUMBNAIL_URI,
482                TvContract.Programs.COLUMN_CANONICAL_GENRE,
483                TvContract.Programs.COLUMN_CONTENT_RATING,
484                TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
485                TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
486                TvContract.Programs.COLUMN_VIDEO_WIDTH,
487                TvContract.Programs.COLUMN_VIDEO_HEIGHT,
488                TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA
489        };
490
491        public Program(Cursor cursor) {
492            int index = 0;
493            mChannelId = cursor.getLong(index++);
494            mTitle = cursor.getString(index++);
495            mSeasonTitle = cursor.getString(index++);
496            mEpisodeTitle = cursor.getString(index++);
497            mSeasonNumber = cursor.getString(index++);
498            mEpisodeNumber = cursor.getString(index++);
499            mDescription = cursor.getString(index++);
500            mPosterArtUri = cursor.getString(index++);
501            mThumbnailUri = cursor.getString(index++);
502            mCanonicalGenres = cursor.getString(index++);
503            mContentRatings = cursor.getString(index++);
504            mStartTimeUtcMillis = cursor.getLong(index++);
505            mEndTimeUtcMillis = cursor.getLong(index++);
506            mVideoWidth = cursor.getInt(index++);
507            mVideoHeight = cursor.getInt(index++);
508            mInternalProviderData = cursor.getBlob(index++);
509            SoftPreconditions.checkArgument(index == PROJECTION.length);
510        }
511
512        public Program(long channelId) {
513            mChannelId = channelId;
514            mTitle = "Unknown";
515            mSeasonTitle = "";
516            mEpisodeTitle = "";
517            mSeasonNumber = "";
518            mEpisodeNumber = "";
519            mDescription = "Unknown";
520            mPosterArtUri = null;
521            mThumbnailUri = null;
522            mCanonicalGenres = null;
523            mContentRatings = null;
524            mStartTimeUtcMillis = 0;
525            mEndTimeUtcMillis = 0;
526            mVideoWidth = 0;
527            mVideoHeight = 0;
528            mInternalProviderData = null;
529        }
530
531        public static Program onQuery(Cursor c) {
532            Program program = null;
533            if (c != null && c.moveToNext()) {
534                program = new Program(c);
535            }
536            return program;
537        }
538
539        public ContentValues buildValues() {
540            ContentValues values = new ContentValues();
541            int index = 0;
542            values.put(PROJECTION[index++], mChannelId);
543            values.put(PROJECTION[index++], mTitle);
544            values.put(PROJECTION[index++], mSeasonTitle);
545            values.put(PROJECTION[index++], mEpisodeTitle);
546            values.put(PROJECTION[index++], mSeasonNumber);
547            values.put(PROJECTION[index++], mEpisodeNumber);
548            values.put(PROJECTION[index++], mDescription);
549            values.put(PROJECTION[index++], mPosterArtUri);
550            values.put(PROJECTION[index++], mThumbnailUri);
551            values.put(PROJECTION[index++], mCanonicalGenres);
552            values.put(PROJECTION[index++], mContentRatings);
553            values.put(PROJECTION[index++], mStartTimeUtcMillis);
554            values.put(PROJECTION[index++], mEndTimeUtcMillis);
555            values.put(PROJECTION[index++], mVideoWidth);
556            values.put(PROJECTION[index++], mVideoHeight);
557            values.put(PROJECTION[index++], mInternalProviderData);
558            SoftPreconditions.checkArgument(index == PROJECTION.length);
559            return values;
560        }
561    }
562
563    private Program getRecordedProgram() {
564        ContentResolver resolver = mContext.getContentResolver();
565        Uri programUri = mProgramUri;
566        if (mProgramUri == null) {
567            long avg = mRecordStartTime / 2 + mRecordEndTime / 2;
568            programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg);
569        }
570        try (Cursor c = resolver.query(programUri, Program.PROJECTION, null, null, SORT_BY_TIME)) {
571            if (c != null) {
572                Program result = Program.onQuery(c);
573                if (DEBUG) {
574                    Log.v(TAG, "Finished query for " + this);
575                }
576                return result;
577            } else {
578                if (c == null) {
579                    Log.e(TAG, "Unknown query error for " + this);
580                } else {
581                    if (DEBUG) Log.d(TAG, "Canceled query for " + this);
582                }
583                return null;
584            }
585        }
586    }
587
588    private Uri insertRecordedProgram(Program program, long channelId, String storageUri,
589            long totalBytes, long startTime, long endTime) {
590        // TODO: Set title even though program is null.
591        RecordedProgram recordedProgram = RecordedProgram.builder()
592                .setInputId(mInputId)
593                .setChannelId(channelId)
594                .setDataUri(storageUri)
595                .setDurationMillis(endTime - startTime)
596                .setDataBytes(totalBytes)
597                // startTime and endTime could be overridden by program's start and end value.
598                .setStartTimeUtcMillis(startTime)
599                .setEndTimeUtcMillis(endTime)
600                .build();
601        ContentValues values = RecordedProgram.toValues(recordedProgram);
602        if (program != null) {
603            values.putAll(program.buildValues());
604        }
605        return mContext.getContentResolver().insert(TvContract.RecordedPrograms.CONTENT_URI,
606                values);
607    }
608
609    private void onRecordingResult(boolean success, long lastExtractedPositionUs) {
610        if (mSessionState != STATE_RECORDING) {
611            // Error notification is not needed.
612            Log.e(TAG, "Recording session status abnormal");
613            return;
614        }
615        if (mRecorderRunning) {
616            // In case of recorder not being stopped, because of premature termination of recording.
617            stopRecorder();
618        }
619        if (!success && lastExtractedPositionUs <
620                TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) {
621            new DeleteRecordingTask().execute(mStorageDir);
622            mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
623            Log.w(TAG, "Recording failed during recording");
624            return;
625        }
626        Log.i(TAG, "recording finished " + (success ? "completely" : "partially"));
627        long recordEndTime =
628                (lastExtractedPositionUs == C.UNKNOWN_TIME_US)
629                        ? System.currentTimeMillis()
630                        : mRecordStartTime + lastExtractedPositionUs / 1000;
631        Uri uri =
632                insertRecordedProgram(
633                        getRecordedProgram(),
634                        mChannel.getChannelId(),
635                        Uri.fromFile(mStorageDir).toString(),
636                        1024 * 1024,
637                        mRecordStartTime,
638                        recordEndTime);
639        if (uri == null) {
640            new DeleteRecordingTask().execute(mStorageDir);
641            mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
642            Log.e(TAG, "Inserting a recording to DB failed");
643            return;
644        }
645        mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks);
646        mSession.onRecordFinished(uri);
647    }
648
649    private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> {
650
651        @Override
652        public Void doInBackground(File... files) {
653            if (files == null || files.length == 0) {
654                return null;
655            }
656            for(File file : files) {
657                Utils.deleteDirOrFile(file);
658            }
659            return null;
660        }
661    }
662}
663