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