• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.tuner.tvinput;
18 
19 import static com.android.tv.tuner.features.TunerFeatures.TVPROVIDER_ALLOWS_COLUMN_CREATION;
20 
21 import android.content.ContentResolver;
22 import android.content.ContentUris;
23 import android.content.ContentValues;
24 import android.content.Context;
25 import android.database.Cursor;
26 import android.media.tv.TvContract;
27 import android.media.tv.TvContract.RecordedPrograms;
28 import android.media.tv.TvInputManager;
29 import android.net.Uri;
30 import android.os.AsyncTask;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.os.HandlerThread;
34 import android.os.Message;
35 import androidx.annotation.IntDef;
36 import androidx.annotation.MainThread;
37 import androidx.annotation.Nullable;
38 import android.util.Log;
39 import android.util.Pair;
40 
41 import androidx.tvprovider.media.tv.Program;
42 
43 import com.android.tv.common.BaseApplication;
44 import com.android.tv.common.data.RecordedProgramState;
45 import com.android.tv.common.recording.RecordingCapability;
46 import com.android.tv.common.recording.RecordingStorageStatusManager;
47 import com.android.tv.common.util.CommonUtils;
48 import com.android.tv.tuner.data.PsipData;
49 import com.android.tv.tuner.data.PsipData.EitItem;
50 import com.android.tv.tuner.data.Track.AtscCaptionTrack;
51 import com.android.tv.tuner.data.TunerChannel;
52 import com.android.tv.tuner.dvb.DvbDeviceAccessor;
53 import com.android.tv.tuner.exoplayer2.ExoPlayerSampleExtractor;
54 import com.android.tv.tuner.exoplayer2.SampleExtractor;
55 import com.android.tv.tuner.exoplayer2.buffer.BufferManager;
56 import com.android.tv.tuner.exoplayer2.buffer.DvrStorageManager;
57 import com.android.tv.tuner.exoplayer2.buffer.PlaybackBufferListener;
58 import com.android.tv.tuner.source.TsDataSource;
59 import com.android.tv.tuner.source.TsDataSourceManager;
60 import com.android.tv.tuner.ts.EventDetector.EventListener;
61 import com.android.tv.tuner.tvinput.datamanager.ChannelDataManager;
62 
63 import com.google.android.exoplayer2.C;
64 import com.google.auto.factory.AutoFactory;
65 import com.google.auto.factory.Provided;
66 
67 import java.io.File;
68 import java.io.IOException;
69 import java.lang.annotation.Retention;
70 import java.lang.annotation.RetentionPolicy;
71 import java.util.ArrayList;
72 import java.util.Arrays;
73 import java.util.Collections;
74 import java.util.HashSet;
75 import java.util.List;
76 import java.util.Locale;
77 import java.util.Random;
78 import java.util.Set;
79 import java.util.concurrent.TimeUnit;
80 
81 /** Implements a DVR feature. */
82 public class TunerRecordingSessionWorkerExoV2
83         implements PlaybackBufferListener,
84                 EventListener,
85                 SampleExtractor.OnCompletionListener,
86                 SampleExtractor.Callback,
87                 Handler.Callback {
88     private static final String TAG = "TunerRecordingSWExoV2";
89     private static final boolean DEBUG = false;
90 
91     private static final String SORT_BY_TIME =
92             TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS
93                     + ", "
94                     + TvContract.Programs.COLUMN_CHANNEL_ID
95                     + ", "
96                     + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS;
97     private static final long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
98     private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
99     private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
100     private static final long PREPARE_RECORDER_POLL_MS = 50;
101     private static final int MSG_TUNE = 1;
102     private static final int MSG_START_RECORDING = 2;
103     private static final int MSG_PREPARE_RECODER = 3;
104     private static final int MSG_STOP_RECORDING = 4;
105     private static final int MSG_MONITOR_STORAGE_STATUS = 5;
106     private static final int MSG_RELEASE = 6;
107     private static final int MSG_UPDATE_CC_INFO = 7;
108     private static final int MSG_UPDATE_PARTIAL_STATE = 8;
109     private static final String COLUMN_SERIES_ID = "series_id";
110     private static final String COLUMN_STATE = "state";
111 
112     private boolean mProgramHasSeriesIdColumn;
113     private boolean mRecordedProgramHasSeriesIdColumn;
114     private boolean mRecordedProgramHasStateColumn;
115 
116     private final RecordingCapability mCapabilities;
117 
118     private static final String[] PROGRAM_PROJECTION = {
119         TvContract.Programs.COLUMN_CHANNEL_ID,
120         TvContract.Programs.COLUMN_TITLE,
121         TvContract.Programs.COLUMN_SEASON_TITLE,
122         TvContract.Programs.COLUMN_EPISODE_TITLE,
123         TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
124         TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
125         TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
126         TvContract.Programs.COLUMN_POSTER_ART_URI,
127         TvContract.Programs.COLUMN_THUMBNAIL_URI,
128         TvContract.Programs.COLUMN_CANONICAL_GENRE,
129         TvContract.Programs.COLUMN_CONTENT_RATING,
130         TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
131         TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
132         TvContract.Programs.COLUMN_VIDEO_WIDTH,
133         TvContract.Programs.COLUMN_VIDEO_HEIGHT,
134         TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA
135     };
136 
137     private static final String[] PROGRAM_PROJECTION_WITH_SERIES_ID =
138             createProjectionWithSeriesId();
139 
140     @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING})
141     @Retention(RetentionPolicy.SOURCE)
142     public @interface DvrSessionState {}
143 
144     private static final int STATE_IDLE = 1;
145     private static final int STATE_TUNING = 2;
146     private static final int STATE_TUNED = 3;
147     private static final int STATE_RECORDING = 4;
148 
149     private static final long CHANNEL_ID_NONE = -1;
150     private static final int MAX_TUNING_RETRY = 6;
151 
152     private final Context mContext;
153     private final ChannelDataManager mChannelDataManager;
154     private final RecordingStorageStatusManager mRecordingStorageStatusManager;
155     private final Handler mHandler;
156     private final TsDataSourceManager mSourceManager;
157     private final Random mRandom = new Random();
158 
159     private TsDataSource mTunerSource;
160     private TunerChannel mChannel;
161     private File mStorageDir;
162     private long mRecordStartTime;
163     private long mRecordEndTime;
164     private Uri mRecordedProgramUri;
165     private boolean mRecorderRunning;
166     private SampleExtractor mRecorder;
167     private final TunerRecordingSessionExoV2 mSession;
168     @DvrSessionState private int mSessionState = STATE_IDLE;
169     private final String mInputId;
170     private Uri mProgramUri;
171     private String mSeriesId;
172 
173     private PsipData.EitItem mCurrenProgram;
174     private List<AtscCaptionTrack> mCaptionTracks;
175     private DvrStorageManager mDvrStorageManager;
176     private final ExoPlayerSampleExtractor.Factory mExoPlayerSampleExtractorFactory;
177 
178     /**
179      * Factory for {@link TunerRecordingSessionWorkerExoV2}}.
180      *
181      * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory}
182      * generated class.
183      */
184     public interface Factory {
create( Context context, String inputId, ChannelDataManager dataManager, TunerRecordingSessionExoV2 session)185         TunerRecordingSessionWorkerExoV2 create(
186                 Context context,
187                 String inputId,
188                 ChannelDataManager dataManager,
189                 TunerRecordingSessionExoV2 session);
190     }
191 
192     @AutoFactory(implementing = Factory.class)
TunerRecordingSessionWorkerExoV2( Context context, String inputId, ChannelDataManager dataManager, TunerRecordingSessionExoV2 session, @Provided ExoPlayerSampleExtractor.Factory exoPlayerSampleExtractorFactory, @Provided TsDataSourceManager.Factory tsDataSourceManagerFactory)193     public TunerRecordingSessionWorkerExoV2(
194             Context context,
195             String inputId,
196             ChannelDataManager dataManager,
197             TunerRecordingSessionExoV2 session,
198             @Provided ExoPlayerSampleExtractor.Factory exoPlayerSampleExtractorFactory,
199             @Provided TsDataSourceManager.Factory tsDataSourceManagerFactory) {
200         mExoPlayerSampleExtractorFactory = exoPlayerSampleExtractorFactory;
201         mRandom.setSeed(System.nanoTime());
202         mContext = context;
203         HandlerThread handlerThread = new HandlerThread(TAG);
204         handlerThread.start();
205         mHandler = new Handler(handlerThread.getLooper(), this);
206         mRecordingStorageStatusManager =
207                 BaseApplication.getSingletons(context).getRecordingStorageStatusManager();
208         mChannelDataManager = dataManager;
209         mChannelDataManager.checkDataVersion(context);
210         mSourceManager = tsDataSourceManagerFactory.create(true);
211         mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId);
212         mInputId = inputId;
213         if (DEBUG) Log.d(TAG, mCapabilities.toString());
214         mSession = session;
215     }
216 
217     // PlaybackBufferListener
218     @Override
onBufferStartTimeChanged(long startTimeMs)219     public void onBufferStartTimeChanged(long startTimeMs) {}
220 
221     @Override
onBufferStateChanged(boolean available)222     public void onBufferStateChanged(boolean available) {}
223 
224     @Override
onDiskTooSlow()225     public void onDiskTooSlow() {}
226 
227     // EventDetector.EventListener
228     @Override
onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime)229     public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
230         if (mChannel == null || mChannel.compareTo(channel) != 0) {
231             return;
232         }
233         mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime);
234     }
235 
236     @Override
onEventDetected(TunerChannel channel, List<PsipData.EitItem> items)237     public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
238         if (mChannel == null || mChannel.compareTo(channel) != 0) {
239             return;
240         }
241         mHandler.obtainMessage(MSG_UPDATE_CC_INFO, Pair.create(channel, items)).sendToTarget();
242         mChannelDataManager.notifyEventDetected(channel, items);
243     }
244 
245     @Override
onChannelScanDone()246     public void onChannelScanDone() {
247         // do nothing.
248     }
249 
250     // SampleExtractor.OnCompletionListener
251     @Override
onCompletion(boolean success, long lastExtractedPositionUs)252     public void onCompletion(boolean success, long lastExtractedPositionUs) {
253         onRecordingResult(success, lastExtractedPositionUs);
254         reset();
255     }
256 
257     /** Tunes to {@code channelUri}. */
258     @MainThread
tune(Uri channelUri)259     public void tune(Uri channelUri) {
260         mHandler.removeCallbacksAndMessages(null);
261         mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget();
262     }
263 
264     /** Starts recording. */
265     @MainThread
startRecording(@ullable Uri programUri)266     public void startRecording(@Nullable Uri programUri) {
267         mHandler.obtainMessage(MSG_START_RECORDING, programUri).sendToTarget();
268     }
269 
270     /** Stops recording. */
271     @MainThread
stopRecording()272     public void stopRecording() {
273         mHandler.sendEmptyMessage(MSG_STOP_RECORDING);
274     }
275 
276     /** Releases all resources. */
277     @MainThread
release()278     public void release() {
279         mHandler.removeCallbacksAndMessages(null);
280         mHandler.sendEmptyMessage(MSG_RELEASE);
281     }
282 
283     @Override
handleMessage(Message msg)284     public boolean handleMessage(Message msg) {
285         switch (msg.what) {
286             case MSG_TUNE:
287                 {
288                     Uri channelUri = (Uri) msg.obj;
289                     int retryCount = msg.arg1;
290                     if (DEBUG) Log.d(TAG, "Tune to " + channelUri);
291                     if (doTune(channelUri)) {
292                         if (mSessionState == STATE_TUNED) {
293                             mSession.onTuned(channelUri);
294                         } else {
295                             Log.w(TAG, "Tuner stream cannot be created due to resource shortage.");
296                             if (retryCount < MAX_TUNING_RETRY) {
297                                 Message tuneMsg =
298                                         mHandler.obtainMessage(
299                                                 MSG_TUNE, retryCount + 1, 0, channelUri);
300                                 mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS);
301                             } else {
302                                 mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
303                                 reset();
304                             }
305                         }
306                     }
307                     return true;
308                 }
309             case MSG_START_RECORDING:
310                 {
311                     if (DEBUG) Log.d(TAG, "Start recording");
312                     if (!doStartRecording((Uri) msg.obj)) {
313                         reset();
314                     }
315                     return true;
316                 }
317             case MSG_PREPARE_RECODER:
318                 {
319                     if (DEBUG) Log.d(TAG, "Preparing recorder");
320                     if (!mRecorderRunning) {
321                         return true;
322                     }
323                     try {
324                         mRecorder.prepare(this);
325                     } catch (IOException e) {
326                         Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor");
327                         mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
328                         reset();
329                     }
330                     return true;
331                 }
332             case MSG_STOP_RECORDING:
333                 {
334                     if (DEBUG) Log.d(TAG, "Stop recording");
335                     if (mSessionState != STATE_RECORDING) {
336                         mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
337                         reset();
338                         return true;
339                     }
340                     if (mRecorderRunning) {
341                         stopRecorder();
342                     }
343                     return true;
344                 }
345             case MSG_MONITOR_STORAGE_STATUS:
346                 {
347                     if (mSessionState != STATE_RECORDING) {
348                         return true;
349                     }
350                     if (!mRecordingStorageStatusManager.isStorageSufficient()) {
351                         if (mRecorderRunning) {
352                             stopRecorder();
353                         }
354                         new DeleteRecordingTask().execute(mStorageDir);
355                         mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
356                         mContext.getContentResolver().delete(mRecordedProgramUri, null, null);
357                         reset();
358                     } else {
359                         mHandler.sendEmptyMessageDelayed(
360                                 MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS);
361                     }
362                     return true;
363                 }
364             case MSG_RELEASE:
365                 {
366                     // Since release was requested, current recording will be cancelled
367                     // without notification.
368                     reset();
369                     mSourceManager.release();
370                     mHandler.removeCallbacksAndMessages(null);
371                     mHandler.getLooper().quitSafely();
372                     return true;
373                 }
374             case MSG_UPDATE_CC_INFO:
375                 {
376                     Pair<TunerChannel, List<EitItem>> pair =
377                             (Pair<TunerChannel, List<EitItem>>) msg.obj;
378                     updateCaptionTracks(pair.first, pair.second);
379                     return true;
380                 }
381             case MSG_UPDATE_PARTIAL_STATE:
382                 {
383                     updateRecordedProgramStatePartial();
384                     return true;
385                 }
386         }
387         return false;
388     }
389 
390     @Override
onPrepared()391     public void onPrepared() {
392         // Do nothing
393     }
394 
395     @Nullable
getChannel(Uri channelUri)396     private TunerChannel getChannel(Uri channelUri) {
397         if (channelUri == null) {
398             return null;
399         }
400         long channelId;
401         try {
402             channelId = ContentUris.parseId(channelUri);
403         } catch (UnsupportedOperationException | NumberFormatException e) {
404             channelId = CHANNEL_ID_NONE;
405         }
406         return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId);
407     }
408 
getStorageKey()409     private String getStorageKey() {
410         long prefix = System.currentTimeMillis();
411         int suffix = mRandom.nextInt();
412         return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix);
413     }
414 
reset()415     private void reset() {
416         if (mRecorder != null) {
417             mRecorder.release();
418             mRecorder = null;
419         }
420         if (mTunerSource != null) {
421             mSourceManager.releaseDataSource(mTunerSource);
422             mTunerSource = null;
423         }
424         mDvrStorageManager = null;
425         mSessionState = STATE_IDLE;
426         mRecorderRunning = false;
427     }
428 
doTune(Uri channelUri)429     private boolean doTune(Uri channelUri) {
430         if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) {
431             mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
432             Log.e(TAG, "Tuning was requested from wrong status.");
433             return false;
434         }
435         mChannel = getChannel(channelUri);
436         if (mChannel == null) {
437             mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
438             Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel);
439             return false;
440         } else if (mChannel.isRecordingProhibited()) {
441             mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
442             Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel);
443             return false;
444         }
445         if (!mRecordingStorageStatusManager.isStorageSufficient()) {
446             mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
447             Log.w(TAG, "Tuning failed due to insufficient storage.");
448             return false;
449         }
450         mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this);
451         if (mTunerSource == null) {
452             // Retry tuning in this case.
453             mSessionState = STATE_TUNING;
454             return true;
455         }
456         mSessionState = STATE_TUNED;
457         return true;
458     }
459 
doStartRecording(@ullable Uri programUri)460     private boolean doStartRecording(@Nullable Uri programUri) {
461         if (mSessionState != STATE_TUNED) {
462             mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
463             Log.e(TAG, "Recording session status abnormal");
464             return false;
465         }
466         mStorageDir =
467                 mRecordingStorageStatusManager.isStorageSufficient()
468                         ? new File(
469                                 mRecordingStorageStatusManager.getRecordingRootDataDirectory(),
470                                 getStorageKey())
471                         : null;
472         if (mStorageDir == null) {
473             mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
474             Log.w(TAG, "Failed to start recording due to insufficient storage.");
475             return false;
476         }
477         // Since tuning might be happened a while ago, shifts the start position of tuned source.
478         mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition());
479         mRecordStartTime = System.currentTimeMillis();
480         mDvrStorageManager = new DvrStorageManager(mStorageDir, true);
481         mRecorder =
482                 mExoPlayerSampleExtractorFactory.create(
483                         Uri.EMPTY, mTunerSource, new BufferManager(mDvrStorageManager), this, true);
484         mRecorder.setOnCompletionListener(this, mHandler);
485         mProgramUri = programUri;
486         mSessionState = STATE_RECORDING;
487         mRecorderRunning = true;
488         mRecordedProgramUri =
489                 insertRecordedProgram(
490                         getRecordedProgram(),
491                         mChannel.getChannelId(),
492                         Uri.fromFile(mStorageDir).toString(),
493                         calculateRecordingSizeInBytes(),
494                         mRecordStartTime,
495                         mRecordStartTime);
496         if (mRecordedProgramUri == null) {
497             new DeleteRecordingTask().execute(mStorageDir);
498             mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
499             Log.e(TAG, "Inserting a recording to DB failed");
500             return false;
501         }
502         mSession.onRecordingUri(mRecordedProgramUri.toString());
503         mHandler.sendEmptyMessageDelayed(
504                 MSG_UPDATE_PARTIAL_STATE, MIN_PARTIAL_RECORDING_DURATION_MS);
505 
506         mHandler.sendEmptyMessage(MSG_PREPARE_RECODER);
507         mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
508         mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS);
509         return true;
510     }
511 
calculateRecordingSizeInBytes()512     private int calculateRecordingSizeInBytes() {
513         // TODO(b/121153491): calcute recording size using mStorageDir
514         return 1024 * 1024;
515     }
516 
stopRecorder()517     private void stopRecorder() {
518         // Do not change session status.
519         if (mRecorder != null) {
520             mRecorder.release();
521             mRecordEndTime = System.currentTimeMillis();
522             mRecorder = null;
523         }
524         mRecorderRunning = false;
525         mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS);
526         Log.i(TAG, "Recording stopped");
527     }
528 
updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items)529     private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) {
530         if (mChannel == null
531                 || channel == null
532                 || mChannel.compareTo(channel) != 0
533                 || items == null
534                 || items.isEmpty()) {
535             return;
536         }
537         PsipData.EitItem currentProgram = getCurrentProgram(items);
538         if (currentProgram == null
539                 || !currentProgram.hasCaptionTrack()
540                 || (mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0)) {
541             return;
542         }
543         mCurrenProgram = currentProgram;
544         mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks());
545         if (DEBUG) {
546             Log.d(
547                     TAG,
548                     "updated " + mCaptionTracks.size() + " caption tracks for " + currentProgram);
549         }
550     }
551 
getCurrentProgram(List<PsipData.EitItem> items)552     private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) {
553         for (PsipData.EitItem item : items) {
554             if (mRecordStartTime >= item.getStartTimeUtcMillis()
555                     && mRecordStartTime < item.getEndTimeUtcMillis()) {
556                 return item;
557             }
558         }
559         return null;
560     }
561 
getRecordedProgram()562     private Program getRecordedProgram() {
563         ContentResolver resolver = mContext.getContentResolver();
564         Uri programUri = mProgramUri;
565         if (mProgramUri == null) {
566             long avg = mRecordStartTime / 2 + mRecordEndTime / 2;
567             programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg);
568         }
569         String[] projection =
570                 checkProgramTable() ? PROGRAM_PROJECTION_WITH_SERIES_ID : PROGRAM_PROJECTION;
571         try (Cursor c = resolver.query(programUri, projection, null, null, SORT_BY_TIME)) {
572             if (c != null && c.moveToNext()) {
573                 Program result = Program.fromCursor(c);
574                 int index;
575                 if ((index = c.getColumnIndex(COLUMN_SERIES_ID)) >= 0 && !c.isNull(index)) {
576                     mSeriesId = c.getString(index);
577                 }
578                 if (DEBUG) {
579                     Log.v(TAG, "Finished query for " + this);
580                 }
581                 return result;
582             } else {
583                 if (c == null) {
584                     Log.e(TAG, "Unknown query error for " + this);
585                 } else {
586                     if (DEBUG) Log.d(TAG, "Can not find program:" + programUri);
587                 }
588                 return null;
589             }
590         }
591     }
592 
insertRecordedProgram( Program program, long channelId, String storageUri, long totalBytes, long startTime, long endTime)593     private Uri insertRecordedProgram(
594             Program program,
595             long channelId,
596             String storageUri,
597             long totalBytes,
598             long startTime,
599             long endTime) {
600         ContentValues values = new ContentValues();
601         values.put(RecordedPrograms.COLUMN_INPUT_ID, mInputId);
602         values.put(RecordedPrograms.COLUMN_CHANNEL_ID, channelId);
603         values.put(RecordedPrograms.COLUMN_RECORDING_DATA_URI, storageUri);
604         values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - startTime);
605         values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
606         // startTime could be overridden by program's start value.
607         values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime);
608         values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
609         if (checkRecordedProgramTable(COLUMN_SERIES_ID)) {
610             values.put(COLUMN_SERIES_ID, mSeriesId);
611         }
612         if (checkRecordedProgramTable(COLUMN_STATE)) {
613             values.put(COLUMN_STATE, RecordedProgramState.STARTED.name());
614         }
615         if (program != null) {
616             values.putAll(program.toContentValues());
617         }
618         return mContext.getContentResolver()
619                 .insert(TvContract.RecordedPrograms.CONTENT_URI, values);
620     }
621 
updateRecordedProgramStateFinished(long endTime, long totalBytes)622     private void updateRecordedProgramStateFinished(long endTime, long totalBytes) {
623         ContentValues values = new ContentValues();
624         values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, totalBytes);
625         values.put(RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - mRecordStartTime);
626         values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
627         if (checkRecordedProgramTable(COLUMN_STATE)) {
628             values.put(COLUMN_STATE, RecordedProgramState.FINISHED.name());
629         }
630         mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
631     }
632 
updateRecordedProgramStatePartial()633     private void updateRecordedProgramStatePartial() {
634         mSession.onRecordingStatePartial(mRecordedProgramUri);
635         if (checkRecordedProgramTable(COLUMN_STATE)) {
636             ContentValues values = new ContentValues();
637             values.put(COLUMN_STATE, RecordedProgramState.PARTIAL.name());
638             mContext.getContentResolver().update(mRecordedProgramUri, values, null, null);
639         }
640     }
641 
onRecordingResult(boolean success, long lastExtractedPositionUs)642     private void onRecordingResult(boolean success, long lastExtractedPositionUs) {
643         if (mSessionState != STATE_RECORDING) {
644             // Error notification is not needed.
645             Log.e(TAG, "Recording session status abnormal");
646             return;
647         }
648         if (mRecorderRunning) {
649             // In case of recorder not being stopped, because of premature termination of recording.
650             stopRecorder();
651         }
652         if (!success
653                 && lastExtractedPositionUs
654                         < TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) {
655             new DeleteRecordingTask().execute(mStorageDir);
656             mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
657             mContext.getContentResolver().delete(mRecordedProgramUri, null, null);
658             Log.w(TAG, "Recording failed during recording");
659             return;
660         }
661         Log.i(TAG, "recording finished " + (success ? "completely" : "partially"));
662         long recordEndTime =
663                 (lastExtractedPositionUs == C.TIME_UNSET)
664                         ? System.currentTimeMillis()
665                         : mRecordStartTime + lastExtractedPositionUs / 1000;
666         updateRecordedProgramStateFinished(recordEndTime, calculateRecordingSizeInBytes());
667         mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks);
668         mSession.onRecordFinished(mRecordedProgramUri);
669     }
670 
checkProgramTable()671     private boolean checkProgramTable() {
672         boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext);
673         if (!canCreateColumn) {
674             return false;
675         }
676         Uri uri = TvContract.Programs.CONTENT_URI;
677         if (!mProgramHasSeriesIdColumn) {
678             if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) {
679                 mProgramHasSeriesIdColumn = true;
680             } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) {
681                 mProgramHasSeriesIdColumn = true;
682             }
683         }
684         return mProgramHasSeriesIdColumn;
685     }
686 
checkRecordedProgramTable(String column)687     private boolean checkRecordedProgramTable(String column) {
688         boolean canCreateColumn = TVPROVIDER_ALLOWS_COLUMN_CREATION.isEnabled(mContext);
689         if (!canCreateColumn) {
690             return false;
691         }
692         Uri uri = TvContract.RecordedPrograms.CONTENT_URI;
693         switch (column) {
694             case COLUMN_SERIES_ID:
695                 {
696                     if (!mRecordedProgramHasSeriesIdColumn) {
697                         if (getExistingColumns(uri).contains(COLUMN_SERIES_ID)) {
698                             mRecordedProgramHasSeriesIdColumn = true;
699                         } else if (addColumnToTable(uri, COLUMN_SERIES_ID)) {
700                             mRecordedProgramHasSeriesIdColumn = true;
701                         }
702                     }
703                     return mRecordedProgramHasSeriesIdColumn;
704                 }
705             case COLUMN_STATE:
706                 {
707                     if (!mRecordedProgramHasStateColumn) {
708                         if (getExistingColumns(uri).contains(COLUMN_STATE)) {
709                             mRecordedProgramHasStateColumn = true;
710                         } else if (addColumnToTable(uri, COLUMN_STATE)) {
711                             mRecordedProgramHasStateColumn = true;
712                         }
713                     }
714                     return mRecordedProgramHasStateColumn;
715                 }
716             default:
717                 return false;
718         }
719     }
720 
getExistingColumns(Uri uri)721     private Set<String> getExistingColumns(Uri uri) {
722         Bundle result =
723                 mContext.getContentResolver()
724                         .call(uri, TvContract.METHOD_GET_COLUMNS, uri.toString(), null);
725         if (result != null) {
726             String[] columns = result.getStringArray(TvContract.EXTRA_EXISTING_COLUMN_NAMES);
727             if (columns != null) {
728                 return new HashSet<>(Arrays.asList(columns));
729             }
730         }
731         Log.e(TAG, "Query existing column names from " + uri + " returned null");
732         return Collections.emptySet();
733     }
734 
735     /**
736      * Add a column to the table
737      *
738      * @return {@code true} if the column is added successfully; {@code false} otherwise.
739      */
addColumnToTable(Uri contentUri, String columnName)740     private boolean addColumnToTable(Uri contentUri, String columnName) {
741         Bundle extra = new Bundle();
742         extra.putCharSequence(TvContract.EXTRA_COLUMN_NAME, columnName);
743         extra.putCharSequence(TvContract.EXTRA_DATA_TYPE, "TEXT");
744         // If the add operation fails, the following just returns null without crashing.
745         Bundle allColumns =
746                 mContext.getContentResolver()
747                         .call(
748                                 contentUri,
749                                 TvContract.METHOD_ADD_COLUMN,
750                                 contentUri.toString(),
751                                 extra);
752         if (allColumns == null) {
753             Log.w(TAG, "Adding new column failed. Uri=" + contentUri);
754         }
755         return allColumns != null;
756     }
757 
createProjectionWithSeriesId()758     private static String[] createProjectionWithSeriesId() {
759         List<String> projectionList = new ArrayList<>(Arrays.asList(PROGRAM_PROJECTION));
760         projectionList.add(COLUMN_SERIES_ID);
761         return projectionList.toArray(new String[0]);
762     }
763 
764     private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> {
765 
766         @Override
doInBackground(File... files)767         public Void doInBackground(File... files) {
768             if (files == null || files.length == 0) {
769                 return null;
770             }
771             for (File file : files) {
772                 if (!CommonUtils.deleteDirOrFile(file)) {
773                     Log.w(TAG, "Unable to delete recording data at " + file);
774                 }
775             }
776             return null;
777         }
778     }
779 }
780