diff options
author | Nick Chalko <nchalko@google.com> | 2016-10-26 14:03:09 -0700 |
---|---|---|
committer | Nick Chalko <nchalko@google.com> | 2016-10-31 10:36:49 -0700 |
commit | d41f0075a7d2ea826204e81fcec57d0aa57171a9 (patch) | |
tree | cb30cfbafd80e01d314868cdc36e783d39981119 /src | |
parent | 5e0ec06a797e3497da94390c63c7072de442695b (diff) | |
download | TV-d41f0075a7d2ea826204e81fcec57d0aa57171a9.tar.gz |
Sync to ub-tv-killing at 6f6e46557accb62c9548e4177d6005aa944dbf33
Change-Id: I873644d6d9d0110c981ef6075cb4019c16bbb94b
Diffstat (limited to 'src')
162 files changed, 8165 insertions, 3973 deletions
diff --git a/src/com/android/exoplayer/MediaFormatUtil.java b/src/com/android/exoplayer/MediaFormatUtil.java index 647f7dd9..d7a981f6 100644 --- a/src/com/android/exoplayer/MediaFormatUtil.java +++ b/src/com/android/exoplayer/MediaFormatUtil.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer; import android.support.annotation.Nullable; +import com.google.android.exoplayer.util.MimeTypes; + import java.nio.ByteBuffer; import java.util.ArrayList; @@ -29,7 +31,6 @@ public class MediaFormatUtil { * {@link android.media.MediaFormat} should be converted to be used with ExoPlayer. */ public static MediaFormat createMediaFormat(android.media.MediaFormat format) { - // TODO: Add test for this method. String mimeType = format.getString(android.media.MediaFormat.KEY_MIME); String language = getOptionalStringV16(format, android.media.MediaFormat.KEY_LANGUAGE); int maxInputSize = @@ -52,36 +53,17 @@ public class MediaFormatUtil { } long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) ? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US; + int pcmEncoding = MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT + : MediaFormat.NO_VALUE; MediaFormat mediaFormat = new MediaFormat(null, mimeType, MediaFormat.NO_VALUE, maxInputSize, durationUs, width, height, rotationDegrees, MediaFormat.NO_VALUE, channelCount, sampleRate, language, MediaFormat.OFFSET_SAMPLE_RELATIVE, - initializationData, false, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, - MediaFormat.NO_VALUE, encoderDelay, encoderPadding); + initializationData, false, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, pcmEncoding, + encoderDelay, encoderPadding, null, MediaFormat.NO_VALUE); mediaFormat.setFrameworkFormatV16(format); return mediaFormat; } - /** - * Creates {@link MediaFormat} for audio track. - */ - public static MediaFormat createAudioMediaFormat(String mimeType, long durationUs, - int channelCount, int sampleRate) { - return MediaFormat.createAudioFormat(null, mimeType, MediaFormat.NO_VALUE, - MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, null, ""); - } - - /** - * Creates {@link MediaFormat} for closed caption track. - */ - public static MediaFormat createTextMediaFormat(String mimeType, long durationUs) { - return new MediaFormat(null, mimeType, 0, MediaFormat.NO_VALUE, durationUs, - MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, - MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, "", - MediaFormat.OFFSET_SAMPLE_RELATIVE, null, false, MediaFormat.NO_VALUE, - MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, - MediaFormat.NO_VALUE); - } - @Nullable private static String getOptionalStringV16(android.media.MediaFormat format, String key) { return format.containsKey(key) ? format.getString(key) : null; diff --git a/src/com/android/tv/ApplicationSingletons.java b/src/com/android/tv/ApplicationSingletons.java index 3f381635..fd125d52 100644 --- a/src/com/android/tv/ApplicationSingletons.java +++ b/src/com/android/tv/ApplicationSingletons.java @@ -24,6 +24,7 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.util.AccountHelper; import com.android.tv.util.TvInputManagerHelper; @@ -39,6 +40,8 @@ public interface ApplicationSingletons { DvrDataManager getDvrDataManager(); + DvrStorageStatusManager getDvrStorageStatusManager(); + DvrScheduleManager getDvrScheduleManager(); DvrManager getDvrManager(); diff --git a/src/com/android/tv/InputSessionManager.java b/src/com/android/tv/InputSessionManager.java index c3380d81..e4b0f456 100644 --- a/src/com/android/tv/InputSessionManager.java +++ b/src/com/android/tv/InputSessionManager.java @@ -33,6 +33,7 @@ import android.os.Looper; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; @@ -108,8 +109,8 @@ public class InputSessionManager { */ @NonNull public RecordingSession createRecordingSession(String inputId, String tag, - RecordingCallback callback, Handler handler) { - RecordingSession session = new RecordingSession(inputId, tag, callback, handler); + RecordingCallback callback, Handler handler, long endTimeMs) { + RecordingSession session = new RecordingSession(inputId, tag, callback, handler, endTimeMs); mRecordingSessions.add(session); if (DEBUG) Log.d(TAG, "Recording session created: " + session); return session; @@ -160,6 +161,24 @@ public class InputSessionManager { return null; } + /** + * Retruns the earliest end time of recording sessions in progress of the certain TV input. + */ + @MainThread + public Long getEarliestRecordingSessionEndTimeMs(String inputId) { + long timeMs = Long.MAX_VALUE; + synchronized (mRecordingSessions) { + for (RecordingSession session : mRecordingSessions) { + if (session.mTuned && TextUtils.equals(inputId, session.mInputId)) { + if (session.mEndTimeMs < timeMs) { + timeMs = session.mEndTimeMs; + } + } + } + } + return timeMs == Long.MAX_VALUE ? null : timeMs; + } + @MainThread int getTunedTvViewSessionCount(String inputId) { int tunedCount = 0; @@ -353,14 +372,17 @@ public class InputSessionManager { private Uri mChannelUri; private final RecordingCallback mCallback; private final Handler mHandler; + private volatile long mEndTimeMs; private TvRecordingClient mClient; private boolean mTuned; - RecordingSession(String inputId, String tag, RecordingCallback callback, Handler handler) { + RecordingSession(String inputId, String tag, RecordingCallback callback, + Handler handler, long endTimeMs) { mInputId = inputId; mCallback = callback; mHandler = handler; mClient = new TvRecordingClient(mContext, tag, callback, handler); + mEndTimeMs = endTimeMs; } void release() { @@ -406,6 +428,7 @@ public class InputSessionManager { }); return; } + mTuned = true; int tunedTuneSessionCount = getTunedTvViewSessionCount(inputId); if (!isTunedForTvView(channelUri) && tunedTuneSessionCount > 0 && tunedRecordingSessionCount + tunedTuneSessionCount @@ -419,7 +442,6 @@ public class InputSessionManager { } } mChannelUri = channelUri; - mTuned = true; mClient.tune(inputId, channelUri); } }); @@ -439,6 +461,13 @@ public class InputSessionManager { mClient.stopRecording(); } + /** + * Sets recording session's ending time. + */ + public void setEndTimeMs(long endTimeMs) { + mEndTimeMs = endTimeMs; + } + private void runOnHandler(Handler handler, Runnable runnable) { if (Looper.myLooper() == handler.getLooper()) { runnable.run(); diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java index b38b2911..58850b5f 100644 --- a/src/com/android/tv/MainActivity.java +++ b/src/com/android/tv/MainActivity.java @@ -94,9 +94,11 @@ import com.android.tv.data.epg.EpgFetcher; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.ConflictChecker; -import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.ui.DvrStopRecordingFragment; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.experiments.Experiments; import com.android.tv.menu.Menu; import com.android.tv.onboarding.OnboardingActivity; @@ -205,6 +207,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_VOLUME_MUTE); BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_MUTE); BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_SEARCH); + BLACKLIST_KEYCODE_TO_TIS.add(KeyEvent.KEYCODE_WINDOW); } @@ -231,10 +234,25 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST, UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO, UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK}) private @interface ChannelBannerUpdateReason {} + /** + * Updates channel banner because the channel banner is forced to show. + */ private static final int UPDATE_CHANNEL_BANNER_REASON_FORCE_SHOW = 1; + /** + * Updates channel banner because of tuning. + */ private static final int UPDATE_CHANNEL_BANNER_REASON_TUNE = 2; + /** + * Updates channel banner because of fast tuning. + */ private static final int UPDATE_CHANNEL_BANNER_REASON_TUNE_FAST = 3; + /** + * Updates channel banner because of info updating. + */ private static final int UPDATE_CHANNEL_BANNER_REASON_UPDATE_INFO = 4; + /** + * Updates channel banner because the current watched channel is locked or unlocked. + */ private static final int UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK = 5; private static final int TVVIEW_SET_MAIN_TIMEOUT_MS = 3000; @@ -256,7 +274,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC private final DurationTimer mMainDurationTimer = new DurationTimer(); private final DurationTimer mTuneDurationTimer = new DurationTimer(); private DvrManager mDvrManager; - private DvrDataManager mDvrDataManager; private ConflictChecker mDvrConflictChecker; private View mContentView; @@ -463,8 +480,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC // Check this permission for the EPG fetch. // TODO: check {@link shouldShowRequestPermissionRationale}. - if (Utils.hasInternalTvInputs(this, true) && checkSelfPermission( - android.Manifest.permission.ACCESS_COARSE_LOCATION) + // While testing, no way to allow the permission when the dialog shows up. So we'd better + // not show the dialog. + if (!TvCommonUtils.isRunningInTest() && Utils.hasInternalTvInputs(this, true) + && checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); @@ -541,7 +560,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mMemoryManageables.add(TvContentRatingCache.getInstance()); if (CommonFeatures.DVR.isEnabled(this)) { mDvrManager = tvApplication.getDvrManager(); - mDvrDataManager = tvApplication.getDvrDataManager(); } mTimeShiftManager = new TimeShiftManager(this, mTvView, mProgramDataManager, mTracker, new OnCurrentProgramUpdatedListener() { @@ -1179,7 +1197,8 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC * It might be a live program. If the time shifting is available, it can be a past program, too. */ public Program getCurrentProgram() { - if (mTimeShiftManager.isAvailable()) { + if (!isChannelChangeKeyDownReceived() && mTimeShiftManager.isAvailable()) { + // We shouldn't get current program from TimeShiftManager during channel tunning return mTimeShiftManager.getCurrentProgram(); } return mProgramDataManager.getCurrentProgram(getCurrentChannelId()); @@ -2276,6 +2295,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC @Override protected void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy()"); + SideFragment.releasePreloadedRecycledViews(); if (mTvView != null) { mTvView.release(); } @@ -2354,9 +2374,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC case KeyEvent.KEYCODE_DPAD_UP: if (event.getRepeatCount() == 0 && mChannelTuner.getBrowsableChannelCount() > 0) { - moveToAdjacentChannel(true, false); + // message sending should be done before moving channel, because we use the + // existence of message to decide if users are switching channel. mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()), CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); + moveToAdjacentChannel(true, false); mTracker.sendChannelUp(); } return true; @@ -2364,9 +2386,11 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC case KeyEvent.KEYCODE_DPAD_DOWN: if (event.getRepeatCount() == 0 && mChannelTuner.getBrowsableChannelCount() > 0) { - moveToAdjacentChannel(false, false); + // message sending should be done before moving channel, because we use the + // existence of message to decide if users are switching channel. mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()), CHANNEL_CHANGE_INITIAL_DELAY_MILLIS); + moveToAdjacentChannel(false, false); mTracker.sendChannelDown(); } return true; @@ -2390,7 +2414,6 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC * S KEYCODE_CAPTIONS: select subtitle * W debug: toggle screen size * V KEYCODE_MEDIA_RECORD debug: record the current channel for 30 sec - * X KEYCODE_BUTTON_X KEYCODE_PROG_BLUE debug: record current channel for a few minutes */ if (SystemProperties.LOG_KEYEVENT.getValue()) { Log.d(TAG, "onKeyUp(" + keyCode + ", " + event + ")"); @@ -2492,7 +2515,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC false); } return true; - + case KeyEvent.KEYCODE_WINDOW: + enterPictureInPictureMode(); + return true; case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_NUMPAD_ENTER: case KeyEvent.KEYCODE_E: @@ -2542,8 +2567,61 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mOverlayManager.showBanner(); return true; } + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_V: { + Channel currentChannel = getCurrentChannel(); + if (currentChannel != null && mDvrManager != null) { + boolean isRecording = + mDvrManager.getCurrentRecording(currentChannel.getId()) != null; + if (!isRecording) { + if (!mDvrManager.isChannelRecordable(currentChannel)) { + Toast.makeText(this, R.string.dvr_msg_cannot_record_program, + Toast.LENGTH_SHORT).show(); + } else { + if (!DvrUiHelper.checkStorageStatusAndShowErrorMessage(this, + currentChannel.getInputId())) { + return true; + } + Program program = mProgramDataManager + .getCurrentProgram(currentChannel.getId()); + if (program == null) { + DvrUiHelper + .showChannelRecordDurationOptions(this, currentChannel); + } else if (DvrUiHelper.handleCreateSchedule(this, program)) { + String msg = getString( + R.string.dvr_msg_current_program_scheduled, + program.getTitle(), Utils.toTimeString( + program.getEndTimeUtcMillis(), false)); + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); + } + } + } else { + DvrUiHelper.showStopRecordingDialog(this, currentChannel.getId(), + DvrStopRecordingFragment.REASON_USER_STOP, + new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrStopRecordingFragment.ACTION_STOP) { + ScheduledRecording currentRecording = + mDvrManager.getCurrentRecording( + currentChannel.getId()); + if (currentRecording != null) { + mDvrManager.stopRecording(currentRecording); + } + } + } + }); + } + } + return true; + } } } + if (keyCode == KeyEvent.KEYCODE_WINDOW) { + // Consumes the PIP button to prevent entering PIP mode + // in case that TV isn't showing properly (e.g. no browsable channel) + return true; + } if (SystemProperties.USE_DEBUG_KEYS.getValue() || BuildConfig.ENG) { switch (keyCode) { case KeyEvent.KEYCODE_W: { @@ -2578,36 +2656,9 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mOverlayManager.getSideFragmentManager().show(new DisplayModeFragment()); return true; } - case KeyEvent.KEYCODE_D: mOverlayManager.getSideFragmentManager().show(new DeveloperOptionFragment()); return true; - - case KeyEvent.KEYCODE_MEDIA_RECORD: // TODO(DVR) handle with debug_keys set - case KeyEvent.KEYCODE_V: { - DvrManager dvrManager = TvApplication.getSingletons(this).getDvrManager(); - long startTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(5); - long endTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(35); - dvrManager.addSchedule(getCurrentChannel(), startTime, endTime); - return true; - } - case KeyEvent.KEYCODE_PROG_BLUE: - case KeyEvent.KEYCODE_BUTTON_X: - case KeyEvent.KEYCODE_X: - if (CommonFeatures.DVR.isEnabled(this)) { - Channel channel = mTvView.getCurrentChannel(); - long channelId = channel.getId(); - Program p = mProgramDataManager.getCurrentProgram(channelId); - if (p == null) { - long now = System.currentTimeMillis(); - mDvrManager - .addSchedule(channel, now, now + TimeUnit.MINUTES.toMillis(1)); - } else { - mDvrManager.addSchedule(p, mDvrManager.getConflictingSchedules(p)); - } - return true; - } - break; } } return super.onKeyUp(keyCode, event); @@ -3109,8 +3160,10 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mHandler.postDelayed(new Runnable() { @Override public void run() { - initAnimations(); - initSideFragments(); + if (mActivityStarted) { + initAnimations(); + initSideFragments(); + } } }, LAZY_INITIALIZATION_DELAY); } @@ -3142,13 +3195,17 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC switch (msg.what) { case MSG_CHANNEL_DOWN_PRESSED: long startTime = (Long) msg.obj; - mainActivity.moveToAdjacentChannel(false, true); + // message re-sending should be done before moving channel, because we use the + // existence of message to decide if users are switching channel. sendMessageDelayed(Message.obtain(msg), getDelay(startTime)); + mainActivity.moveToAdjacentChannel(false, true); break; case MSG_CHANNEL_UP_PRESSED: startTime = (Long) msg.obj; - mainActivity.moveToAdjacentChannel(true, true); + // message re-sending should be done before moving channel, because we use the + // existence of message to decide if users are switching channel. sendMessageDelayed(Message.obtain(msg), getDelay(startTime)); + mainActivity.moveToAdjacentChannel(true, true); break; case MSG_UPDATE_CHANNEL_BANNER_BY_INFO_UPDATE: mainActivity.updateChannelBannerAndShowIfNeeded( @@ -3262,7 +3319,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC mUnlockAllowedRatingBeforeShrunken = isUnderShrunkenTvView(); mTvView.unblockContent(rating); } - + mChannelBannerView.setBlockingContentRating(rating); updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); mTvViewUiManager.fadeInTvView(); } @@ -3272,6 +3329,7 @@ public class MainActivity extends Activity implements AudioManager.OnAudioFocusC if (!isUnderShrunkenTvView()) { mUnlockAllowedRatingBeforeShrunken = false; } + mChannelBannerView.setBlockingContentRating(null); updateChannelBannerAndShowIfNeeded(UPDATE_CHANNEL_BANNER_REASON_LOCK_OR_UNLOCK); } } diff --git a/src/com/android/tv/TimeShiftManager.java b/src/com/android/tv/TimeShiftManager.java index 650f5191..2d6d45c4 100644 --- a/src/com/android/tv/TimeShiftManager.java +++ b/src/com/android/tv/TimeShiftManager.java @@ -19,8 +19,6 @@ package com.android.tv; import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.Context; -import android.os.AsyncTask; -import android.os.AsyncTask.Status; import android.os.Handler; import android.os.Message; import android.support.annotation.IntDef; @@ -63,8 +61,7 @@ import java.util.concurrent.TimeUnit; */ public class TimeShiftManager { private static final String TAG = "TimeShiftManager"; - // STOPSHIP: Turn this flag off once b/31074952 is fixed. - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; @Retention(RetentionPolicy.SOURCE) @IntDef({PLAY_STATUS_PAUSED, PLAY_STATUS_PLAYING}) @@ -894,7 +891,6 @@ public class TimeShiftManager { endTimeMs + PREFETCH_DURATION_FOR_NEXT); if (needToLoad) { Range<Long> period = Range.create(fetchStartTimeMs, endTimeMs); - SoftPreconditions.checkState(isAvailable(), TAG, "Time shifting is not available"); mProgramLoadQueue.add(period); startTaskIfNeeded(); } @@ -907,52 +903,24 @@ public class TimeShiftManager { if (mProgramLoadTask == null || mProgramLoadTask.isCancelled()) { startNext(); } else { - switch (mProgramLoadTask.getStatus()) { - case PENDING: - SoftPreconditions.checkState(false, TAG, - "The state of task can not be PENDING"); - if (mProgramLoadTask.overlaps(mProgramLoadQueue)) { - if (mProgramLoadTask.cancel(true)) { - SoftPreconditions.checkState(isAvailable(), TAG, - "Time shifting is not available"); - mProgramLoadQueue.add(mProgramLoadTask.getPeriod()); - mProgramLoadTask = null; - startNext(); - } - } - break; - case RUNNING: - // Remove pending task fully satisfied by the current - Range<Long> current = mProgramLoadTask.getPeriod(); - Iterator<Range<Long>> i = mProgramLoadQueue.iterator(); - while (i.hasNext()) { - Range<Long> r = i.next(); - if (current.contains(r)) { - i.remove(); - } - } - break; - case FINISHED: - SoftPreconditions.checkState(false, TAG, - "The state of task can not be FINISHED"); - // The task should have already cleared it self, clear and restart anyways. - Log.w(TAG, mProgramLoadTask + " is finished, but was not cleared"); - startNext(); - break; + // Remove pending task fully satisfied by the current + Range<Long> current = mProgramLoadTask.getPeriod(); + Iterator<Range<Long>> i = mProgramLoadQueue.iterator(); + while (i.hasNext()) { + Range<Long> r = i.next(); + if (current.contains(r)) { + i.remove(); + } } } } private void startNext() { - SoftPreconditions.checkState(mProgramLoadTask == null - || mProgramLoadTask.getStatus() == Status.RUNNING, TAG, - "The status of task should be \"RUNNING\""); mProgramLoadTask = null; if (mProgramLoadQueue.isEmpty()) { return; } - SoftPreconditions.checkState(isAvailable(), TAG, "Time shifting should be available."); Range<Long> next = mProgramLoadQueue.poll(); // Extend next to include any overlapping Ranges. Iterator<Range<Long>> i = mProgramLoadQueue.iterator(); @@ -1134,7 +1102,6 @@ public class TimeShiftManager { private void schedulePrefetchPrograms() { if (DEBUG) Log.d(TAG, "Scheduling prefetching programs."); - SoftPreconditions.checkState(isAvailable(), TAG, "Time shifting is not available"); if (mHandler.hasMessages(MSG_PREFETCH_PROGRAM)) { return; } @@ -1172,7 +1139,6 @@ public class TimeShiftManager { // Prefetch programs within PREFETCH_DURATION_FOR_NEXT from now. private void prefetchPrograms() { - SoftPreconditions.checkState(isAvailable(), TAG, "Time shifting is not available"); long startTimeMs; Program lastValidProgram = getLastValidProgram(); if (lastValidProgram == null) { diff --git a/src/com/android/tv/TvApplication.java b/src/com/android/tv/TvApplication.java index e15bad02..0e18a259 100644 --- a/src/com/android/tv/TvApplication.java +++ b/src/com/android/tv/TvApplication.java @@ -55,6 +55,7 @@ import com.android.tv.dvr.DvrDataManagerImpl; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrRecordingService; import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.tvinput.TunerTvInputService; @@ -95,6 +96,7 @@ public class TvApplication extends Application implements ApplicationSingletons private DvrManager mDvrManager; private DvrScheduleManager mDvrScheduleManager; private DvrDataManager mDvrDataManager; + private DvrStorageStatusManager mDvrStorageStatusManager; private DvrWatchedPositionManager mDvrWatchedPositionManager; @Nullable private InputSessionManager mInputSessionManager; @@ -129,14 +131,16 @@ public class TvApplication extends Application implements ApplicationSingletons // Only set StrictMode for ENG builds because the build server only produces userdebug // builds. if (BuildConfig.ENG && SystemProperties.ALLOW_STRICT_MODE.getValue()) { - StrictMode.setThreadPolicy( - new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); - StrictMode.VmPolicy.Builder vmPolicyBuilder = new StrictMode.VmPolicy.Builder() - .detectAll().penaltyLog(); - if (BuildConfig.ENG && SystemProperties.ALLOW_DEATH_PENALTY.getValue() && - !TvCommonUtils.isRunningInTest()) { - // TODO turn on death penalty for tests when they stop leaking MainActivity + StrictMode.ThreadPolicy.Builder threadPolicyBuilder = + new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog(); + StrictMode.VmPolicy.Builder vmPolicyBuilder = + new StrictMode.VmPolicy.Builder().detectAll().penaltyLog(); + if (!TvCommonUtils.isRunningInTest()) { + threadPolicyBuilder.penaltyDialog(); + // Turn off death penalty for tests b/23355898 + vmPolicyBuilder.penaltyDeath(); } + StrictMode.setThreadPolicy(threadPolicyBuilder.build()); StrictMode.setVmPolicy(vmPolicyBuilder.build()); } if (BuildConfig.ENG && !SystemProperties.ALLOW_ANALYTICS_IN_ENG.getValue()) { @@ -160,6 +164,9 @@ public class TvApplication extends Application implements ApplicationSingletons return; } mRunningInMainProcess = isMainProcess; + if (CommonFeatures.DVR.isEnabled(this)) { + mDvrStorageStatusManager = new DvrStorageStatusManager(this, mRunningInMainProcess); + } if (mRunningInMainProcess) { mTvInputManagerHelper.addCallback(new TvInputCallback() { @Override @@ -286,14 +293,23 @@ public class TvApplication extends Application implements ApplicationSingletons @Override public DvrDataManager getDvrDataManager() { if (mDvrDataManager == null) { - DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this, Clock.SYSTEM); - mDvrDataManager = dvrDataManager; - dvrDataManager.start(); + DvrDataManagerImpl dvrDataManager = new DvrDataManagerImpl(this, Clock.SYSTEM); + mDvrDataManager = dvrDataManager; + dvrDataManager.start(); } return mDvrDataManager; } /** + * Returns {@link DvrStorageStatusManager}. + */ + @TargetApi(Build.VERSION_CODES.N) + @Override + public DvrStorageStatusManager getDvrStorageStatusManager() { + return mDvrStorageStatusManager; + } + + /** * Returns {@link TvInputManagerHelper}. */ @Override diff --git a/src/com/android/tv/data/BaseProgram.java b/src/com/android/tv/data/BaseProgram.java index c32da431..f420de02 100644 --- a/src/com/android/tv/data/BaseProgram.java +++ b/src/com/android/tv/data/BaseProgram.java @@ -129,6 +129,11 @@ public abstract class BaseProgram { abstract public long getDurationMillis(); /** + * Returns the series ID. + */ + abstract public String getSeriesId(); + + /** * Returns the season number. */ abstract public String getSeasonNumber(); @@ -149,6 +154,11 @@ public abstract class BaseProgram { abstract public String getThumbnailUri(); /** + * Returns the array of the ID's of the canonical genres. + */ + abstract public int[] getCanonicalGenreIds(); + + /** * Returns channel's ID of the program. */ abstract public long getChannelId(); diff --git a/src/com/android/tv/data/Channel.java b/src/com/android/tv/data/Channel.java index 898324f0..30f84236 100644 --- a/src/com/android/tv/data/Channel.java +++ b/src/com/android/tv/data/Channel.java @@ -150,11 +150,6 @@ public final class Channel { private long mDvrId; - /** - * TODO(DVR): Need to fill the following data. - */ - private boolean mRecordable; - private Channel() { // Do nothing. } @@ -566,6 +561,8 @@ public final class Channel { getUri().toString()); mAppLinkType = APP_LINK_TYPE_CHANNEL; return; + } else { + Log.w(TAG, "No activity exists to handle : " + mAppLinkIntentUri); } } catch (URISyntaxException e) { Log.w(TAG, "Unable to set app link for " + mAppLinkIntentUri, e); diff --git a/src/com/android/tv/data/ChannelDataManager.java b/src/com/android/tv/data/ChannelDataManager.java index af7ed904..6f9ea6d7 100644 --- a/src/com/android/tv/data/ChannelDataManager.java +++ b/src/com/android/tv/data/ChannelDataManager.java @@ -297,6 +297,16 @@ public class ChannelDataManager { } /** + * Checks if the channel exists in DB. + * + * <p>Note that the channels of the removed inputs can not be obtained from {@link #getChannel}. + * In that case this method is used to check if the channel exists in the DB. + */ + public boolean doesChannelExistInDb(long channelId) { + return mChannelWrapperMap.get(channelId) != null; + } + + /** * Returns true if and only if there exists at least one channel and all channels are hidden. */ public boolean areAllChannelsHidden() { diff --git a/src/com/android/tv/data/Lineup.java b/src/com/android/tv/data/Lineup.java index 3e162d1c..d0e9d7ba 100644 --- a/src/com/android/tv/data/Lineup.java +++ b/src/com/android/tv/data/Lineup.java @@ -46,7 +46,6 @@ public class Lineup { */ public final String location; - /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({LINEUP_CABLE, LINEUP_SATELLITE, LINEUP_BROADCAST_DIGITAL, LINEUP_BROADCAST_ANALOG, LINEUP_IPTV, LINEUP_MVPD}) diff --git a/src/com/android/tv/data/Program.java b/src/com/android/tv/data/Program.java index 05d7d9c4..b9cd3d8d 100644 --- a/src/com/android/tv/data/Program.java +++ b/src/com/android/tv/data/Program.java @@ -260,6 +260,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Returns the series ID. */ + @Override public String getSeriesId() { return mSeriesId; } @@ -400,6 +401,7 @@ public final class Program extends BaseProgram implements Comparable<Program>, P /** * Returns array of canonical genre ID's for this program. */ + @Override public int[] getCanonicalGenreIds() { return mCanonicalGenreIds; } diff --git a/src/com/android/tv/data/StreamInfo.java b/src/com/android/tv/data/StreamInfo.java index df842737..fe461f14 100644 --- a/src/com/android/tv/data/StreamInfo.java +++ b/src/com/android/tv/data/StreamInfo.java @@ -16,6 +16,8 @@ package com.android.tv.data; +import android.media.tv.TvContentRating; + public interface StreamInfo { int VIDEO_DEFINITION_LEVEL_UNKNOWN = 0; int VIDEO_DEFINITION_LEVEL_SD = 1; @@ -26,6 +28,7 @@ public interface StreamInfo { int AUDIO_CHANNEL_COUNT_UNKNOWN = 0; Channel getCurrentChannel(); + TvContentRating getBlockedContentRating(); int getVideoWidth(); int getVideoHeight(); diff --git a/src/com/android/tv/data/epg/EpgFetcher.java b/src/com/android/tv/data/epg/EpgFetcher.java index dd46caf1..3b093b6a 100644 --- a/src/com/android/tv/data/epg/EpgFetcher.java +++ b/src/com/android/tv/data/epg/EpgFetcher.java @@ -30,7 +30,6 @@ import android.media.tv.TvContract; import android.media.tv.TvContract.Programs; import android.media.tv.TvContract.Programs.Genres; import android.media.tv.TvInputInfo; -import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; @@ -45,13 +44,11 @@ import android.util.Log; import com.android.tv.TvApplication; import com.android.tv.common.WeakHandler; -import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.InternalDataUtils; import com.android.tv.data.Lineup; import com.android.tv.data.Program; -import com.android.tv.dvr.SeriesRecordingScheduler; import com.android.tv.util.LocationUtils; import com.android.tv.util.RecurringRunner; import com.android.tv.util.Utils; @@ -69,13 +66,13 @@ import java.util.concurrent.TimeUnit; */ public class EpgFetcher { private static final String TAG = "EpgFetcher"; - private static final boolean DEBUG = true; // STOPSHIP(DVR) + private static final boolean DEBUG = false; private static final int MSG_FETCH_EPG = 1; private static final long EPG_PREFETCH_RECURRING_PERIOD_MS = TimeUnit.HOURS.toMillis(4); private static final long EPG_READER_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1); - private static final long LOCATION_INIT_WAIT_MS = TimeUnit.MINUTES.toMillis(1); + private static final long LOCATION_INIT_WAIT_MS = TimeUnit.SECONDS.toMillis(10); private static final long LOCATION_ERROR_WAIT_MS = TimeUnit.HOURS.toMillis(1); private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(30); @@ -184,17 +181,16 @@ public class EpgFetcher { try { Address address = LocationUtils.getCurrentAddress(mContext); - if (address == null) { - if (DEBUG) Log.d(TAG, "Failed to get the current address."); - return false; - } - if (!TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { + if (address != null + && !TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) { if (DEBUG) Log.d(TAG, "Country not supported: " + address.getCountryCode()); return false; } - } catch (IOException | SecurityException e) { - Log.w(TAG, "Exception when getting the current location", e); + } catch (SecurityException e) { + Log.w(TAG, "No permission to get the current location", e); return false; + } catch (IOException e) { + Log.w(TAG, "IO Exception when getting the current location", e); } return true; } @@ -319,15 +315,6 @@ public class EpgFetcher { final boolean epgUpdated = updated; setLastUpdatedEpgTimestamp(epgTimestamp); mHandler.removeMessages(MSG_FETCH_EPG); - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - mRecurringRunner.resetNextRunTime(); - if (epgUpdated && CommonFeatures.DVR.isEnabled(mContext)) { - SeriesRecordingScheduler.getInstance(mContext).updateSchedules(); - } - } - }); if (DEBUG) Log.d(TAG, "Fetching EPG is finished."); } diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java index 6af77940..89661df3 100644 --- a/src/com/android/tv/dvr/BaseDvrDataManager.java +++ b/src/com/android/tv/dvr/BaseDvrDataManager.java @@ -30,9 +30,10 @@ import com.android.tv.dvr.ScheduledRecording.RecordingState; import com.android.tv.util.Clock; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -136,35 +137,35 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } /** - * Calls {@link RecordedProgramListener#onRecordedProgramAdded(RecordedProgram)} + * Calls {@link RecordedProgramListener#onRecordedProgramsAdded} * for each listener. */ - protected final void notifyRecordedProgramAdded(RecordedProgram recordedProgram) { + protected final void notifyRecordedProgramsAdded(RecordedProgram... recordedPrograms) { for (RecordedProgramListener l : mRecordedProgramListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "added " + recordedProgram); - l.onRecordedProgramAdded(recordedProgram); + if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(recordedPrograms)); + l.onRecordedProgramsAdded(recordedPrograms); } } /** - * Calls {@link RecordedProgramListener#onRecordedProgramChanged(RecordedProgram)} + * Calls {@link RecordedProgramListener#onRecordedProgramsChanged} * for each listener. */ - protected final void notifyRecordedProgramChanged(RecordedProgram recordedProgram) { + protected final void notifyRecordedProgramsChanged(RecordedProgram... recordedPrograms) { for (RecordedProgramListener l : mRecordedProgramListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "changed " + recordedProgram); - l.onRecordedProgramChanged(recordedProgram); + if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(recordedPrograms)); + l.onRecordedProgramsChanged(recordedPrograms); } } /** - * Calls {@link RecordedProgramListener#onRecordedProgramRemoved(RecordedProgram)} + * Calls {@link RecordedProgramListener#onRecordedProgramsRemoved} * for each listener. */ - protected final void notifyRecordedProgramRemoved(RecordedProgram recordedProgram) { + protected final void notifyRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { for (RecordedProgramListener l : mRecordedProgramListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "removed " + recordedProgram); - l.onRecordedProgramRemoved(recordedProgram); + if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(recordedPrograms)); + l.onRecordedProgramsRemoved(recordedPrograms); } } @@ -174,7 +175,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifySeriesRecordingAdded(SeriesRecording... seriesRecordings) { for (SeriesRecordingListener l : mSeriesRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "added " + seriesRecordings); + if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(seriesRecordings)); l.onSeriesRecordingAdded(seriesRecordings); } } @@ -185,7 +186,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifySeriesRecordingRemoved(SeriesRecording... seriesRecordings) { for (SeriesRecordingListener l : mSeriesRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "removed " + seriesRecordings); + if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(seriesRecordings)); l.onSeriesRecordingRemoved(seriesRecordings); } } @@ -197,7 +198,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifySeriesRecordingChanged(SeriesRecording... seriesRecordings) { for (SeriesRecordingListener l : mSeriesRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "changed " + seriesRecordings); + if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(seriesRecordings)); l.onSeriesRecordingChanged(seriesRecordings); } } @@ -208,7 +209,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifyScheduledRecordingAdded(ScheduledRecording... scheduledRecording) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "added " + scheduledRecording); + if (DEBUG) Log.d(TAG, "notify " + l + " added " + Arrays.asList(scheduledRecording)); l.onScheduledRecordingAdded(scheduledRecording); } } @@ -219,7 +220,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { */ protected final void notifyScheduledRecordingRemoved(ScheduledRecording... scheduledRecording) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "removed " + scheduledRecording); + if (DEBUG) Log.d(TAG, "notify " + l + " removed " + Arrays.asList(scheduledRecording)); l.onScheduledRecordingRemoved(scheduledRecording); } } @@ -232,7 +233,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { protected final void notifyScheduledRecordingStatusChanged( ScheduledRecording... scheduledRecording) { for (ScheduledRecordingListener l : mScheduledRecordingListeners) { - if (DEBUG) Log.d(TAG, "notify " + l + "changed " + scheduledRecording); + if (DEBUG) Log.d(TAG, "notify " + l + " changed " + Arrays.asList(scheduledRecording)); l.onScheduledRecordingStatusChanged(scheduledRecording); } } @@ -259,14 +260,6 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } @Override - public List<ScheduledRecording> getAvailableAndCanceledScheduledRecordings() { - return filterEndTimeIsPast(getRecordingsWithState( - ScheduledRecording.STATE_RECORDING_IN_PROGRESS, - ScheduledRecording.STATE_RECORDING_NOT_STARTED, - ScheduledRecording.STATE_RECORDING_CANCELED)); - } - - @Override public List<ScheduledRecording> getStartedRecordings() { return filterEndTimeIsPast(getRecordingsWithState( ScheduledRecording.STATE_RECORDING_IN_PROGRESS)); @@ -274,8 +267,6 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { @Override public List<ScheduledRecording> getNonStartedScheduledRecordings() { - Set<Integer> states = new HashSet<>(); - states.add(ScheduledRecording.STATE_RECORDING_NOT_STARTED); return filterEndTimeIsPast(getRecordingsWithState( ScheduledRecording.STATE_RECORDING_NOT_STARTED)); } @@ -314,6 +305,9 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { @Override public List<RecordedProgram> getRecordedPrograms(long seriesRecordingId) { SeriesRecording seriesRecording = getSeriesRecording(seriesRecordingId); + if (seriesRecording == null) { + return Collections.emptyList(); + } List<RecordedProgram> result = new ArrayList<>(); for (RecordedProgram r : getRecordedPrograms()) { if (seriesRecording.getSeriesId().equals(r.getSeriesId())) { @@ -322,4 +316,7 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } return result; } + + @Override + public void forgetStorage(String inputId) { } } diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java index 126f3e74..06613667 100644 --- a/src/com/android/tv/dvr/DvrDataManager.java +++ b/src/com/android/tv/dvr/DvrDataManager.java @@ -69,11 +69,6 @@ public interface DvrDataManager { List<ScheduledRecording> getAvailableScheduledRecordings(); /** - * Return all available and canceled {@link ScheduledRecording}. - */ - List<ScheduledRecording> getAvailableAndCanceledScheduledRecordings(); - - /** * Returns started recordings that expired. */ List<ScheduledRecording> getStartedRecordings(); @@ -260,10 +255,10 @@ public interface DvrDataManager { * Listens for changes to {@link RecordedProgram}s. */ interface RecordedProgramListener { - void onRecordedProgramAdded(RecordedProgram recordedProgram); + void onRecordedProgramsAdded(RecordedProgram... recordedPrograms); - void onRecordedProgramChanged(RecordedProgram recordedProgram); + void onRecordedProgramsChanged(RecordedProgram... recordedPrograms); - void onRecordedProgramRemoved(RecordedProgram recordedProgram); + void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms); } } diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java index 5ae2c4ea..46682a48 100644 --- a/src/com/android/tv/dvr/DvrDataManagerImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -16,12 +16,16 @@ package com.android.tv.dvr; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.database.ContentObserver; +import android.database.sqlite.SQLiteException; import android.media.tv.TvContract.RecordedPrograms; +import android.media.tv.TvInputInfo; +import android.media.tv.TvInputManager.TvInputCallback; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -37,7 +41,7 @@ import android.util.Range; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; -import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener; import com.android.tv.dvr.ScheduledRecording.RecordingState; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask; @@ -47,9 +51,13 @@ import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask; +import com.android.tv.util.AsyncDbTask; import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask; import com.android.tv.util.Clock; +import com.android.tv.util.Filter; +import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.TvProviderUriMatcher; +import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; @@ -69,6 +77,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private static final String TAG = "DvrDataManagerImpl"; private static final boolean DEBUG = false; + private final TvInputManagerHelper mInputManager; + private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>(); private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>(); private final HashMap<Long, SeriesRecording> mSeriesRecordings = new HashMap<>(); @@ -76,6 +86,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { new HashMap<>(); private final HashMap<String, SeriesRecording> mSeriesId2SeriesRecordings = new HashMap<>(); + private final HashMap<Long, ScheduledRecording> mScheduledRecordingsForRemovedInput = + new HashMap<>(); + private final HashMap<Long, RecordedProgram> mRecordedProgramsForRemovedInput = new HashMap<>(); + private final HashMap<Long, SeriesRecording> mSeriesRecordingsForRemovedInput = new HashMap<>(); + private final Context mContext; private final ContentObserver mContentObserver = new ContentObserver(new Handler( Looper.getMainLooper())) { @@ -96,15 +111,68 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private boolean mDvrLoadFinished; private boolean mRecordedProgramLoadFinished; private final Set<AsyncTask> mPendingTasks = new ArraySet<>(); - private final DvrDbSync mDbSync; + private DvrDbSync mDbSync; + private DvrStorageStatusManager mStorageStatusManager; + + private final TvInputCallback mInputCallback = new TvInputCallback() { + @Override + public void onInputAdded(String inputId) { + if (DEBUG) Log.d(TAG, "onInputAdded " + inputId); + if (!isInputAvailable(inputId)) { + if (DEBUG) Log.d(TAG, "Not available for recording"); + return; + } + unhideInput(inputId); + } + + @Override + public void onInputRemoved(String inputId) { + if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId); + hideInput(inputId); + } + }; + + private final OnStorageMountChangedListener mStorageMountChangedListener = + new OnStorageMountChangedListener() { + @Override + public void onStorageMountChanged(boolean storageMounted) { + for (TvInputInfo input : mInputManager.getTvInputInfos(true, true)) { + if (Utils.isBundledInput(input.getId())) { + if (storageMounted) { + unhideInput(input.getId()); + } else { + hideInput(input.getId()); + } + } + } + } + }; + + private static <T> List<T> moveElements(HashMap<Long, T> from, HashMap<Long, T> to, + Filter<T> filter) { + List<T> moved = new ArrayList<>(); + Iterator<Entry<Long, T>> iter = from.entrySet().iterator(); + while (iter.hasNext()) { + Entry<Long, T> entry = iter.next(); + if (filter.filter(entry.getValue())) { + to.put(entry.getKey(), entry.getValue()); + iter.remove(); + moved.add(entry.getValue()); + } + } + return moved; + } public DvrDataManagerImpl(Context context, Clock clock) { super(context, clock); mContext = context; - mDbSync = new DvrDbSync(context, this); + mInputManager = TvApplication.getSingletons(context).getTvInputManagerHelper(); + mStorageStatusManager = TvApplication.getSingletons(context).getDvrStorageStatusManager(); } public void start() { + mInputManager.addCallback(mInputCallback); + mStorageStatusManager.addListener(mStorageMountChangedListener); AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask = new AsyncDvrQuerySeriesRecordingTask(mContext) { @Override @@ -116,9 +184,18 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { protected void onPostExecute(List<SeriesRecording> seriesRecordings) { mPendingTasks.remove(this); long maxId = 0; + HashSet<String> seriesIds = new HashSet<>(); for (SeriesRecording r : seriesRecordings) { - mSeriesRecordings.put(r.getId(), r); - mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + if (SoftPreconditions.checkState(!seriesIds.contains(r.getSeriesId()), TAG, + "Skip loading series recording with duplicate series ID: " + r)) { + seriesIds.add(r.getSeriesId()); + if (isInputAvailable(r.getInputId())) { + mSeriesRecordings.put(r.getId(), r); + mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + } else { + mSeriesRecordingsForRemovedInput.put(r.getId(), r); + } + } if (maxId < r.getId()) { maxId = r.getId(); } @@ -128,20 +205,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { }; dvrQuerySeriesRecordingTask.executeOnDbThread(); mPendingTasks.add(dvrQuerySeriesRecordingTask); - AsyncDvrQueryScheduleTask dvrQueryRecordingTask + AsyncDvrQueryScheduleTask dvrQueryScheduleTask = new AsyncDvrQueryScheduleTask(mContext) { @Override protected void onCancelled(List<ScheduledRecording> scheduledRecordings) { mPendingTasks.remove(this); } + @SuppressLint("SwitchIntDef") @Override protected void onPostExecute(List<ScheduledRecording> result) { mPendingTasks.remove(this); long maxId = 0; + List<SeriesRecording> seriesRecordingsToAdd = new ArrayList<>(); List<ScheduledRecording> toUpdate = new ArrayList<>(); + List<ScheduledRecording> toDelete = new ArrayList<>(); for (ScheduledRecording r : result) { - if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) { + if (!isInputAvailable(r.getInputId())) { + mScheduledRecordingsForRemovedInput.put(r.getId(), r); + } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) { getDeletedScheduleMap().put(r.getProgramId(), r); } else { mScheduledRecordings.put(r.getId(), r); @@ -149,22 +231,29 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mProgramId2ScheduledRecordings.put(r.getProgramId(), r); } // Adjust the state of the schedules before DB loading is finished. - if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setState(ScheduledRecording.STATE_RECORDING_FAILED) - .build()); - } else { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) - .build()); - } - } else if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { - if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { - toUpdate.add(ScheduledRecording.buildFrom(r) - .setState(ScheduledRecording.STATE_RECORDING_FAILED) - .build()); - } + switch (r.getState()) { + case ScheduledRecording.STATE_RECORDING_IN_PROGRESS: + if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { + toUpdate.add(ScheduledRecording.buildFrom(r) + .setState(ScheduledRecording.STATE_RECORDING_FAILED) + .build()); + } else { + toUpdate.add(ScheduledRecording.buildFrom(r) + .setState( + ScheduledRecording.STATE_RECORDING_NOT_STARTED) + .build()); + } + break; + case ScheduledRecording.STATE_RECORDING_NOT_STARTED: + if (r.getEndTimeMs() <= mClock.currentTimeMillis()) { + toUpdate.add(ScheduledRecording.buildFrom(r) + .setState(ScheduledRecording.STATE_RECORDING_FAILED) + .build()); + } + break; + case ScheduledRecording.STATE_RECORDING_CANCELED: + toDelete.add(r); + break; } } if (maxId < r.getId()) { @@ -172,19 +261,23 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (!toUpdate.isEmpty()) { - updateScheduledRecording(true, ScheduledRecording.toArray(toUpdate)); + updateScheduledRecording(ScheduledRecording.toArray(toUpdate)); + } + if (!toDelete.isEmpty()) { + removeScheduledRecording(ScheduledRecording.toArray(toDelete)); } IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); mDvrLoadFinished = true; notifyDvrScheduleLoadFinished(); + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); mDbSync.start(); if (isInitialized()) { SeriesRecordingScheduler.getInstance(mContext).start(); } } }; - dvrQueryRecordingTask.executeOnDbThread(); - mPendingTasks.add(dvrQueryRecordingTask); + dvrQueryScheduleTask.executeOnDbThread(); + mPendingTasks.add(dvrQueryScheduleTask); RecordedProgramsQueryTask mRecordedProgramQueryTask = new RecordedProgramsQueryTask(mContext.getContentResolver(), null); mRecordedProgramQueryTask.executeOnDbThread(); @@ -193,8 +286,12 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } public void stop() { + mInputManager.removeCallback(mInputCallback); + mStorageStatusManager.removeListener(mStorageMountChangedListener); SeriesRecordingScheduler.getInstance(mContext).stop(); - mDbSync.stop(); + if (mDbSync != null) { + mDbSync.stop(); + } ContentResolver cr = mContext.getContentResolver(); cr.unregisterContentObserver(mContentObserver); Iterator<AsyncTask> i = mPendingTasks.iterator(); @@ -213,52 +310,76 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) { if (!mRecordedProgramLoadFinished) { for (RecordedProgram recorded : recordedPrograms) { - mRecordedPrograms.put(recorded.getId(), recorded); + if (isInputAvailable(recorded.getInputId())) { + mRecordedPrograms.put(recorded.getId(), recorded); + } else { + mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded); + } } mRecordedProgramLoadFinished = true; notifyRecordedProgramLoadFinished(); } else if (recordedPrograms == null || recordedPrograms.isEmpty()) { - for (RecordedProgram recorded : mRecordedPrograms.values()) { - notifyRecordedProgramRemoved(recorded); - } + List<RecordedProgram> oldRecordedPrograms = + new ArrayList<>(mRecordedPrograms.values()); mRecordedPrograms.clear(); + mRecordedProgramsForRemovedInput.clear(); + notifyRecordedProgramsRemoved(RecordedProgram.toArray(oldRecordedPrograms)); } else { HashMap<Long, RecordedProgram> oldRecordedPrograms = new HashMap<>(mRecordedPrograms); mRecordedPrograms.clear(); + mRecordedProgramsForRemovedInput.clear(); + List<RecordedProgram> addedRecordedPrograms = new ArrayList<>(); + List<RecordedProgram> changedRecordedPrograms = new ArrayList<>(); for (RecordedProgram recorded : recordedPrograms) { - mRecordedPrograms.put(recorded.getId(), recorded); - RecordedProgram old = oldRecordedPrograms.remove(recorded.getId()); - if (old == null) { - notifyRecordedProgramAdded(recorded); + if (isInputAvailable(recorded.getInputId())) { + mRecordedPrograms.put(recorded.getId(), recorded); + if (oldRecordedPrograms.remove(recorded.getId()) == null) { + addedRecordedPrograms.add(recorded); + } else { + changedRecordedPrograms.add(recorded); + } } else { - notifyRecordedProgramChanged(recorded); + mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded); } } - for (RecordedProgram recorded : oldRecordedPrograms.values()) { - notifyRecordedProgramRemoved(recorded); + if (!addedRecordedPrograms.isEmpty()) { + notifyRecordedProgramsAdded(RecordedProgram.toArray(addedRecordedPrograms)); + } + if (!changedRecordedPrograms.isEmpty()) { + notifyRecordedProgramsChanged(RecordedProgram.toArray(changedRecordedPrograms)); + } + if (!oldRecordedPrograms.isEmpty()) { + notifyRecordedProgramsRemoved( + RecordedProgram.toArray(oldRecordedPrograms.values())); } } if (isInitialized()) { SeriesRecordingScheduler.getInstance(mContext).start(); } } else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) { + if (!mRecordedProgramLoadFinished) { + return; + } long id = ContentUris.parseId(uri); if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms); if (recordedPrograms == null || recordedPrograms.isEmpty()) { + mRecordedProgramsForRemovedInput.remove(id); RecordedProgram old = mRecordedPrograms.remove(id); if (old != null) { - notifyRecordedProgramRemoved(old); - } else { - Log.w(TAG, "Could not find old version of deleted program #" + id); + notifyRecordedProgramsRemoved(old); } } else { - RecordedProgram newRecorded = recordedPrograms.get(0); - RecordedProgram old = mRecordedPrograms.put(id, newRecorded); - if (old == null) { - notifyRecordedProgramAdded(newRecorded); + RecordedProgram recordedProgram = recordedPrograms.get(0); + if (isInputAvailable(recordedProgram.getInputId())) { + RecordedProgram old = mRecordedPrograms.put(id, recordedProgram); + if (old == null) { + notifyRecordedProgramsAdded(recordedProgram); + } else { + notifyRecordedProgramsChanged(recordedProgram); + } } else { - notifyRecordedProgramChanged(newRecorded); + mRecordedProgramsForRemovedInput.put(id, recordedProgram); } } } @@ -432,7 +553,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { @Override public void addScheduledRecording(ScheduledRecording... schedules) { for (ScheduledRecording r : schedules) { - r.setId(IdGenerator.SCHEDULED_RECORDING.newId()); + if (r.getId() == ScheduledRecording.ID_NOT_SET) { + r.setId(IdGenerator.SCHEDULED_RECORDING.newId()); + } mScheduledRecordings.put(r.getId(), r); if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { mProgramId2ScheduledRecordings.put(r.getProgramId(), r); @@ -450,7 +573,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { for (SeriesRecording r : seriesRecordings) { r.setId(IdGenerator.SERIES_RECORDING.newId()); mSeriesRecordings.put(r.getId(), r); - mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + SeriesRecording previousSeries = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + SoftPreconditions.checkArgument(previousSeries == null, TAG, "Attempt to add series" + + " recording with the duplicate series ID: " + r.getSeriesId()); } if (mDvrLoadFinished) { notifySeriesRecordingAdded(seriesRecordings); @@ -463,18 +588,22 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { removeScheduledRecording(false, schedules); } - private void removeScheduledRecording(boolean forceDelete, ScheduledRecording... schedules) { + @Override + public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>(); for (ScheduledRecording r : schedules) { mScheduledRecordings.remove(r.getId()); - if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) { - mProgramId2ScheduledRecordings.remove(r.getProgramId()); - } - // If it belongs to the series recording and it's not started yet, do not delete. - // Instead mark deleted. - if (!forceDelete && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET - && r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + getDeletedScheduleMap().remove(r.getId()); + mProgramId2ScheduledRecordings.remove(r.getProgramId()); + boolean isScheduleForRemovedInput = + mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null; + // If it belongs to the series recording and it's not started yet, just mark delete + // instead of deleting it. + if (!isScheduleForRemovedInput && !forceRemove + && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET + && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || r.getState() == ScheduledRecording.STATE_RECORDING_CANCELED)) { SoftPreconditions.checkState(r.getProgramId() != ScheduledRecording.ID_NOT_SET); ScheduledRecording deleted = ScheduledRecording.buildFrom(r) .setState(ScheduledRecording.STATE_RECORDING_DELETED).build(); @@ -538,8 +667,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { updateScheduledRecording(true, schedules); } - private void updateScheduledRecording(boolean updateDb, - final ScheduledRecording... schedules) { + private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) { List<ScheduledRecording> toUpdate = new ArrayList<>(); for (ScheduledRecording r : schedules) { if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG, @@ -550,9 +678,8 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { ScheduledRecording oldScheduledRecording = mScheduledRecordings.put(r.getId(), r); // The channel ID should not be changed. SoftPreconditions.checkState(r.getChannelId() == oldScheduledRecording.getChannelId()); - if (DEBUG) Log.d(TAG, "Updating " + oldScheduledRecording + " with " + r); long programId = r.getProgramId(); - if (oldScheduledRecording != null && oldScheduledRecording.getProgramId() != programId + if (oldScheduledRecording.getProgramId() != programId && oldScheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) { ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings .get(oldScheduledRecording.getProgramId()); @@ -565,6 +692,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mProgramId2ScheduledRecordings.put(programId, r); } } + if (toUpdate.isEmpty()) { + return; + } ScheduledRecording[] scheduleArray = ScheduledRecording.toArray(toUpdate); if (mDvrLoadFinished) { notifyScheduledRecordingStatusChanged(scheduleArray); @@ -572,17 +702,16 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (updateDb) { new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray); } - removeDeletedSchedules(scheduleArray); + removeDeletedSchedules(schedules); } @Override public void updateSeriesRecording(final SeriesRecording... seriesRecordings) { for (SeriesRecording r : seriesRecordings) { - SeriesRecording old = mSeriesRecordings.put(r.getId(), r); - if (old != null) { - mSeriesId2SeriesRecordings.remove(old.getSeriesId()); - } - mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r); + SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); + SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be" + + " updated: " + r); } if (mDvrLoadFinished) { notifySeriesRecordingChanged(seriesRecordings); @@ -590,6 +719,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { new AsyncUpdateSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings); } + private boolean isInputAvailable(String inputId) { + return mInputManager.hasTvInputInfo(inputId) + && (!Utils.isBundledInput(inputId) || mStorageStatusManager.isStorageMounted()); + } + private void removeDeletedSchedules(ScheduledRecording... addedSchedules) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); for (ScheduledRecording r : addedSchedules) { @@ -625,6 +759,148 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } + private void unhideInput(String inputId) { + if (DEBUG) Log.d(TAG, "unhideInput " + inputId); + List<ScheduledRecording> movedSchedules = + moveElements(mScheduledRecordingsForRemovedInput, mScheduledRecordings, + new Filter<ScheduledRecording>() { + @Override + public boolean filter(ScheduledRecording r) { + return r.getInputId().equals(inputId); + } + }); + List<SeriesRecording> movedSeriesRecordings = + moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, + new Filter<SeriesRecording>() { + @Override + public boolean filter(SeriesRecording r) { + return r.getInputId().equals(inputId); + } + }); + List<RecordedProgram> movedRecordedPrograms = + moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms, + new Filter<RecordedProgram>() { + @Override + public boolean filter(RecordedProgram r) { + return r.getInputId().equals(inputId); + } + }); + if (!movedSchedules.isEmpty()) { + for (ScheduledRecording schedule : movedSchedules) { + mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule); + } + } + if (!movedSeriesRecordings.isEmpty()) { + for (SeriesRecording seriesRecording : movedSeriesRecordings) { + mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording); + } + } + // Notify after all the data are moved. + if (!movedSchedules.isEmpty()) { + notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules)); + } + if (!movedSeriesRecordings.isEmpty()) { + notifySeriesRecordingAdded(SeriesRecording.toArray(movedSeriesRecordings)); + } + if (!movedRecordedPrograms.isEmpty()) { + notifyRecordedProgramsAdded(RecordedProgram.toArray(movedRecordedPrograms)); + } + } + + private void hideInput(String inputId) { + if (DEBUG) Log.d(TAG, "hideInput " + inputId); + List<ScheduledRecording> movedSchedules = + moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput, + new Filter<ScheduledRecording>() { + @Override + public boolean filter(ScheduledRecording r) { + return r.getInputId().equals(inputId); + } + }); + List<SeriesRecording> movedSeriesRecordings = + moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput, + new Filter<SeriesRecording>() { + @Override + public boolean filter(SeriesRecording r) { + return r.getInputId().equals(inputId); + } + }); + List<RecordedProgram> movedRecordedPrograms = + moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput, + new Filter<RecordedProgram>() { + @Override + public boolean filter(RecordedProgram r) { + return r.getInputId().equals(inputId); + } + }); + if (!movedSchedules.isEmpty()) { + for (ScheduledRecording schedule : movedSchedules) { + mProgramId2ScheduledRecordings.remove(schedule.getProgramId()); + } + } + if (!movedSeriesRecordings.isEmpty()) { + for (SeriesRecording seriesRecording : movedSeriesRecordings) { + mSeriesId2SeriesRecordings.remove(seriesRecording.getSeriesId()); + } + } + // Notify after all the data are moved. + if (!movedSchedules.isEmpty()) { + notifyScheduledRecordingRemoved(ScheduledRecording.toArray(movedSchedules)); + } + if (!movedSeriesRecordings.isEmpty()) { + notifySeriesRecordingRemoved(SeriesRecording.toArray(movedSeriesRecordings)); + } + if (!movedRecordedPrograms.isEmpty()) { + notifyRecordedProgramsRemoved(RecordedProgram.toArray(movedRecordedPrograms)); + } + } + + @Override + public void forgetStorage(String inputId) { + List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); + for (Iterator<ScheduledRecording> i = + mScheduledRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) { + ScheduledRecording r = i.next(); + if (inputId.equals(r.getInputId())) { + schedulesToDelete.add(r); + i.remove(); + } + } + List<SeriesRecording> seriesRecordingsToDelete = new ArrayList<>(); + for (Iterator<SeriesRecording> i = + mSeriesRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) { + SeriesRecording r = i.next(); + if (inputId.equals(r.getInputId())) { + seriesRecordingsToDelete.add(r); + i.remove(); + } + } + for (Iterator<RecordedProgram> i = + mRecordedProgramsForRemovedInput.values().iterator(); i.hasNext(); ) { + if (inputId.equals(i.next().getInputId())) { + i.remove(); + } + } + new AsyncDeleteScheduleTask(mContext).executeOnDbThread( + ScheduledRecording.toArray(schedulesToDelete)); + new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread( + SeriesRecording.toArray(seriesRecordingsToDelete)); + new AsyncDbTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + ContentResolver resolver = mContext.getContentResolver(); + String args[] = { inputId }; + try { + resolver.delete(RecordedPrograms.CONTENT_URI, + RecordedPrograms.COLUMN_INPUT_ID + " = ?", args); + } catch (SQLiteException e) { + Log.e(TAG, "Failed to delete recorded programs for inputId: " + inputId, e); + } + return null; + } + }.executeOnDbThread(); + } + private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask { private final Uri mUri; diff --git a/src/com/android/tv/dvr/DvrDbSync.java b/src/com/android/tv/dvr/DvrDbSync.java index baa7f3d9..df181455 100644 --- a/src/com/android/tv/dvr/DvrDbSync.java +++ b/src/com/android/tv/dvr/DvrDbSync.java @@ -28,73 +28,132 @@ import android.os.Handler; import android.os.Looper; import android.support.annotation.MainThread; import android.support.annotation.VisibleForTesting; +import android.util.Log; +import com.android.tv.TvApplication; +import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask; import com.android.tv.util.TvProviderUriMatcher; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; +import java.util.List; import java.util.Objects; import java.util.Queue; +import java.util.Set; /** * A class to synchronizes DVR DB with TvProvider. + * + * <p>The current implementation of AsyncDbTask allows only one task to run at a time, and all the + * other tasks are blocked until the current one finishes. As this class performs the low priority + * jobs which take long time, it should not block others if possible. For this reason, only one + * program is queried at a time and others are queued and will be executed on the other + * AsyncDbTask's after the current one finishes to minimize the execution time of one AsyncDbTask. */ @MainThread @TargetApi(Build.VERSION_CODES.N) class DvrDbSync { + private static final String TAG = "DvrDbSync"; + private static final boolean DEBUG = false; + private final Context mContext; private final DvrDataManagerImpl mDataManager; - private UpdateProgramTask mUpdateProgramTask; + private final ChannelDataManager mChannelDataManager; private final Queue<Long> mProgramIdQueue = new LinkedList<>(); - private final ContentObserver mProgramsContentObserver = new ContentObserver(new Handler( + private QueryProgramTask mQueryProgramTask; + private final SeriesRecordingScheduler mSeriesRecordingScheduler; + private final ContentObserver mContentObserver = new ContentObserver(new Handler( Looper.getMainLooper())) { @SuppressLint("SwitchIntDef") @Override public void onChange(boolean selfChange, Uri uri) { switch (TvProviderUriMatcher.match(uri)) { case TvProviderUriMatcher.MATCH_PROGRAM: + if (DEBUG) Log.d(TAG, "onProgramsUpdated"); onProgramsUpdated(); break; case TvProviderUriMatcher.MATCH_PROGRAM_ID: - addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId( - ContentUris.parseId(uri))); + if (DEBUG) { + Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri)); + } + onProgramUpdated(ContentUris.parseId(uri)); break; } } }; + + private final ChannelDataManager.Listener mChannelDataManagerListener = + new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { + start(); + } + + @Override + public void onChannelListUpdated() { + onChannelsUpdated(); + } + + @Override + public void onChannelBrowsableChanged() { } + }; + private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() { @Override public void onScheduledRecordingAdded(ScheduledRecording... schedules) { for (ScheduledRecording schedule : schedules) { addProgramIdToCheckIfNeeded(schedule); } + startNextUpdateIfNeeded(); } @Override - public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { } + public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + mProgramIdQueue.remove(schedule.getProgramId()); + } + } @Override public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { for (ScheduledRecording schedule : schedules) { mProgramIdQueue.remove(schedule.getProgramId()); + addProgramIdToCheckIfNeeded(schedule); } + startNextUpdateIfNeeded(); } }; - public DvrDbSync(Context context, DvrDataManagerImpl dataManager) { + DvrDbSync(Context context, DvrDataManagerImpl dataManager) { + this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager()); + } + + @VisibleForTesting + DvrDbSync(Context context, DvrDataManagerImpl dataManager, + ChannelDataManager channelDataManager) { mContext = context; mDataManager = dataManager; + mChannelDataManager = channelDataManager; + mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context); } /** * Starts the DB sync. */ public void start() { + if (!mChannelDataManager.isDbLoadFinished()) { + mChannelDataManager.addListener(mChannelDataManagerListener); + return; + } mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, - mProgramsContentObserver); + mContentObserver); mDataManager.addScheduledRecordingListener(mScheduleListener); + onChannelsUpdated(); onProgramsUpdated(); } @@ -103,17 +162,51 @@ class DvrDbSync { */ public void stop() { mProgramIdQueue.clear(); - if (mUpdateProgramTask != null) { - mUpdateProgramTask.cancel(true); + if (mQueryProgramTask != null) { + mQueryProgramTask.cancel(true); } + mChannelDataManager.removeListener(mChannelDataManagerListener); mDataManager.removeScheduledRecordingListener(mScheduleListener); - mContext.getContentResolver().unregisterContentObserver(mProgramsContentObserver); + mContext.getContentResolver().unregisterContentObserver(mContentObserver); + } + + private void onChannelsUpdated() { + List<SeriesRecording> seriesRecordingsToUpdate = new ArrayList<>(); + for (SeriesRecording r : mDataManager.getSeriesRecordings()) { + if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE + && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { + seriesRecordingsToUpdate.add(SeriesRecording.buildFrom(r) + .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL) + .setState(SeriesRecording.STATE_SERIES_STOPPED).build()); + } + } + if (!seriesRecordingsToUpdate.isEmpty()) { + mDataManager.updateSeriesRecording( + SeriesRecording.toArray(seriesRecordingsToUpdate)); + } + List<ScheduledRecording> schedulesToRemove = new ArrayList<>(); + for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { + if (!mChannelDataManager.doesChannelExistInDb(r.getChannelId())) { + schedulesToRemove.add(r); + mProgramIdQueue.remove(r.getProgramId()); + } + } + if (!schedulesToRemove.isEmpty()) { + mDataManager.removeScheduledRecording( + ScheduledRecording.toArray(schedulesToRemove)); + } } private void onProgramsUpdated() { - for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) { + for (ScheduledRecording schedule : mDataManager.getAvailableScheduledRecordings()) { addProgramIdToCheckIfNeeded(schedule); } + startNextUpdateIfNeeded(); + } + + private void onProgramUpdated(long programId) { + addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(programId)); + startNextUpdateIfNeeded(); } private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) { @@ -125,34 +218,48 @@ class DvrDbSync { && !mProgramIdQueue.contains(programId) && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId); mProgramIdQueue.offer(programId); - startNextUpdateIfNeeded(); + // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the + // schedule updates finish. + // Note that the SeriesRecordingScheduler should be paused even though the program to + // check is not episodic because it can be changed to the episodic program after the + // update, which affect the SeriesRecordingScheduler. + mSeriesRecordingScheduler.pauseUpdate(); } } private void startNextUpdateIfNeeded() { - if (mProgramIdQueue.isEmpty()) { + if (mQueryProgramTask != null && !mQueryProgramTask.isCancelled()) { return; } - if (mUpdateProgramTask == null || mUpdateProgramTask.isCancelled()) { - mUpdateProgramTask = new UpdateProgramTask(mProgramIdQueue.poll()); - mUpdateProgramTask.executeOnDbThread(); + if (!mProgramIdQueue.isEmpty()) { + if (DEBUG) Log.d(TAG, "Program ID dequeued: " + mProgramIdQueue.peek()); + mQueryProgramTask = new QueryProgramTask(mProgramIdQueue.poll()); + mQueryProgramTask.executeOnDbThread(); + } else { + mSeriesRecordingScheduler.resumeUpdate(); } } @VisibleForTesting void handleUpdateProgram(Program program, long programId) { + Set<SeriesRecording> seriesRecordingsToUpdate = new HashSet<>(); ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId); if (schedule != null && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { if (program == null) { mDataManager.removeScheduledRecording(schedule); + if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); + if (seriesRecording != null) { + seriesRecordingsToUpdate.add(seriesRecording); + } + } } else { long currentTimeMs = System.currentTimeMillis(); - // Change start time only when the recording start time has not passed. - boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs - && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule) .setEndTimeMs(program.getEndTimeUtcMillis()) .setSeasonNumber(program.getSeasonNumber()) @@ -162,10 +269,51 @@ class DvrDbSync { .setProgramLongDescription(program.getLongDescription()) .setProgramPosterArtUri(program.getPosterArtUri()) .setProgramThumbnailUri(program.getThumbnailUri()); + boolean needUpdate = false; + // Check the series recording. + SeriesRecording seriesRecordingForOldSchedule = + mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); + if (program.getSeriesId() != null) { + // New program belongs to a series. + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(program.getSeriesId()); + if (seriesRecording == null) { + // The new program is episodic while the previous one isn't. + SeriesRecording newSeriesRecording = TvApplication.getSingletons(mContext) + .getDvrManager().addSeriesRecording(program, + Collections.singletonList(program), + SeriesRecording.STATE_SERIES_STOPPED); + builder.setSeriesRecordingId(newSeriesRecording.getId()); + needUpdate = true; + } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { + // The new program belongs to the other series. + builder.setSeriesRecordingId(seriesRecording.getId()); + needUpdate = true; + seriesRecordingsToUpdate.add(seriesRecording); + if (seriesRecordingForOldSchedule != null) { + seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); + } + } else if (!Objects.equals(schedule.getSeasonNumber(), + program.getSeasonNumber()) + || !Objects.equals(schedule.getEpisodeNumber(), + program.getEpisodeNumber())) { + // The episode number has been changed. + if (seriesRecordingForOldSchedule != null) { + seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); + } + } + } else if (seriesRecordingForOldSchedule != null) { + // Old program belongs to a series but the new one doesn't. + seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); + } + // Change start time only when the recording start time has not passed. + boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs + && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); if (needToChangeStartTime) { - mDataManager.updateScheduledRecording( - builder.setStartTimeMs(program.getStartTimeUtcMillis()).build()); - } else if (schedule.getEndTimeMs() != program.getEndTimeUtcMillis() + builder.setStartTimeMs(program.getStartTimeUtcMillis()); + needUpdate = true; + } + if (needUpdate || schedule.getEndTimeMs() != program.getEndTimeUtcMillis() || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber()) || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber()) || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle()) @@ -179,27 +327,35 @@ class DvrDbSync { program.getThumbnailUri())) { mDataManager.updateScheduledRecording(builder.build()); } + if (!seriesRecordingsToUpdate.isEmpty()) { + // The series recordings will be updated after it's resumed. + mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate); + } } } } - private class UpdateProgramTask extends AsyncQueryProgramTask { + private class QueryProgramTask extends AsyncQueryProgramTask { private final long mProgramId; - public UpdateProgramTask(long programId) { + QueryProgramTask(long programId) { super(mContext.getContentResolver(), programId); mProgramId = programId; } @Override protected void onCancelled(Program program) { - mUpdateProgramTask = null; + if (mQueryProgramTask == this) { + mQueryProgramTask = null; + } startNextUpdateIfNeeded(); } @Override protected void onPostExecute(Program program) { - mUpdateProgramTask = null; + if (mQueryProgramTask == this) { + mQueryProgramTask = null; + } handleUpdateProgram(program, mProgramId); startNextUpdateIfNeeded(); } diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java index 48ca6eee..5fa6f90f 100644 --- a/src/com/android/tv/dvr/DvrManager.java +++ b/src/com/android/tv/dvr/DvrManager.java @@ -17,29 +17,36 @@ package com.android.tv.dvr; import android.annotation.TargetApi; +import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; +import android.content.OperationApplicationException; import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Handler; +import android.os.RemoteException; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.util.Log; +import android.util.Range; import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; -import com.android.tv.dvr.SeriesRecordingScheduler.ProgramLoadCallback; +import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; +import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; +import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener; +import com.android.tv.dvr.SeriesRecording.SeriesState; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.Utils; @@ -63,7 +70,6 @@ public class DvrManager { private static final boolean DEBUG = false; private final WritableDvrDataManager mDataManager; - private final ChannelDataManager mChannelDataManager; private final DvrScheduleManager mScheduleManager; // @GuardedBy("mListener") private final Map<Listener, Handler> mListener = new HashMap<>(); @@ -71,64 +77,124 @@ public class DvrManager { public DvrManager(Context context) { SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG); + mAppContext = context.getApplicationContext(); ApplicationSingletons appSingletons = TvApplication.getSingletons(context); mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager(); - mAppContext = context.getApplicationContext(); - mChannelDataManager = appSingletons.getChannelDataManager(); mScheduleManager = appSingletons.getDvrScheduleManager(); + if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) { + createSeriesRecordingsForRecordedProgramsIfNeeded(mDataManager.getRecordedPrograms()); + } else { + // No need to handle DVR schedule load finished because schedule manager is initialized + // after the all the schedules are loaded. + if (!mDataManager.isRecordedProgramLoadFinished()) { + mDataManager.addRecordedProgramLoadFinishedListener( + new OnRecordedProgramLoadFinishedListener() { + @Override + public void onRecordedProgramLoadFinished() { + mDataManager.removeRecordedProgramLoadFinishedListener(this); + if (mDataManager.isInitialized() + && mScheduleManager.isInitialized()) { + createSeriesRecordingsForRecordedProgramsIfNeeded( + mDataManager.getRecordedPrograms()); + } + } + }); + } + if (!mScheduleManager.isInitialized()) { + mScheduleManager.addOnInitializeListener(new OnInitializeListener() { + @Override + public void onInitialize() { + mScheduleManager.removeOnInitializeListener(this); + if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) { + createSeriesRecordingsForRecordedProgramsIfNeeded( + mDataManager.getRecordedPrograms()); + } + } + }); + } + } + mDataManager.addRecordedProgramListener(new RecordedProgramListener() { + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + if (!mDataManager.isInitialized() || !mScheduleManager.isInitialized()) { + return; + } + for (RecordedProgram recordedProgram : recordedPrograms) { + createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram); + } + } + + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } + + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + // Removing series recording is handled in the SeriesRecordingDetailsFragment. + } + }); + } + + private void createSeriesRecordingsForRecordedProgramsIfNeeded( + List<RecordedProgram> recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram); + } + } + + private void createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram) { + if (recordedProgram.getSeriesId() != null) { + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(recordedProgram.getSeriesId()); + if (seriesRecording == null) { + addSeriesRecording(recordedProgram); + } + } } /** * Schedules a recording for {@code program}. */ - public void addSchedule(Program program) { + public ScheduledRecording addSchedule(Program program) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - return; - } - TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program); - if (input == null) { - Log.e(TAG, "Can't find input for program: " + program); - return; + return null; } - ScheduledRecording schedule; SeriesRecording seriesRecording = getSeriesRecording(program); - if (seriesRecording == null) { - schedule = createScheduledRecordingBuilder(input.getId(), program) - .setPriority(mScheduleManager.suggestNewPriority()) - .build(); - } else { - schedule = createScheduledRecordingBuilder(input.getId(), program) - .setPriority(seriesRecording.getPriority()) - .setSeriesRecordingId(seriesRecording.getId()) - .build(); - } - mDataManager.addScheduledRecording(schedule); + return addSchedule(program, seriesRecording == null + ? mScheduleManager.suggestNewPriority() + : seriesRecording.getPriority()); } /** - * Schedules a recording for {@code program} instead of the list of recording that conflict. - * - * @param program the program to record - * @param recordingsToOverride the possible empty list of recordings that will not be recorded + * Schedules a recording for {@code program} with the highest priority so that the schedule + * can be recorded. */ - public void addSchedule(Program program, List<ScheduledRecording> recordingsToOverride) { - Log.i(TAG, "Adding scheduled recording of " + program + " instead of " + - recordingsToOverride); + public ScheduledRecording addScheduleWithHighestPriority(Program program) { if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - return; + return null; } + SeriesRecording seriesRecording = getSeriesRecording(program); + return addSchedule(program, seriesRecording == null + ? mScheduleManager.suggestNewPriority() + : mScheduleManager.suggestHighestPriority(seriesRecording.getInputId(), + new Range(program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis()), + seriesRecording.getPriority())); + } + + private ScheduledRecording addSchedule(Program program, long priority) { TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program); if (input == null) { Log.e(TAG, "Can't find input for program: " + program); - return; + return null; } - Collections.sort(recordingsToOverride, ScheduledRecording.PRIORITY_COMPARATOR); - long priority = recordingsToOverride.isEmpty() ? Long.MAX_VALUE - : recordingsToOverride.get(0).getPriority() + 1; - ScheduledRecording r = createScheduledRecordingBuilder(input.getId(), program) + ScheduledRecording schedule; + SeriesRecording seriesRecording = getSeriesRecording(program); + schedule = createScheduledRecordingBuilder(input.getId(), program) .setPriority(priority) + .setSeriesRecordingId(seriesRecording == null ? SeriesRecording.ID_NOT_SET + : seriesRecording.getId()) .build(); - mDataManager.addScheduledRecording(r); + mDataManager.addScheduledRecording(schedule); + return schedule; } /** @@ -148,6 +214,15 @@ public class DvrManager { addScheduleInternal(input.getId(), channel.getId(), startTime, endTime); } + /** + * Adds the schedule. + */ + public void addSchedule(ScheduledRecording schedule) { + if (mDataManager.isDvrScheduleLoadFinished()) { + mDataManager.addScheduledRecording(schedule); + } + } + private void addScheduleInternal(String inputId, long channelId, long startTime, long endTime) { mDataManager.addScheduledRecording(ScheduledRecording .builder(inputId, channelId, startTime, endTime) @@ -156,10 +231,10 @@ public class DvrManager { } /** - * Adds a new series recording and schedules for the programs. + * Adds a new series recording and schedules for the programs with the initial state. */ public SeriesRecording addSeriesRecording(Program selectedProgram, - List<Program> programsToSchedule) { + List<Program> programsToSchedule, @SeriesState int initialState) { Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: " + programsToSchedule); if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { @@ -172,6 +247,7 @@ public class DvrManager { } SeriesRecording seriesRecording = SeriesRecording.builder(input.getId(), selectedProgram) .setPriority(mScheduleManager.suggestNewSeriesPriority()) + .setState(initialState) .build(); mDataManager.addSeriesRecording(seriesRecording); // The schedules for the recorded programs should be added not to create the schedule the @@ -181,6 +257,18 @@ public class DvrManager { return seriesRecording; } + private void addSeriesRecording(RecordedProgram recordedProgram) { + SeriesRecording seriesRecording = + SeriesRecording.builder(recordedProgram.getInputId(), recordedProgram) + .setPriority(mScheduleManager.suggestNewSeriesPriority()) + .setState(SeriesRecording.STATE_SERIES_STOPPED) + .build(); + mDataManager.addSeriesRecording(seriesRecording); + // The schedules for the recorded programs should be added not to create the schedule the + // duplicate episodes. + addRecordedProgramToSeriesRecording(seriesRecording); + } + private void addRecordedProgramToSeriesRecording(SeriesRecording series) { List<ScheduledRecording> toAdd = new ArrayList<>(); for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) { @@ -221,11 +309,8 @@ public class DvrManager { mDataManager.getScheduledRecordingForProgramId(program.getId()); if (scheduleWithSameProgram != null) { if (scheduleWithSameProgram.getState() - == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || scheduleWithSameProgram.getState() - == ScheduledRecording.STATE_RECORDING_CANCELED) { + == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram) - .setPriority(series.getPriority()) .setSeriesRecordingId(series.getId()) .build(); if (!r.equals(scheduleWithSameProgram)) { @@ -233,15 +318,10 @@ public class DvrManager { } } } else { - ScheduledRecording.Builder scheduledRecordingBuilder = - createScheduledRecordingBuilder(input.getId(), program) + toAdd.add(createScheduledRecordingBuilder(input.getId(), program) .setPriority(series.getPriority()) - .setSeriesRecordingId(series.getId()); - if (series.getState() == SeriesRecording.STATE_SERIES_CANCELED) { - scheduledRecordingBuilder.setState( - ScheduledRecording.STATE_RECORDING_CANCELED); - } - toAdd.add(scheduledRecordingBuilder.build()); + .setSeriesRecordingId(series.getId()) + .build()); } } if (!toAdd.isEmpty()) { @@ -257,29 +337,33 @@ public class DvrManager { */ public void updateSeriesRecording(SeriesRecording series) { if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - // TODO: revise this method. b/30946239 - boolean isPreviousCanceled = false; - long oldPriority = 0; + SeriesRecordingScheduler scheduler = SeriesRecordingScheduler.getInstance(mAppContext); + scheduler.pauseUpdate(); SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId()); if (previousSeries != null) { - isPreviousCanceled = previousSeries.getState() - == SeriesRecording.STATE_SERIES_CANCELED; - oldPriority = previousSeries.getPriority(); + if (previousSeries.getChannelOption() != series.getChannelOption() + || (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE + && previousSeries.getChannelId() != series.getChannelId())) { + List<ScheduledRecording> schedules = + mDataManager.getScheduledRecordings(series.getId()); + List<ScheduledRecording> schedulesToRemove = new ArrayList<>(); + for (ScheduledRecording schedule : schedules) { + if (schedule.isNotStarted()) { + schedulesToRemove.add(schedule); + } + } + mDataManager.removeScheduledRecording(true, + ScheduledRecording.toArray(schedulesToRemove)); + } } mDataManager.updateSeriesRecording(series); - if (!isPreviousCanceled && series.getState() == SeriesRecording.STATE_SERIES_CANCELED) { - cancelScheduleToSeriesRecording(series); - } else if (isPreviousCanceled - && series.getState() == SeriesRecording.STATE_SERIES_NORMAL) { - resumeScheduleToSeriesRecording(series); - } - if (oldPriority != series.getPriority()) { + if (previousSeries == null + || previousSeries.getPriority() != series.getPriority()) { long priority = series.getPriority(); List<ScheduledRecording> schedulesToUpdate = new ArrayList<>(); for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(series.getId())) { - if (schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS - && schedule.getStartTimeMs() > System.currentTimeMillis()) { + if (schedule.isNotStarted()) { schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule) .setPriority(priority).build()); } @@ -289,59 +373,10 @@ public class DvrManager { ScheduledRecording.toArray(schedulesToUpdate)); } } + scheduler.resumeUpdate(); } } - private void cancelScheduleToSeriesRecording(SeriesRecording series) { - List<ScheduledRecording> allRecordings = mDataManager.getAvailableScheduledRecordings(); - for (ScheduledRecording recording : allRecordings) { - if (recording.getSeriesRecordingId() == series.getId()) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - stopRecording(recording); - continue; - } - updateScheduledRecording(ScheduledRecording.buildFrom(recording).setState - (ScheduledRecording.STATE_RECORDING_CANCELED).build()); - } - } - } - - private void resumeScheduleToSeriesRecording(SeriesRecording series) { - List<ScheduledRecording> allRecording = mDataManager - .getAvailableAndCanceledScheduledRecordings(); - for (ScheduledRecording recording : allRecording) { - if (recording.getSeriesRecordingId() == series.getId()) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_CANCELED && - recording.getEndTimeMs() > System.currentTimeMillis()) { - updateScheduledRecording(ScheduledRecording.buildFrom(recording) - .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED).build()); - } - } - } - } - - /** - * Queries the programs which belong to the same series as {@code seriesProgram}. - * <p> - * It's done in the background because it needs the DB access, and the callback will be called - * when it finishes. - */ - public void queryProgramsForSeries(Program seriesProgram, ProgramLoadCallback callback) { - if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { - callback.onProgramLoadFinished(Collections.emptyList()); - return; - } - TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, seriesProgram); - if (input == null) { - Log.e(TAG, "Can't find input for program: " + seriesProgram); - return; - } - SeriesRecordingScheduler.getInstance(mAppContext).queryPrograms( - SeriesRecording.builder(input.getId(), seriesProgram) - .setPriority(mScheduleManager.suggestNewPriority()) - .build(), callback); - } - /** * Removes the series recording and all the corresponding schedules which are not started yet. */ @@ -365,6 +400,33 @@ public class DvrManager { } /** + * Returns true, if the series recording can be removed. If a series recording is NORMAL state + * or has recordings or schedules, it cannot be removed. + */ + public boolean canRemoveSeriesRecording(long seriesRecordingId) { + SeriesRecording seriesRecording = mDataManager.getSeriesRecording(seriesRecordingId); + if (seriesRecording == null) { + return false; + } + if (!seriesRecording.isStopped()) { + return false; + } + for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { + if (r.getSeriesRecordingId() == seriesRecordingId) { + return false; + } + } + String seriesId = seriesRecording.getSeriesId(); + SoftPreconditions.checkNotNull(seriesId); + for (RecordedProgram r : mDataManager.getRecordedPrograms()) { + if (seriesId.equals(r.getSeriesId())) { + return false; + } + } + return true; + } + + /** * Stops the currently recorded program */ public void stopRecording(final ScheduledRecording recording) { @@ -401,6 +463,23 @@ public class DvrManager { } /** + * Removes scheduled recordings without changing to the DELETED state. + */ + public void forceRemoveScheduledRecording(ScheduledRecording... schedules) { + Log.i(TAG, "Force removing " + Arrays.asList(schedules)); + if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { + return; + } + for (ScheduledRecording r : schedules) { + if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + stopRecording(r); + } else { + mDataManager.removeScheduledRecording(true, r); + } + } + } + + /** * Removes the recorded program. It deletes the file if possible. */ public void removeRecordedProgram(Uri recordedProgramUri) { @@ -434,51 +513,51 @@ public class DvrManager { @Override protected Void doInBackground(Void... params) { ContentResolver resolver = mAppContext.getContentResolver(); - resolver.delete(recordedProgram.getUri(), null, null); - try { - Uri dataUri = recordedProgram.getDataUri(); - if (dataUri != null && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme()) - && dataUri.getPath() != null) { - File recordedProgramPath = new File(dataUri.getPath()); - if (!recordedProgramPath.exists()) { - if (DEBUG) Log.d(TAG, "File to delete not exist: " - + recordedProgramPath); - } else { - Utils.deleteDirOrFile(recordedProgramPath); - if (DEBUG) { - Log.d(TAG, "Sucessfully deleted files of the recorded program: " - + recordedProgram.getDataUri()); - } + int deletedCounts = resolver.delete(recordedProgram.getUri(), null, null); + if (deletedCounts > 0) { + // TODO: executeOnExecutor should be called on the main thread. + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + removeRecordedData(recordedProgram.getDataUri()); + return null; } - } - } catch (SecurityException e) { - if (DEBUG) { - Log.d(TAG, "To delete " + recordedProgram - + "\nyou should manually delete video data at" - + "\nadb shell rm -rf " + recordedProgram.getDataUri()); - } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } return null; } }.executeOnDbThread(); } - /** - * Remove all recorded programs due to missing storage. - * - * @param inputId for the recorded programs to remove - */ - public void removeRecordedProgramByMissingStorage(final String inputId) { - if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { - return; + public void removeRecordedPrograms(List<Long> recordedProgramIds) { + final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>(); + final List<Uri> dataUris = new ArrayList<>(); + for (Long rId : recordedProgramIds) { + RecordedProgram r = mDataManager.getRecordedProgram(rId); + if (r != null) { + dataUris.add(r.getDataUri()); + dbOperations.add(ContentProviderOperation.newDelete(r.getUri()).build()); + } } new AsyncDbTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { ContentResolver resolver = mAppContext.getContentResolver(); - String args[] = { inputId }; - resolver.delete(TvContract.RecordedPrograms.CONTENT_URI, - TvContract.RecordedPrograms.COLUMN_INPUT_ID + " = ?", args); + try { + resolver.applyBatch(TvContract.AUTHORITY, dbOperations); + // TODO: executeOnExecutor should be called on the main thread. + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + for (Uri dataUri : dataUris) { + removeRecordedData(dataUri); + } + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (RemoteException | OperationApplicationException e) { + Log.w(TAG, "Remove reocrded programs from DB failed.", e); + } return null; } }.executeOnDbThread(); @@ -527,10 +606,9 @@ public class DvrManager { * {@code false}. */ public boolean isConflicting(ScheduledRecording schedule) { - if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - return false; - } - return mScheduleManager.isConflicting(schedule); + return schedule != null + && SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished()) + && mScheduleManager.isConflicting(schedule); } /** @@ -547,20 +625,26 @@ public class DvrManager { } /** - * Returns the earliest end time of the current recording for the TV input. If there are no - * recordings, Long.MAX_VALUE is returned. + * Sets the highest priority to the schedule. */ - public long getEarliestRecordingEndTime(String inputId) { - long result = Long.MAX_VALUE; - for (ScheduledRecording schedule : mDataManager.getStartedRecordings()) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mAppContext, - schedule.getChannelId()); - if (input != null && input.getId().equals(inputId) - && schedule.getEndTimeMs() < result) { - result = schedule.getEndTimeMs(); + public void setHighestPriority(ScheduledRecording schedule) { + if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { + long newPriority = mScheduleManager.suggestHighestPriority(schedule); + if (newPriority != schedule.getPriority()) { + mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule) + .setPriority(newPriority).build()); } } - return result; + } + + /** + * Suggests the higher priority than the schedules which overlap with {@code schedule}. + */ + public long suggestHighestPriority(ScheduledRecording schedule) { + if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { + return mScheduleManager.suggestHighestPriority(schedule); + } + return DvrScheduleManager.DEFAULT_PRIORITY; } /** @@ -622,6 +706,23 @@ public class DvrManager { } /** + * Returns schedules which is available (i.e., isNotStarted or isInProgress) and belongs to + * the series recording {@code seriesRecordingId}. + */ + public List<ScheduledRecording> getAvailableScheduledRecording(long seriesRecordingId) { + if (!mDataManager.isDvrScheduleLoadFinished()) { + return Collections.emptyList(); + } + List<ScheduledRecording> schedules = new ArrayList<>(); + for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(seriesRecordingId)) { + if (schedule.isInProgress() || schedule.isNotStarted()) { + schedules.add(schedule); + } + } + return schedules; + } + + /** * Returns the series recording related to the program. */ @Nullable @@ -704,6 +805,40 @@ public class DvrManager { return null; } + @WorkerThread + private void removeRecordedData(Uri dataUri) { + try { + if (dataUri != null && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme()) + && dataUri.getPath() != null) { + File recordedProgramPath = new File(dataUri.getPath()); + if (!recordedProgramPath.exists()) { + if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath); + } else { + Utils.deleteDirOrFile(recordedProgramPath); + if (DEBUG) { + Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri); + } + } + } + } catch (SecurityException e) { + if (DEBUG) { + Log.d(TAG, "To delete this recorded program, please manually delete video data at" + + "\nadb shell rm -rf " + dataUri); + } + } + } + + /** + * Remove all the records related to the input. + * <p> + * Note that this should be called after the input was removed. + */ + public void forgetStorage(String inputId) { + if (mDataManager.isInitialized()) { + mDataManager.forgetStorage(inputId); + } + } + /** * Listener internally used inside dvr package. */ diff --git a/src/com/android/tv/dvr/DvrPlaybackActivity.java b/src/com/android/tv/dvr/DvrPlaybackActivity.java index 3320e0fd..5deda44a 100644 --- a/src/com/android/tv/dvr/DvrPlaybackActivity.java +++ b/src/com/android/tv/dvr/DvrPlaybackActivity.java @@ -23,6 +23,7 @@ import android.os.Bundle; import android.util.Log; import com.android.tv.R; +import com.android.tv.TvApplication; import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; /** @@ -36,6 +37,7 @@ public class DvrPlaybackActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_playback); diff --git a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java index da815712..9759a856 100644 --- a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java +++ b/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java @@ -17,6 +17,7 @@ package com.android.tv.dvr; import android.app.Activity; +import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.MediaMetadata; @@ -32,8 +33,10 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; import com.android.tv.util.ImageLoader; import com.android.tv.util.TimeShiftUtils; +import com.android.tv.util.Utils; public class DvrPlaybackMediaSessionHelper { private static final String TAG = "DvrPlaybackMediaSessionHelper"; @@ -50,8 +53,8 @@ public class DvrPlaybackMediaSessionHelper { private final DvrWatchedPositionManager mDvrWatchedPositionManager; private final ChannelDataManager mChannelDataManager; - public DvrPlaybackMediaSessionHelper(Activity activity, - String mediaSessionTag, DvrPlayer dvrPlayer) { + public DvrPlaybackMediaSessionHelper(Activity activity, String mediaSessionTag, + DvrPlayer dvrPlayer, DvrPlaybackOverlayFragment overlayFragment) { mActivity = activity; mDvrPlayer = dvrPlayer; mDvrWatchedPositionManager = @@ -71,6 +74,21 @@ public class DvrPlaybackMediaSessionHelper { .setWatchedPosition(mDvrPlayer.getProgram().getId(), positionMs); } } + + @Override + public void onPlaybackEnded() { + // TODO: Deal with watched over recordings in DVR library + RecordedProgram nextEpisode = + overlayFragment.getNextEpisode(mDvrPlayer.getProgram()); + if (nextEpisode == null) { + mDvrPlayer.reset(); + mActivity.finish(); + } else { + Intent intent = new Intent(activity, DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, nextEpisode.getId()); + mActivity.startActivity(intent); + } + } }); initializeMediaSession(mediaSessionTag); } @@ -122,7 +140,7 @@ public class DvrPlaybackMediaSessionHelper { * Checks if the recorded program is the same as now playing one. */ public boolean isCurrentProgram(RecordedProgram program) { - return program == null ? false : program.equals(getProgram()); + return program != null && program.equals(getProgram()); } /** @@ -216,7 +234,7 @@ public class DvrPlaybackMediaSessionHelper { private void updateMediaMetadata(final long programId, final String title, final String subtitle, final long duration, final Bitmap posterArt, final int imageResId) { - new AsyncTask<Void, Void, Void> () { + new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... arg0) { MediaMetadata.Builder builder = new MediaMetadata.Builder(); @@ -252,16 +270,23 @@ public class DvrPlaybackMediaSessionHelper { @Override public void onPlay() { - mDvrPlayer.play(); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.play(); + } } @Override public void onPause() { - mDvrPlayer.pause(); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.pause(); + } } @Override public void onFastForward() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING) { if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { mSpeedLevel++; @@ -277,6 +302,9 @@ public class DvrPlaybackMediaSessionHelper { @Override public void onRewind() { + if (!mDvrPlayer.isPlaybackPrepared()) { + return; + } if (mDvrPlayer.getPlaybackState() == PlaybackState.STATE_REWINDING) { if (mSpeedLevel < TimeShiftUtils.MAX_SPEED_LEVEL) { mSpeedLevel++; @@ -291,7 +319,9 @@ public class DvrPlaybackMediaSessionHelper { @Override public void onSeekTo(long positionMs) { - mDvrPlayer.seekTo(positionMs); + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.seekTo(positionMs); + } } } } diff --git a/src/com/android/tv/dvr/DvrPlayer.java b/src/com/android/tv/dvr/DvrPlayer.java index 027d99f4..5656655c 100644 --- a/src/com/android/tv/dvr/DvrPlayer.java +++ b/src/com/android/tv/dvr/DvrPlayer.java @@ -55,6 +55,8 @@ public class DvrPlayer { private boolean mPauseOnPrepared; private final PlaybackParams mPlaybackParams = new PlaybackParams(); private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); + private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + private boolean mTimeShiftPlayAvailable; public static class DvrPlayerCallback { /** @@ -67,6 +69,10 @@ public class DvrPlayer { * Called when the playback state or the playback speed is changed. */ public void onPlaybackStateChanged(int playbackState, int playbackSpeed) { } + /** + * Called when the playback toward the end. + */ + public void onPlaybackEnded() { } } public interface AspectRatioChangedListener { @@ -208,7 +214,7 @@ public class DvrPlayer { } positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS); if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs); - mTvView.timeShiftSeekTo(positionMs); + mTvView.timeShiftSeekTo(positionMs + mStartPositionMs); if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING || mPlaybackState == PlaybackState.STATE_REWINDING) { mPlaybackState = PlaybackState.STATE_PLAYING; @@ -222,12 +228,14 @@ public class DvrPlayer { */ public void reset() { if (DEBUG) Log.d(TAG, "reset()"); - mTvView.reset(); + mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1); mPlaybackState = PlaybackState.STATE_NONE; + mTvView.reset(); + mTimeShiftPlayAvailable = false; + mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; mTimeShiftCurrentPositionMs = 0; mPlaybackParams.setSpeed(1.0f); mProgram = null; - mCallback.onPlaybackStateChanged(mPlaybackState, 1); } /** @@ -317,43 +325,50 @@ public class DvrPlayer { private void setTvViewCallbacks() { mTvView.setTimeShiftPositionCallback(new TvView.TimeShiftPositionCallback() { @Override + public void onTimeShiftStartPositionChanged(String inputId, long timeMs) { + if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs); + mStartPositionMs = timeMs; + if (mTimeShiftPlayAvailable) { + resumeToWatchedPositionIfNeeded(); + } + } + + @Override public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) { - // Workaround solution for b/29994826: - // prevents rewinding and fast-forwarding over the ends. + if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs); + if (!mTimeShiftPlayAvailable) { + // Workaround of b/31436263 + return; + } + // Workaround of b/32211561, TIF won't report start position when TIS report + // its start position as 0. In that case, we have to do the prework of playback + // on the first time we get current position, and the start position should be 0 + // at that time. + if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mStartPositionMs = 0; + resumeToWatchedPositionIfNeeded(); + } + timeMs -= mStartPositionMs; if (mPlaybackState == PlaybackState.STATE_REWINDING && timeMs <= REWIND_POSITION_MARGIN_MS) { play(); - } else if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING - && timeMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) { - mTvView.timeShiftSeekTo(mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS); - pause(); - } - else { + } else { mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); + if (timeMs >= mProgram.getDurationMillis()) { + pause(); + mCallback.onPlaybackEnded(); + } } } }); mTvView.setCallback(new TvView.TvInputCallback() { @Override public void onTimeShiftStatusChanged(String inputId, int status) { - if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged"); + if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE && mPlaybackState == PlaybackState.STATE_CONNECTING) { - if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - mTvView.timeShiftSeekTo(getRealSeekPosition( - mInitialSeekPositionMs, SEEK_POSITION_MARGIN_MS)); - mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; - } - if (mPauseOnPrepared) { - mTvView.timeShiftPause(); - mPlaybackState = PlaybackState.STATE_PAUSED; - mPauseOnPrepared = false; - } else { - mTvView.timeShiftResume(); - mPlaybackState = PlaybackState.STATE_PLAYING; - } - mCallback.onPlaybackStateChanged(mPlaybackState, 1); + mTimeShiftPlayAvailable = true; } } @@ -390,4 +405,21 @@ public class DvrPlayer { } }); } + + private void resumeToWatchedPositionIfNeeded() { + if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + mTvView.timeShiftSeekTo(getRealSeekPosition(mInitialSeekPositionMs, + SEEK_POSITION_MARGIN_MS) + mStartPositionMs); + mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + } + if (mPauseOnPrepared) { + mTvView.timeShiftPause(); + mPlaybackState = PlaybackState.STATE_PAUSED; + mPauseOnPrepared = false; + } else { + mTvView.timeShiftResume(); + mPlaybackState = PlaybackState.STATE_PLAYING; + } + mCallback.onPlaybackStateChanged(mPlaybackState, 1); + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java index 39be7961..8c40aaa8 100644 --- a/src/com/android/tv/dvr/DvrRecordingService.java +++ b/src/com/android/tv/dvr/DvrRecordingService.java @@ -70,7 +70,6 @@ public class DvrRecordingService extends Service { super.onCreate(); SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); ApplicationSingletons singletons = TvApplication.getSingletons(this); - DvrManager dvrManager = singletons.getDvrManager(); WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager(); AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); @@ -103,7 +102,7 @@ public class DvrRecordingService extends Service { mScheduler.stop(); mScheduler = null; if (mHandlerThread != null) { - mHandlerThread.quit(); + mHandlerThread.quitSafely(); mHandlerThread = null; } super.onDestroy(); diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java index aa77c400..a5851a75 100644 --- a/src/com/android/tv/dvr/DvrScheduleManager.java +++ b/src/com/android/tv/dvr/DvrScheduleManager.java @@ -24,6 +24,7 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.ArraySet; +import android.util.LongSparseArray; import android.util.Range; import com.android.tv.ApplicationSingletons; @@ -34,16 +35,18 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.util.CompositeComparator; import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; /** * A class to manage the schedules. @@ -64,15 +67,34 @@ public class DvrScheduleManager { // The new priority will have the offset from the existing one. private static final long PRIORITY_OFFSET = 1024; + private static final Comparator<ScheduledRecording> RESULT_COMPARATOR = + new CompositeComparator<>( + ScheduledRecording.PRIORITY_COMPARATOR.reversed(), + ScheduledRecording.START_TIME_COMPARATOR, + ScheduledRecording.ID_COMPARATOR.reversed()); + + // The candidate comparator should be the consistent with + // InputTaskScheduler#CANDIDATE_COMPARATOR. + private static final Comparator<ScheduledRecording> CANDIDATE_COMPARATOR = + new CompositeComparator<>( + ScheduledRecording.PRIORITY_COMPARATOR, + ScheduledRecording.END_TIME_COMPARATOR, + ScheduledRecording.ID_COMPARATOR); + private final Context mContext; private final DvrDataManagerImpl mDataManager; private final ChannelDataManager mChannelDataManager; private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>(); - private final Map<String, List<ScheduledRecording>> mInputConflictMap = new HashMap<>(); + // The inner map is a hash map from scheduled recording to its conflicting status, i.e., + // the boolean value true denotes the schedule is just partially conflicting, which means + // although there's conflictit, it might still be recorded partially. + private final Map<String, Map<ScheduledRecording, Boolean>> mInputConflictInfoMap = + new HashMap<>(); private boolean mInitialized; + private final Set<OnInitializeListener> mOnInitializeListeners = new CopyOnWriteArraySet<>(); private final Set<ScheduledRecordingListener> mScheduledRecordingListeners = new ArraySet<>(); private final Set<OnConflictStateChangeListener> mOnConflictStateChangeListeners = new ArraySet<>(); @@ -106,10 +128,13 @@ public class DvrScheduleManager { if (!schedule.isNotStarted() && !schedule.isInProgress()) { continue; } - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, - schedule.getChannelId()); - if (input == null) { + TvInputInfo input = Utils + .getTvInputInfoForInputId(mContext, schedule.getInputId()); + if (!SoftPreconditions.checkArgument(input != null, TAG, + "Input was removed for : " + schedule)) { // Input removed. + mInputScheduleMap.remove(schedule.getInputId()); + mInputConflictInfoMap.remove(schedule.getInputId()); continue; } String inputId = input.getId(); @@ -131,9 +156,11 @@ public class DvrScheduleManager { } for (ScheduledRecording schedule : scheduledRecordings) { TvInputInfo input = Utils - .getTvInputInfoForChannelId(mContext, schedule.getChannelId()); + .getTvInputInfoForInputId(mContext, schedule.getInputId()); if (input == null) { // Input removed. + mInputScheduleMap.remove(schedule.getInputId()); + mInputConflictInfoMap.remove(schedule.getInputId()); continue; } String inputId = input.getId(); @@ -144,6 +171,14 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } } + Map<ScheduledRecording, Boolean> conflictInfo = + mInputConflictInfoMap.get(inputId); + if (conflictInfo != null) { + conflictInfo.remove(schedule); + if (conflictInfo.isEmpty()) { + mInputConflictInfoMap.remove(inputId); + } + } } onSchedulesChanged(); notifyScheduledRecordingRemoved(scheduledRecordings); @@ -157,9 +192,12 @@ public class DvrScheduleManager { } for (ScheduledRecording schedule : scheduledRecordings) { TvInputInfo input = Utils - .getTvInputInfoForChannelId(mContext, schedule.getChannelId()); - if (input == null) { + .getTvInputInfoForInputId(mContext, schedule.getInputId()); + if (!SoftPreconditions.checkArgument(input != null, TAG, + "Input was removed for : " + schedule)) { // Input removed. + mInputScheduleMap.remove(schedule.getInputId()); + mInputConflictInfoMap.remove(schedule.getInputId()); continue; } String inputId = input.getId(); @@ -170,8 +208,7 @@ public class DvrScheduleManager { } // Compare ID because ScheduledRecording.equals() doesn't work if the state // is changed. - Iterator<ScheduledRecording> i = schedules.iterator(); - while (i.hasNext()) { + for (Iterator<ScheduledRecording> i = schedules.iterator(); i.hasNext(); ) { if (i.next().getId() == schedule.getId()) { i.remove(); break; @@ -183,6 +220,24 @@ public class DvrScheduleManager { if (schedules.isEmpty()) { mInputScheduleMap.remove(inputId); } + // Update conflict list as well + Map<ScheduledRecording, Boolean> conflictInfo = + mInputConflictInfoMap.get(inputId); + if (conflictInfo != null) { + // Compare ID because ScheduledRecording.equals() doesn't work if the state + // is changed. + ScheduledRecording oldSchedule = null; + for (ScheduledRecording s : conflictInfo.keySet()) { + if (s.getId() == schedule.getId()) { + oldSchedule = s; + break; + } + } + if (oldSchedule != null) { + conflictInfo.put(schedule, conflictInfo.get(oldSchedule)); + conflictInfo.remove(oldSchedule); + } + } } onSchedulesChanged(); notifyScheduledRecordingStatusChanged(scheduledRecordings); @@ -249,33 +304,39 @@ public class DvrScheduleManager { schedules.add(schedule); } } - mInitialized = true; + if (!mInitialized) { + mInitialized = true; + notifyInitialize(); + } onSchedulesChanged(); } private void onSchedulesChanged() { + // TODO: notify conflict state change when some conflicting recording becomes partially + // conflicting, vice versa. List<ScheduledRecording> addedConflicts = new ArrayList<>(); List<ScheduledRecording> removedConflicts = new ArrayList<>(); for (String inputId : mInputScheduleMap.keySet()) { - List<ScheduledRecording> oldConflicts = mInputConflictMap.get(inputId); + Map<ScheduledRecording, Boolean> oldConflictsInfo = mInputConflictInfoMap.get(inputId); Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>(); - if (oldConflicts != null) { - for (ScheduledRecording r : oldConflicts) { + if (oldConflictsInfo != null) { + for (ScheduledRecording r : oldConflictsInfo.keySet()) { oldConflictMap.put(r.getId(), r); } } - List<ScheduledRecording> conflicts = getConflictingSchedules(inputId); - for (ScheduledRecording r : conflicts) { - if (oldConflictMap.remove(r.getId()) == null) { - addedConflicts.add(r); + Map<ScheduledRecording, Boolean> conflictInfo = getConflictingSchedulesInfo(inputId); + if (conflictInfo.isEmpty()) { + mInputConflictInfoMap.remove(inputId); + } else { + mInputConflictInfoMap.put(inputId, conflictInfo); + List<ScheduledRecording> conflicts = new ArrayList<>(conflictInfo.keySet()); + for (ScheduledRecording r : conflicts) { + if (oldConflictMap.remove(r.getId()) == null) { + addedConflicts.add(r); + } } } removedConflicts.addAll(oldConflictMap.values()); - if (conflicts.isEmpty()) { - mInputConflictMap.remove(inputId); - } else { - mInputConflictMap.put(inputId, conflicts); - } } if (!removedConflicts.isEmpty()) { notifyConflictStateChange(false, ScheduledRecording.toArray(removedConflicts)); @@ -334,6 +395,29 @@ public class DvrScheduleManager { } /** + * Adds a {@link OnInitializeListener}. + */ + public final void addOnInitializeListener(OnInitializeListener listener) { + mOnInitializeListeners.add(listener); + } + + /** + * Removes a {@link OnInitializeListener}. + */ + public final void removeOnInitializeListener(OnInitializeListener listener) { + mOnInitializeListeners.remove(listener); + } + + /** + * Calls {@link OnInitializeListener#onInitialize} for each listener. + */ + private void notifyInitialize() { + for (OnInitializeListener l : mOnInitializeListeners) { + l.onInitialize(); + } + } + + /** * Adds a {@link OnConflictStateChangeListener}. */ public final void addOnConflictStateChangeListener(OnConflictStateChangeListener listener) { @@ -380,6 +464,47 @@ public class DvrScheduleManager { } /** + * Suggests the higher priority than the schedules which overlap with {@code schedule}. + */ + public long suggestHighestPriority(ScheduledRecording schedule) { + List<ScheduledRecording> schedules = mInputScheduleMap.get(schedule.getInputId()); + if (schedules == null) { + return DEFAULT_PRIORITY; + } + long highestPriority = Long.MIN_VALUE; + for (ScheduledRecording r : schedules) { + if (!r.equals(schedule) && r.isOverLapping(schedule) + && r.getPriority() > highestPriority) { + highestPriority = r.getPriority(); + } + } + if (highestPriority == Long.MIN_VALUE || highestPriority < schedule.getPriority()) { + return schedule.getPriority(); + } + return highestPriority + PRIORITY_OFFSET; + } + + /** + * Suggests the higher priority than the schedules which overlap with {@code schedule}. + */ + public long suggestHighestPriority(String inputId, Range<Long> peroid, long basePriority) { + List<ScheduledRecording> schedules = mInputScheduleMap.get(inputId); + if (schedules == null) { + return DEFAULT_PRIORITY; + } + long highestPriority = Long.MIN_VALUE; + for (ScheduledRecording r : schedules) { + if (r.isOverLapping(peroid) && r.getPriority() > highestPriority) { + highestPriority = r.getPriority(); + } + } + if (highestPriority == Long.MIN_VALUE || highestPriority < basePriority) { + return basePriority; + } + return highestPriority + PRIORITY_OFFSET; + } + + /** * Returns the priority for a series recording. * <p> * The recording will have the higher priority than the existing series. @@ -411,11 +536,12 @@ public class DvrScheduleManager { } /** - * Returns priority ordered list of all scheduled recordings that will not be recorded if - * this program is. + * Returns a sorted list of all scheduled recordings that will not be recorded if + * this program is going to be recorded, with their priorities in decending order. * <p> - * Any empty list means there is no conflicts. If there is conflict the program must be - * scheduled to record with a priority higher than the first recording in the list returned. + * An empty list means there is no conflicts. If there is conflict, a priority higher than + * the first recording in the returned list should be assigned to the new schedule of this + * program to guarantee the program would be completely recorded. */ public List<ScheduledRecording> getConflictingSchedules(Program program) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); @@ -439,11 +565,34 @@ public class DvrScheduleManager { } /** - * Returns priority ordered list of all scheduled recordings that will not be recorded if - * this channel is. + * Returns list of all conflicting scheduled recordings with schedules belonging to {@code + * seriesRecording} + * recording. + * <p> + * Any empty list means there is no conflicts. + */ + public List<ScheduledRecording> getConflictingSchedules(SeriesRecording seriesRecording) { + SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); + SoftPreconditions.checkState(seriesRecording != null, TAG, "series recording is null"); + if (!mInitialized || seriesRecording == null) { + return Collections.emptyList(); + } + TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, seriesRecording.getInputId()); + if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { + return Collections.emptyList(); + } + List<ScheduledRecording> schedulesForSeries = mDataManager.getScheduledRecordings( + seriesRecording.getId()); + return getConflictingSchedules(input, schedulesForSeries); + } + + /** + * Returns a sorted list of all scheduled recordings that will not be recorded if + * this channel is going to be recorded, with their priority in decending order. * <p> - * Any empty list means there is no conflicts. If there is conflict the channel must be - * scheduled to record with a priority higher than the first recording in the list returned. + * An empty list means there is no conflicts. If there is conflict, a priority higher than + * the first recording in the returned list should be assigned to the new schedule of this + * channel to guarantee the channel would be completely recorded in the designated time range. */ public List<ScheduledRecording> getConflictingSchedules(long channelId, long startTimeMs, long endTimeMs) { @@ -468,18 +617,18 @@ public class DvrScheduleManager { * the given input. */ @NonNull - private List<ScheduledRecording> getConflictingSchedules(String inputId) { + private Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(String inputId) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId); SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId); if (!mInitialized || input == null) { - return Collections.emptyList(); + return Collections.emptyMap(); } List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId()); if (schedules == null || schedules.isEmpty()) { - return Collections.emptyList(); + return Collections.emptyMap(); } - return getConflictingSchedules(schedules, input.getTunerCount()); + return getConflictingSchedulesInfo(schedules, input.getTunerCount()); } /** @@ -490,14 +639,33 @@ public class DvrScheduleManager { */ public boolean isConflicting(ScheduledRecording schedule) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId()); + TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); + SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : " + + schedule.getChannelId()); + if (!mInitialized || input == null) { + return false; + } + Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); + return conflicts != null && conflicts.containsKey(schedule); + } + + /** + * Checks if the schedule is partially conflicting, i.e., part of the scheduled program might be + * recorded even if the priority of the schedule is not raised. + * <p> + * If the given schedule is not conflicting or is totally conflicting, i.e., cannot be recorded + * at all, this method returns {@code false} in both cases. + */ + public boolean isPartiallyConflicting(@NonNull ScheduledRecording schedule) { + SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); + TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); SoftPreconditions.checkState(input != null, TAG, "Can't find input for channel ID : " + schedule.getChannelId()); if (!mInitialized || input == null) { return false; } - List<ScheduledRecording> conflicts = mInputConflictMap.get(input.getId()); - return conflicts != null && conflicts.contains(schedule); + Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); + return conflicts != null && conflicts.getOrDefault(schedule, false); } /** @@ -599,7 +767,7 @@ public class DvrScheduleManager { List<ScheduledRecording> result = new ArrayList<>(); result.addAll(getConflictingSchedules(schedulesSameChannel, 1)); result.addAll(getConflictingSchedules(schedulesToCheck, tunerCount)); - Collections.sort(result, ScheduledRecording.PRIORITY_COMPARATOR); + Collections.sort(result, RESULT_COMPARATOR); return result; } @@ -639,66 +807,161 @@ public class DvrScheduleManager { */ public static List<ScheduledRecording> getConflictingSchedules( List<ScheduledRecording> schedules, int tunerCount) { - return getConflictingSchedules(schedules, tunerCount, - Collections.singletonList(new Range<>(Long.MIN_VALUE, Long.MAX_VALUE))); + return getConflictingSchedules(schedules, tunerCount, null); } @VisibleForTesting - static List<ScheduledRecording> getConflictingSchedules(List<ScheduledRecording> schedules, - int tunerCount, List<Range<Long>> periods) { - List<ScheduledRecording> schedulesToCheck = new ArrayList<>(); - // Filter out non-overlapping or empty duration of schedules. - for (ScheduledRecording schedule : schedules) { - for (Range<Long> period : periods) { - if (schedule.isOverLapping(period) - && schedule.getStartTimeMs() < schedule.getEndTimeMs()) { - schedulesToCheck.add(schedule); - break; + static List<ScheduledRecording> getConflictingSchedules( + List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { + List<ScheduledRecording> result = new ArrayList<>( + getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet()); + Collections.sort(result, RESULT_COMPARATOR); + return result; + } + + @VisibleForTesting + static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( + List<ScheduledRecording> schedules, int tunerCount) { + return getConflictingSchedulesInfo(schedules, tunerCount, null); + } + + /** + * This is the core method to calculate all the conflicting schedules (in given periods). + * <p> + * Note that this method will ignore duplicated schedules with a same hash code. (Please refer + * to {@link ScheduledRecording#hashCode}.) + * + * @return A {@link HashMap} from {@link ScheduledRecording} to {@link Boolean}. The boolean + * value denotes if the scheduled recording is partially conflicting, i.e., is possible + * to be partially recorded under the given schedules and tuner count {@code true}, + * or not {@code false}. + */ + private static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( + List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { + List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules); + // Sort by the same order as that in InputTaskScheduler. + Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator()); + List<ScheduledRecording> recordings = new ArrayList<>(); + Map<ScheduledRecording, Boolean> conflicts = new HashMap<>(); + Map<ScheduledRecording, ScheduledRecording> modified2OriginalSchedules = new HashMap<>(); + // Simulate InputTaskScheduler. + while (!schedulesToCheck.isEmpty()) { + ScheduledRecording schedule = schedulesToCheck.remove(0); + removeFinishedRecordings(recordings, schedule.getStartTimeMs()); + if (recordings.size() < tunerCount) { + recordings.add(schedule); + if (modified2OriginalSchedules.containsKey(schedule)) { + // Schedule has been modified, which means it's already conflicted. + // Modify its state to partially conflicted. + conflicts.put(modified2OriginalSchedules.get(schedule), true); } - } - } - // Sort by the end time. - // If a.end <= b.end <= c.end and a overlaps with b and c, then b overlaps with c. - // Likewise, if a1.end <= a2.end <= ... , all the schedules which overlap with a1 overlap - // with each other. - Collections.sort(schedulesToCheck, ScheduledRecording.END_TIME_COMPARATOR); - Set<ScheduledRecording> conflicts = new ArraySet<>(); - List<ScheduledRecording> overlaps = new ArrayList<>(); - for (int i = 0; i < schedulesToCheck.size(); ++i) { - ScheduledRecording r1 = schedulesToCheck.get(i); - if (conflicts.contains(r1)) { - // No need to check r1 because it's a conflicting schedule already. - continue; - } - overlaps.clear(); - overlaps.add(r1); - // Find schedules which overlap with r1. - for (int j = i + 1; j < schedulesToCheck.size(); ++j) { - ScheduledRecording r2 = schedulesToCheck.get(j); - if (!conflicts.contains(r2) && r1.getEndTimeMs() > r2.getStartTimeMs()) { - overlaps.add(r2); + } else { + ScheduledRecording candidate = findReplaceableRecording(recordings, schedule); + if (candidate != null) { + if (!modified2OriginalSchedules.containsKey(candidate)) { + conflicts.put(candidate, true); + } + recordings.remove(candidate); + recordings.add(schedule); + if (modified2OriginalSchedules.containsKey(schedule)) { + // Schedule has been modified, which means it's already conflicted. + // Modify its state to partially conflicted. + conflicts.put(modified2OriginalSchedules.get(schedule), true); + } + } else { + if (!modified2OriginalSchedules.containsKey(schedule)) { + // if schedule has been modified, it's already conflicted. + // No need to add it again. + conflicts.put(schedule, false); + } + long earliestEndTime = getEarliestEndTime(recordings); + if (earliestEndTime < schedule.getEndTimeMs()) { + // The schedule can starts when other recording ends even though it's + // clipped. + ScheduledRecording modifiedSchedule = ScheduledRecording.buildFrom(schedule) + .setStartTimeMs(earliestEndTime).build(); + ScheduledRecording originalSchedule = + modified2OriginalSchedules.getOrDefault(schedule, schedule); + modified2OriginalSchedules.put(modifiedSchedule, originalSchedule); + int insertPosition = Collections.binarySearch(schedulesToCheck, + modifiedSchedule, + ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); + if (insertPosition >= 0) { + schedulesToCheck.add(insertPosition, modifiedSchedule); + } else { + schedulesToCheck.add(-insertPosition - 1, modifiedSchedule); + } + } } } - Collections.sort(overlaps, ScheduledRecording.PRIORITY_COMPARATOR); - // If there are more than one overlapping schedules for the same channel, only one - // schedule will be recorded. - HashSet<Long> channelIds = new HashSet<>(); - for (Iterator<ScheduledRecording> iter = overlaps.iterator(); iter.hasNext(); ) { + } + // Returns only the schedules with the given range. + if (periods != null && !periods.isEmpty()) { + for (Iterator<ScheduledRecording> iter = conflicts.keySet().iterator(); + iter.hasNext(); ) { + boolean overlapping = false; ScheduledRecording schedule = iter.next(); - if (channelIds.contains(schedule.getChannelId())) { - conflicts.add(schedule); + for (Range<Long> period : periods) { + if (schedule.isOverLapping(period)) { + overlapping = true; + break; + } + } + if (!overlapping) { iter.remove(); - } else { - channelIds.add(schedule.getChannelId()); } } - if (overlaps.size() > tunerCount) { - conflicts.addAll(overlaps.subList(tunerCount, overlaps.size())); + } + return conflicts; + } + + private static void removeFinishedRecordings(List<ScheduledRecording> recordings, + long currentTimeMs) { + for (Iterator<ScheduledRecording> iter = recordings.iterator(); iter.hasNext(); ) { + if (iter.next().getEndTimeMs() <= currentTimeMs) { + iter.remove(); } } - List<ScheduledRecording> result = new ArrayList<>(conflicts); - Collections.sort(result, ScheduledRecording.PRIORITY_COMPARATOR); - return result; + } + + /** + * @see InputTaskScheduler#getReplacableTask + */ + private static ScheduledRecording findReplaceableRecording(List<ScheduledRecording> recordings, + ScheduledRecording schedule) { + // Returns the recording with the following priority. + // 1. The recording with the lowest priority is returned. + // 2. If the priorities are the same, the recording which finishes early is returned. + // 3. If 1) and 2) are the same, the early created schedule is returned. + ScheduledRecording candidate = null; + for (ScheduledRecording recording : recordings) { + if (schedule.getPriority() > recording.getPriority()) { + if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, recording) > 0) { + candidate = recording; + } + } + } + return candidate; + } + + private static long getEarliestEndTime(List<ScheduledRecording> recordings) { + long earliest = Long.MAX_VALUE; + for (ScheduledRecording recording : recordings) { + if (earliest > recording.getEndTimeMs()) { + earliest = recording.getEndTimeMs(); + } + } + return earliest; + } + + /** + * A listener which is notified the initialization of schedule manager. + */ + public interface OnInitializeListener { + /** + * Called when the schedule manager has been initialized. + */ + void onInitialize(); } /** @@ -714,4 +977,4 @@ public class DvrScheduleManager { */ void onConflictStateChange(boolean conflict, ScheduledRecording... schedules); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java new file mode 100644 index 00000000..a653b5f4 --- /dev/null +++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr; + +import android.content.BroadcastReceiver; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Environment; +import android.os.Looper; +import android.os.RemoteException; +import android.os.StatFs; +import android.support.annotation.AnyThread; +import android.support.annotation.IntDef; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.util.Utils; + +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Signals DVR storage status change such as plugging/unplugging. + */ +public class DvrStorageStatusManager { + private static final String TAG = "DvrStorageStatusManager"; + private static final boolean DEBUG = false; + + /** + * Minimum storage size to support DVR + */ + public static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; // 50GB + private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES + = 10 * 1024 * 1024 * 1024L; // 10GB + private static final String RECORDING_DATA_SUB_PATH = "/recording"; + + private static final String[] PROJECTION = { + TvContract.RecordedPrograms._ID, + TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME, + TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI + }; + private final static int BATCH_OPERATION_COUNT = 100; + + @IntDef({STORAGE_STATUS_OK, STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL, + STORAGE_STATUS_FREE_SPACE_INSUFFICIENT, STORAGE_STATUS_MISSING}) + @Retention(RetentionPolicy.SOURCE) + public @interface StorageStatus { + } + + /** + * Current storage is OK to record a program. + */ + public static final int STORAGE_STATUS_OK = 0; + + /** + * Current storage's total capacity is smaller than DVR requirement. + */ + public static final int STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL = 1; + + /** + * Current storage's free space is insufficient to record programs. + */ + public static final int STORAGE_STATUS_FREE_SPACE_INSUFFICIENT = 2; + + /** + * Current storage is missing. + */ + public static final int STORAGE_STATUS_MISSING = 3; + + private final Context mContext; + private final Set<OnStorageMountChangedListener> mOnStorageMountChangedListeners = + new CopyOnWriteArraySet<>(); + private final boolean mRunningInMainProcess; + private MountedStorageStatus mMountedStorageStatus; + private boolean mStorageValid; + private CleanUpDbTask mCleanUpDbTask; + + private class MountedStorageStatus { + private final boolean mStorageMounted; + private final File mStorageMountedDir; + private final long mStorageMountedCapacity; + + private MountedStorageStatus(boolean mounted, File mountedDir, long capacity) { + mStorageMounted = mounted; + mStorageMountedDir = mountedDir; + mStorageMountedCapacity = capacity; + } + + private boolean isValidForDvr() { + return mStorageMounted && mStorageMountedCapacity >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof MountedStorageStatus)) { + return false; + } + MountedStorageStatus status = (MountedStorageStatus) other; + return mStorageMounted == status.mStorageMounted + && Objects.equals(mStorageMountedDir, status.mStorageMountedDir) + && mStorageMountedCapacity == status.mStorageMountedCapacity; + } + } + + public interface OnStorageMountChangedListener { + + /** + * Listener for DVR storage status change. + * + * @param storageMounted {@code true} when DVR possible storage is mounted, + * {@code false} otherwise. + */ + void onStorageMountChanged(boolean storageMounted); + } + + private final class StorageStatusBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + MountedStorageStatus result = getStorageStatusInternal(); + if (mMountedStorageStatus.equals(result)) { + return; + } + mMountedStorageStatus = result; + if (result.mStorageMounted && mRunningInMainProcess) { + // Cleans up DB in LC process. + // Tuner process is not always on. + if (mCleanUpDbTask != null) { + mCleanUpDbTask.cancel(true); + } + mCleanUpDbTask = new CleanUpDbTask(); + mCleanUpDbTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + boolean valid = result.isValidForDvr(); + if (valid == mStorageValid) { + return; + } + mStorageValid = valid; + for (OnStorageMountChangedListener l : mOnStorageMountChangedListeners) { + l.onStorageMountChanged(valid); + } + } + } + + /** + * Creates DvrStorageStatusManager. + * + * @param context {@link Context} + */ + public DvrStorageStatusManager(final Context context, boolean runningInMainProcess) { + mContext = context; + mRunningInMainProcess = runningInMainProcess; + mMountedStorageStatus = getStorageStatusInternal(); + mStorageValid = mMountedStorageStatus.isValidForDvr(); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_MEDIA_MOUNTED); + filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); + filter.addAction(Intent.ACTION_MEDIA_EJECT); + filter.addAction(Intent.ACTION_MEDIA_REMOVED); + filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); + filter.addDataScheme(ContentResolver.SCHEME_FILE); + mContext.registerReceiver(new StorageStatusBroadcastReceiver(), filter); + } + + /** + * Adds the listener for receiving storage status change. + * + * @param listener + */ + public void addListener(OnStorageMountChangedListener listener) { + mOnStorageMountChangedListeners.add(listener); + } + + /** + * Removes the current listener. + */ + public void removeListener(OnStorageMountChangedListener listener) { + mOnStorageMountChangedListeners.remove(listener); + } + + /** + * Returns true if a storage is mounted. + */ + public boolean isStorageMounted() { + return mMountedStorageStatus.mStorageMounted; + } + + /** + * Returns the path to DVR recording data directory. + * This can take for a while sometimes. + */ + @WorkerThread + public File getRecordingRootDataDirectory() { + SoftPreconditions.checkState(Looper.myLooper() != Looper.getMainLooper()); + if (mMountedStorageStatus.mStorageMountedDir == null) { + return null; + } + File root = mContext.getExternalFilesDir(null); + String rootPath; + try { + rootPath = root != null ? root.getCanonicalPath() : null; + } catch (IOException | SecurityException e) { + return null; + } + return rootPath == null ? null : new File(rootPath + RECORDING_DATA_SUB_PATH); + } + + /** + * Returns the current storage status for DVR recordings. + * + * @return {@link StorageStatus} + */ + @AnyThread + public @StorageStatus int getDvrStorageStatus() { + MountedStorageStatus status = mMountedStorageStatus; + if (status.mStorageMountedDir == null) { + return STORAGE_STATUS_MISSING; + } + if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(mContext)) { + return STORAGE_STATUS_OK; + } + if (status.mStorageMountedCapacity < MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES) { + return STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL; + } + try { + StatFs statFs = new StatFs(status.mStorageMountedDir.toString()); + if (statFs.getAvailableBytes() < MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) { + return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; + } + } catch (IllegalArgumentException e) { + // In rare cases, storage status change was not notified yet. + SoftPreconditions.checkState(false); + return STORAGE_STATUS_FREE_SPACE_INSUFFICIENT; + } + return STORAGE_STATUS_OK; + } + + /** + * Returns whether the storage has sufficient storage. + * + * @return {@code true} when there is sufficient storage, {@code false} otherwise + */ + public boolean isStorageSufficient() { + return getDvrStorageStatus() == STORAGE_STATUS_OK; + } + + private MountedStorageStatus getStorageStatusInternal() { + boolean storageMounted = + Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); + File storageMountedDir = storageMounted ? Environment.getExternalStorageDirectory() : null; + storageMounted = storageMounted && storageMountedDir != null; + long storageMountedCapacity = 0L; + if (storageMounted) { + try { + StatFs statFs = new StatFs(storageMountedDir.toString()); + storageMountedCapacity = statFs.getTotalBytes(); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Storage mount status was changed."); + storageMounted = false; + storageMountedDir = null; + } + } + return new MountedStorageStatus( + storageMounted, storageMountedDir, storageMountedCapacity); + } + + private class CleanUpDbTask extends AsyncTask<Void, Void, Void> { + private final ContentResolver mContentResolver; + + private CleanUpDbTask() { + mContentResolver = mContext.getContentResolver(); + } + + @Override + protected Void doInBackground(Void... params) { + @DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus(); + if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { + return null; + } + List<ContentProviderOperation> ops = getDeleteOps(storageStatus + == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL); + if (ops == null || ops.isEmpty()) { + return null; + } + Log.i(TAG, "New device storage mounted. # of recordings to be forgotten : " + + ops.size()); + for (int i = 0 ; i < ops.size() && !isCancelled() ; i += BATCH_OPERATION_COUNT) { + int toIndex = (i + BATCH_OPERATION_COUNT) > ops.size() + ? ops.size() : (i + BATCH_OPERATION_COUNT); + ArrayList<ContentProviderOperation> batchOps = + new ArrayList<>(ops.subList(i, toIndex)); + try { + mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, batchOps); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Failed to clean up RecordedPrograms.", e); + } + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + if (mCleanUpDbTask == this) { + mCleanUpDbTask = null; + } + } + + private List<ContentProviderOperation> getDeleteOps(boolean deleteAll) { + List<ContentProviderOperation> ops = new ArrayList<>(); + + try (Cursor c = mContentResolver.query( + TvContract.RecordedPrograms.CONTENT_URI, PROJECTION, null, null, null)) { + if (c == null) { + return null; + } + while (c.moveToNext()) { + @DvrStorageStatusManager.StorageStatus int storageStatus = + getDvrStorageStatus(); + if (isCancelled() + || storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { + ops.clear(); + break; + } + String id = c.getString(0); + String packageName = c.getString(1); + String dataUriString = c.getString(2); + if (dataUriString == null) { + continue; + } + Uri dataUri = Uri.parse(dataUriString); + if (!Utils.isInBundledPackageSet(packageName) + || dataUri == null || dataUri.getPath() == null + || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) { + continue; + } + File recordedProgramDir = new File(dataUri.getPath()); + if (deleteAll || !recordedProgramDir.exists()) { + ops.add(ContentProviderOperation.newDelete( + TvContract.buildRecordedProgramUri(Long.parseLong(id))).build()); + } + } + return ops; + } + } + } +} diff --git a/src/com/android/tv/dvr/DvrUiHelper.java b/src/com/android/tv/dvr/DvrUiHelper.java index be934fd4..c0d3b0c5 100644 --- a/src/com/android/tv/dvr/DvrUiHelper.java +++ b/src/com/android/tv/dvr/DvrUiHelper.java @@ -36,7 +36,6 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.ui.DvrCancelAllSeriesRecordingDialogFragment; import com.android.tv.dvr.ui.DvrDetailsActivity; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment; @@ -44,13 +43,19 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialo import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrSchedulesActivity; import com.android.tv.dvr.ui.DvrSeriesDeletionActivity; +import com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity; import com.android.tv.dvr.ui.DvrSeriesSettingsActivity; +import com.android.tv.dvr.ui.DvrStopRecordingFragment; +import com.android.tv.dvr.ui.DvrStopSeriesRecordingDialogFragment; +import com.android.tv.dvr.ui.DvrStopSeriesRecordingFragment; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.dvr.ui.list.DvrSchedulesFragment; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import com.android.tv.util.Utils; @@ -82,7 +87,7 @@ public class DvrUiHelper { } } else { SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); - if (seriesRecording == null) { + if (seriesRecording == null || seriesRecording.isStopped()) { DvrUiHelper.showScheduleDialog(activity, program); return false; } else { @@ -111,6 +116,32 @@ public class DvrUiHelper { } /** + * Checks if the storage status is good for recording and shows error messages if needed. + * + * @return true if the storage status is fine to be recorded for {@code inputId}. + */ + public static boolean checkStorageStatusAndShowErrorMessage(Activity activity, String inputId) { + if (!Utils.isBundledInput(inputId)) { + return true; + } + DvrStorageStatusManager dvrStorageStatusManager = + TvApplication.getSingletons(activity).getDvrStorageStatusManager(); + int status = dvrStorageStatusManager.getDvrStorageStatus(); + if (status == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { + showDvrSmallSizedStorageErrorDialog(activity); + return false; + } else if (status == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { + showDvrMissingStorageErrorDialog(activity, inputId); + return false; + } else if (status == DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT) { + // TODO: handle insufficient storage case. + return true; + } else { + return true; + } + } + + /** * Shows the schedule dialog. */ public static void showScheduleDialog(MainActivity activity, Program program) { @@ -170,7 +201,7 @@ public class DvrUiHelper { /** * Shows DVR missing storage error dialog. */ - public static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) { + private static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) { SoftPreconditions.checkArgument(!TextUtils.isEmpty(inputId)); Bundle args = new Bundle(); args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, inputId); @@ -178,15 +209,23 @@ public class DvrUiHelper { } /** + * Shows DVR small sized storage error dialog. + */ + public static void showDvrSmallSizedStorageErrorDialog(Activity activity) { + showDialogFragment(activity, new DvrSmallSizedStorageErrorDialogFragment(), null); + } + + /** * Shows stop recording dialog. */ - public static void showStopRecordingDialog(MainActivity activity, Channel channel) { - if (channel == null) { - return; - } + public static void showStopRecordingDialog(Activity activity, long channelId, int reason, + HalfSizedDialogFragment.OnActionClickListener listener) { Bundle args = new Bundle(); - args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId()); - showDialogFragment(activity, new DvrStopRecordingDialogFragment(), args); + args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channelId); + args.putInt(DvrStopRecordingFragment.KEY_REASON, reason); + DvrHalfSizedDialogFragment fragment = new DvrStopRecordingDialogFragment(); + fragment.setOnActionClickListener(listener); + showDialogFragment(activity, fragment, args); } /** @@ -244,7 +283,8 @@ public class DvrUiHelper { recordings) { ScheduledRecording earlistScheduledRecording = null; if (!recordings.isEmpty()) { - Collections.sort(recordings, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR); + Collections.sort(recordings, + ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); earlistScheduledRecording = recordings.get(0); } return earlistScheduledRecording; @@ -300,32 +340,72 @@ public class DvrUiHelper { /** * Shows the series settings activity. + * + * @param channelIds Channel ID list which has programs belonging to the series. */ - public static void startSeriesSettingsActivity(Context context, long seriesRecordingId) { + public static void startSeriesSettingsActivity(Context context, long seriesRecordingId, + @Nullable long[] channelIds, boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog) { Intent intent = new Intent(context, DvrSeriesSettingsActivity.class); intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId); + intent.putExtra(DvrSeriesSettingsActivity.CHANNEL_ID_LIST, channelIds); + intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING, + removeEmptySeriesSchedule); + intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent); + intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG, + showViewScheduleOptionInDialog); context.startActivity(intent); } /** - * Shows the details activity for the schedule. + * Shows "series recording scheduled" dialog activity. */ - public static void startDetailsActivity(Activity activity, ScheduledRecording schedule, + public static void StartSeriesScheduledDialogActivity(Context context, + SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog) { + if (seriesRecording == null) { + return; + } + Intent intent = new Intent(context, DvrSeriesScheduledDialogActivity.class); + intent.putExtra(DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, + seriesRecording.getId()); + intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, + showViewScheduleOptionInDialog); + context.startActivity(intent); + } + + /** + * Shows the details activity for the DVR items. The type of DVR items may be + * {@link ScheduledRecording}, {@link RecordedProgram}, or {@link SeriesRecording}. + */ + public static void startDetailsActivity(Activity activity, Object dvrItem, @Nullable ImageView imageView, boolean hideViewSchedule) { - if (schedule == null) { + if (dvrItem == null) { return; } + Intent intent = new Intent(activity, DvrDetailsActivity.class); + long recordingId; int viewType; - if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { - viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; - } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; + if (dvrItem instanceof ScheduledRecording) { + ScheduledRecording schedule = (ScheduledRecording) dvrItem; + recordingId = schedule.getId(); + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; + } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; + } else { + return; + } + } else if (dvrItem instanceof RecordedProgram) { + recordingId = ((RecordedProgram) dvrItem).getId(); + viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; + } else if (dvrItem instanceof SeriesRecording) { + recordingId = ((SeriesRecording) dvrItem).getId(); + viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW; } else { return; } - Intent intent = new Intent(activity, DvrDetailsActivity.class); + intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId); intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType); - intent.putExtra(DvrDetailsActivity.RECORDING_ID, schedule.getId()); intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); Bundle bundle = null; if (imageView != null) { @@ -336,30 +416,18 @@ public class DvrUiHelper { } /** - * Shows the details activity for the recorded program. - */ - public static void startDetailsActivity(Activity activity, RecordedProgram recordedProgram, - @Nullable ImageView imageView) { - Intent intent = new Intent(activity, DvrDetailsActivity.class); - intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordedProgram.getId()); - intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, - DvrDetailsActivity.RECORDED_PROGRAM_VIEW); - Bundle bundle = null; - if (imageView != null) { - bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, imageView, - DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); - } - activity.startActivity(intent, bundle); - } - - /** * Shows the cancel all dialog for series schedules list. */ - public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity) { - DvrCancelAllSeriesRecordingDialogFragment dvrCancelAllSeriesRecordingDialogFragment = - new DvrCancelAllSeriesRecordingDialogFragment(); - dvrCancelAllSeriesRecordingDialogFragment.show(activity.getFragmentManager(), - DvrCancelAllSeriesRecordingDialogFragment.DIALOG_TAG); + public static void showCancelAllSeriesRecordingDialog(DvrSchedulesActivity activity, + SeriesRecording seriesRecording) { + DvrStopSeriesRecordingDialogFragment dvrStopSeriesRecordingDialogFragment = + new DvrStopSeriesRecordingDialogFragment(); + Bundle arguments = new Bundle(); + arguments.putParcelable(DvrStopSeriesRecordingFragment.KEY_SERIES_RECORDING, + seriesRecording); + dvrStopSeriesRecordingDialogFragment.setArguments(arguments); + dvrStopSeriesRecordingDialogFragment.show(activity.getFragmentManager(), + DvrStopSeriesRecordingDialogFragment.DIALOG_TAG); } /** diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java index cb723f83..4eada742 100644 --- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java +++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java @@ -19,9 +19,12 @@ package com.android.tv.dvr; import android.content.Context; import android.content.SharedPreferences; import android.media.tv.TvInputManager; +import android.support.annotation.IntDef; import com.android.tv.common.SharedPreferencesUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; @@ -37,13 +40,33 @@ public class DvrWatchedPositionManager { private final boolean DEBUG = false; private SharedPreferences mWatchedPositions; - private final Context mContext; private final Map<Long, Set> mListeners = new HashMap<>(); + /** + * The minimum percentage of recorded program being watched that will be considered as being + * completely watched. + */ + public static final float DVR_WATCHED_THRESHOLD_RATE = 0.98f; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({DVR_WATCHED_STATUS_NEW, DVR_WATCHED_STATUS_WATCHING, DVR_WATCHED_STATUS_WATCHED}) + public @interface DvrWatchedStatus {} + /** + * The status indicates the recorded program has not been watched at all. + */ + public static final int DVR_WATCHED_STATUS_NEW = 0; + /** + * The status indicates the recorded program is being watched. + */ + public static final int DVR_WATCHED_STATUS_WATCHING = 1; + /** + * The status indicates the recorded program was completely watched. + */ + public static final int DVR_WATCHED_STATUS_WATCHED = 2; + public DvrWatchedPositionManager(Context context) { - mContext = context.getApplicationContext(); - mWatchedPositions = mContext.getSharedPreferences(SharedPreferencesUtils - .SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE); + mWatchedPositions = context.getSharedPreferences( + SharedPreferencesUtils.SHARED_PREF_DVR_WATCHED_POSITION, Context.MODE_PRIVATE); } /** @@ -62,6 +85,18 @@ public class DvrWatchedPositionManager { TvInputManager.TIME_SHIFT_INVALID_TIME); } + @DvrWatchedStatus public int getWatchedStatus(RecordedProgram recordedProgram) { + long watchedPosition = getWatchedPosition(recordedProgram.getId()); + if (watchedPosition == TvInputManager.TIME_SHIFT_INVALID_TIME) { + return DVR_WATCHED_STATUS_NEW; + } else if (watchedPosition > recordedProgram + .getDurationMillis() * DVR_WATCHED_THRESHOLD_RATE) { + return DVR_WATCHED_STATUS_WATCHED; + } else { + return DVR_WATCHED_STATUS_WATCHING; + } + } + /** * Adds {@link WatchedPositionChangedListener}. */ diff --git a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/EpisodicProgramLoadTask.java new file mode 100644 index 00000000..15ca2700 --- /dev/null +++ b/src/com/android/tv/dvr/EpisodicProgramLoadTask.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr; + +import android.annotation.TargetApi; +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.media.tv.TvContract.Programs; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.text.TextUtils; + +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Program; +import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask; +import com.android.tv.util.AsyncDbTask.CursorFilter; +import com.android.tv.util.PermissionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * A wrapper of AsyncProgramQueryTask to load the episodic programs for the series recordings. + */ +@TargetApi(Build.VERSION_CODES.N) +abstract public class EpisodicProgramLoadTask { + private static final String TAG = "EpisodicProgramLoadTask"; + + private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID); + private static final int START_TIME_INDEX = + Program.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS); + private static final int RECORDING_PROHIBITED_INDEX = + Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED); + + private static final String PARAM_START_TIME = "start_time"; + private static final String PARAM_END_TIME = "end_time"; + + private static final String PROGRAM_PREDICATE = + Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND " + + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; + private static final String PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM = + Programs.COLUMN_END_TIME_UTC_MILLIS + ">? AND " + + Programs.COLUMN_RECORDING_PROHIBITED + "=0"; + private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?"; + private static final String PROGRAM_TITLE_PREDICATE = Programs.COLUMN_TITLE + "=?"; + + private final Context mContext; + private final DvrDataManager mDataManager; + private boolean mQueryAllChannels; + private boolean mLoadCurrentProgram; + private boolean mLoadScheduledEpisode; + private boolean mLoadDisallowedProgram; + // If true, match programs with OPTION_CHANNEL_ALL. + private boolean mIgnoreChannelOption; + private final ArrayList<SeriesRecording> mSeriesRecordings = new ArrayList<>(); + private AsyncProgramQueryTask mProgramQueryTask; + + /** + * + * Constructor used to load programs for one series recording with the given channel option. + */ + public EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording) { + this(context, Collections.singletonList(seriesRecording)); + } + + /** + * Constructor used to load programs for multiple series recordings. The channel option is + * {@link SeriesRecording#OPTION_CHANNEL_ALL}. + */ + public EpisodicProgramLoadTask(Context context, Collection<SeriesRecording> seriesRecordings) { + mContext = context.getApplicationContext(); + mDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mSeriesRecordings.addAll(seriesRecordings); + } + + /** + * Returns the series recordings. + */ + public List<SeriesRecording> getSeriesRecordings() { + return mSeriesRecordings; + } + + /** + * Returns the program query task. It is {@code null} until it is executed. + */ + @Nullable + public AsyncProgramQueryTask getTask() { + return mProgramQueryTask; + } + + /** + * Enables loading current programs. The default value is {@code false}. + */ + public EpisodicProgramLoadTask setLoadCurrentProgram(boolean loadCurrentProgram) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mLoadCurrentProgram = loadCurrentProgram; + return this; + } + + /** + * Enables already schedules episodes. The default value is {@code false}. + */ + public EpisodicProgramLoadTask setLoadScheduledEpisode(boolean loadScheduledEpisode) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mLoadScheduledEpisode = loadScheduledEpisode; + return this; + } + + /** + * Enables loading disallowed programs whose schedules were removed manually by the user. + * The default value is {@code false}. + */ + public EpisodicProgramLoadTask setLoadDisallowedProgram(boolean loadDisallowedProgram) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mLoadDisallowedProgram = loadDisallowedProgram; + return this; + } + + /** + * Gives the option whether to ignore the channel option when matching programs. + * If {@code ignoreChannelOption} is {@code true}, the program will be matched with + * {@link SeriesRecording#OPTION_CHANNEL_ALL} option. + */ + public EpisodicProgramLoadTask setIgnoreChannelOption(boolean ignoreChannelOption) { + SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't change setting after execution."); + mIgnoreChannelOption = ignoreChannelOption; + return this; + } + + /** + * Executes the task. + * + * @see com.android.tv.util.AsyncDbTask#executeOnDbThread + */ + public void execute() { + if (SoftPreconditions.checkState(mProgramQueryTask == null, TAG, + "Can't execute task: the task is already running.")) { + mQueryAllChannels = mSeriesRecordings.size() > 1 + || mSeriesRecordings.get(0).getChannelOption() + == SeriesRecording.OPTION_CHANNEL_ALL + || mIgnoreChannelOption; + mProgramQueryTask = createTask(); + mProgramQueryTask.executeOnDbThread(); + } + } + + /** + * Cancels the task. + * + * @see android.os.AsyncTask#cancel + */ + public void cancel(boolean mayInterruptIfRunning) { + if (mProgramQueryTask != null) { + mProgramQueryTask.cancel(mayInterruptIfRunning); + } + } + + /** + * Runs on the UI thread after the program loading finishes successfully. + */ + protected void onPostExecute(List<Program> programs) { + } + + /** + * Runs on the UI thread after the program loading was canceled. + */ + protected void onCancelled(List<Program> programs) { + } + + private AsyncProgramQueryTask createTask() { + SqlParams sqlParams = createSqlParams(); + return new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri, + sqlParams.selection, sqlParams.selectionArgs, null, sqlParams.filter) { + @Override + protected void onPostExecute(List<Program> programs) { + EpisodicProgramLoadTask.this.onPostExecute(programs); + } + + @Override + protected void onCancelled(List<Program> programs) { + EpisodicProgramLoadTask.this.onCancelled(programs); + } + }; + } + + private SqlParams createSqlParams() { + SqlParams sqlParams = new SqlParams(); + if (PermissionUtils.hasAccessAllEpg(mContext)) { + sqlParams.uri = Programs.CONTENT_URI; + // Base + StringBuilder selection = new StringBuilder(mLoadCurrentProgram + ? PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM : PROGRAM_PREDICATE); + List<String> args = new ArrayList<>(); + args.add(Long.toString(System.currentTimeMillis())); + // Channel option + if (!mQueryAllChannels) { + selection.append(" AND ").append(CHANNEL_ID_PREDICATE); + args.add(Long.toString(mSeriesRecordings.get(0).getChannelId())); + } + // Title + if (mSeriesRecordings.size() == 1) { + selection.append(" AND ").append(PROGRAM_TITLE_PREDICATE); + args.add(mSeriesRecordings.get(0).getTitle()); + } + sqlParams.selection = selection.toString(); + sqlParams.selectionArgs = args.toArray(new String[args.size()]); + sqlParams.filter = new SeriesRecordingCursorFilter(mSeriesRecordings); + } else { + // The query includes the current program. Will be filtered if needed. + if (mQueryAllChannels) { + sqlParams.uri = Programs.CONTENT_URI.buildUpon() + .appendQueryParameter(PARAM_START_TIME, + String.valueOf(System.currentTimeMillis())) + .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE)) + .build(); + } else { + sqlParams.uri = TvContract.buildProgramsUriForChannel( + mSeriesRecordings.get(0).getChannelId(), + System.currentTimeMillis(), Long.MAX_VALUE); + } + sqlParams.selection = null; + sqlParams.selectionArgs = null; + sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(mSeriesRecordings); + } + return sqlParams; + } + + @VisibleForTesting + static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes, + ScheduledEpisode episode) { + // The episode whose season number or episode number is null will always be scheduled. + return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber) + && !TextUtils.isEmpty(episode.episodeNumber); + } + + /** + * Filter the programs which match the series recording. The episodes which the schedules are + * already created for are filtered out too. + */ + private class SeriesRecordingCursorFilter implements CursorFilter { + private final Set<Long> mDisallowedProgramIds = new HashSet<>(); + private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>(); + + SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) { + if (!mLoadDisallowedProgram) { + mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds()); + } + if (!mLoadScheduledEpisode) { + Set<Long> seriesRecordingIds = new HashSet<>(); + for (SeriesRecording r : seriesRecordings) { + seriesRecordingIds.add(r.getId()); + } + for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) { + if (seriesRecordingIds.contains(r.getSeriesRecordingId()) + && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED + && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { + mScheduledEpisodes.add(new ScheduledEpisode(r)); + } + } + } + } + + @Override + @WorkerThread + public boolean filter(Cursor c) { + if (!mLoadDisallowedProgram + && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) { + return false; + } + Program program = Program.fromCursor(c); + for (SeriesRecording seriesRecording : mSeriesRecordings) { + boolean programMatches; + if (mIgnoreChannelOption) { + programMatches = seriesRecording.matchProgram(program, + SeriesRecording.OPTION_CHANNEL_ALL); + } else { + programMatches = seriesRecording.matchProgram(program); + } + if (programMatches) { + return mLoadScheduledEpisode + || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode( + seriesRecording.getId(), program.getSeasonNumber(), + program.getEpisodeNumber())); + } + } + return false; + } + } + + private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter { + SeriesRecordingCursorFilterForNonSystem(List<SeriesRecording> seriesRecordings) { + super(seriesRecordings); + } + + @Override + public boolean filter(Cursor c) { + return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis()) + && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c); + } + } + + private static class SqlParams { + public Uri uri; + public String selection; + public String[] selectionArgs; + public CursorFilter filter; + } + + /** + * A plain java object which includes the season/episode number for the series recording. + */ + public static class ScheduledEpisode { + public final long seriesRecordingId; + public final String seasonNumber; + public final String episodeNumber; + + /** + * Create a new Builder with the values set from an existing {@link ScheduledRecording}. + */ + ScheduledEpisode(ScheduledRecording r) { + this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); + } + + public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) { + this.seriesRecordingId = seriesRecordingId; + this.seasonNumber = seasonNumber; + this.episodeNumber = episodeNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ScheduledEpisode)) return false; + ScheduledEpisode that = (ScheduledEpisode) o; + return seriesRecordingId == that.seriesRecordingId + && Objects.equals(seasonNumber, that.seasonNumber) + && Objects.equals(episodeNumber, that.episodeNumber); + } + + @Override + public int hashCode() { + return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber); + } + + @Override + public String toString() { + return "ScheduledEpisode{" + + "seriesRecordingId=" + seriesRecordingId + + ", seasonNumber='" + seasonNumber + + ", episodeNumber=" + episodeNumber + + '}'; + } + } +} diff --git a/src/com/android/tv/dvr/InputTaskScheduler.java b/src/com/android/tv/dvr/InputTaskScheduler.java index 23eacb73..53c89ebc 100644 --- a/src/com/android/tv/dvr/InputTaskScheduler.java +++ b/src/com/android/tv/dvr/InputTaskScheduler.java @@ -21,7 +21,6 @@ import android.media.tv.TvInputInfo; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.ArrayMap; @@ -32,9 +31,11 @@ import com.android.tv.InputSessionManager; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.util.Clock; +import com.android.tv.util.CompositeComparator; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -42,7 +43,6 @@ import java.util.Map; /** * The scheduler for a TV input. */ -@MainThread public class InputTaskScheduler { private static final String TAG = "InputTaskScheduler"; private static final boolean DEBUG = false; @@ -51,6 +51,24 @@ public class InputTaskScheduler { private static final int MSG_REMOVE_SCHEDULED_RECORDING = 2; private static final int MSG_UPDATE_SCHEDULED_RECORDING = 3; private static final int MSG_BUILD_SCHEDULE = 4; + private static final int MSG_STOP_SCHEDULE = 5; + + private static final float MIN_REMAIN_DURATION_PERCENT = 0.05f; + + // The candidate comparator should be the consistent with + // DvrScheduleManager#CANDIDATE_COMPARATOR. + private static final Comparator<RecordingTask> CANDIDATE_COMPARATOR = + new CompositeComparator<>( + RecordingTask.PRIORITY_COMPARATOR, + RecordingTask.END_TIME_COMPARATOR, + RecordingTask.ID_COMPARATOR); + + /** + * Returns the comparator which the schedules are sorted with when executed. + */ + public static Comparator<ScheduledRecording> getRecordingOrderComparator() { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR; + } /** * Wraps a {@link RecordingTask} removing it from {@link #mPendingRecordings} when it is done. @@ -217,6 +235,23 @@ public class InputTaskScheduler { } } + /** + * Stops the input task scheduler. + */ + public void stop() { + mHandler.removeCallbacksAndMessages(null); + mHandler.sendEmptyMessage(MSG_STOP_SCHEDULE); + } + + private void handleStopSchedule() { + mWaitingSchedules.clear(); + int size = mPendingRecordings.size(); + for (int i = 0; i < size; ++i) { + RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; + task.cleanUp(); + } + } + @VisibleForTesting void handleBuildSchedule() { if (mWaitingSchedules.isEmpty()) { @@ -227,7 +262,8 @@ public class InputTaskScheduler { for (Iterator<ScheduledRecording> iter = mWaitingSchedules.values().iterator(); iter.hasNext(); ) { ScheduledRecording schedule = iter.next(); - if (schedule.getEndTimeMs() <= currentTimeMs) { + if (schedule.getEndTimeMs() - currentTimeMs + <= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) { fail(schedule); iter.remove(); } @@ -244,7 +280,13 @@ public class InputTaskScheduler { schedulesToStart.add(schedule); } } - Collections.sort(schedulesToStart, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR); + // The schedules will be executed with the following order. + // 1. The schedule which starts early. It can be replaced later when the schedule with the + // higher priority needs to start. + // 2. The schedule with the higher priority. It can be replaced later when the schedule with + // the higher priority needs to start. + // 3. The schedule which was created recently. + Collections.sort(schedulesToStart, getRecordingOrderComparator()); int tunerCount; synchronized (mInputLock) { tunerCount = mInput.canRecord() ? mInput.getTunerCount() : 0; @@ -266,11 +308,6 @@ public class InputTaskScheduler { task.stop(); // Just return. The schedules will be rebuilt after the task is stopped. return; - } else { - // TODO: Do not fail immediately. Start the recording later when available. - // There are no replaceable task. Remove it. - fail(schedule); - mWaitingSchedules.remove(schedule.getId()); } } } @@ -280,12 +317,20 @@ public class InputTaskScheduler { // Set next scheduling. long earliest = Long.MAX_VALUE; for (ScheduledRecording schedule : mWaitingSchedules.values()) { - if (earliest > schedule.getStartTimeMs()) { - earliest = schedule.getStartTimeMs(); + // The conflicting schedules will be removed if they end before conflicting resolved. + if (schedulesToStart.contains(schedule)) { + if (earliest > schedule.getEndTimeMs()) { + earliest = schedule.getEndTimeMs(); + } + } else { + if (earliest > schedule.getStartTimeMs() + - RecordingTask.RECORDING_EARLY_START_OFFSET_MS) { + earliest = schedule.getStartTimeMs() + - RecordingTask.RECORDING_EARLY_START_OFFSET_MS; + } } } - mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - - RecordingTask.RECORDING_EARLY_START_OFFSET_MS - currentTimeMs); + mHandler.sendEmptyMessageDelayed(MSG_BUILD_SCHEDULE, earliest - currentTimeMs); } private RecordingTask createRecordingTask(ScheduledRecording schedule) { @@ -309,13 +354,18 @@ public class InputTaskScheduler { } private RecordingTask getReplacableTask(ScheduledRecording schedule) { + // Returns the recording with the following priority. + // 1. The recording with the lowest priority is returned. + // 2. If the priorities are the same, the recording which finishes early is returned. + // 3. If 1) and 2) are the same, the early created schedule is returned. int size = mPendingRecordings.size(); RecordingTask candidate = null; for (int i = 0; i < size; ++i) { RecordingTask task = mPendingRecordings.get(mPendingRecordings.keyAt(i)).mTask; - if (schedule.getPriority() > task.getPriority() - && (candidate == null || candidate.getPriority() > task.getPriority())) { - candidate = task; + if (schedule.getPriority() > task.getPriority()) { + if (candidate == null || CANDIDATE_COMPARATOR.compare(candidate, task) > 0) { + candidate = task; + } } } return candidate; @@ -372,6 +422,9 @@ public class InputTaskScheduler { case MSG_BUILD_SCHEDULE: handleBuildSchedule(); break; + case MSG_STOP_SCHEDULE: + handleStopSchedule(); + break; } } } diff --git a/src/com/android/tv/dvr/RecordedProgram.java b/src/com/android/tv/dvr/RecordedProgram.java index 085402a4..dd744f80 100644 --- a/src/com/android/tv/dvr/RecordedProgram.java +++ b/src/com/android/tv/dvr/RecordedProgram.java @@ -18,21 +18,25 @@ package com.android.tv.dvr; import static android.media.tv.TvContract.RecordedPrograms; +import android.annotation.TargetApi; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.media.tv.TvContract; import android.net.Uri; +import android.os.Build; import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.tv.common.R; import com.android.tv.data.BaseProgram; +import com.android.tv.data.GenreItems; import com.android.tv.data.InternalDataUtils; import com.android.tv.util.Utils; import java.util.Arrays; +import java.util.Collection; import java.util.Comparator; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -40,6 +44,7 @@ import java.util.concurrent.TimeUnit; /** * Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. */ +@TargetApi(Build.VERSION_CODES.N) public class RecordedProgram extends BaseProgram { public static final int ID_NOT_SET = -1; @@ -553,6 +558,21 @@ public class RecordedProgram extends BaseProgram { return mCanonicalGenres; } + /** + * Returns array of canonical genre ID's for this recorded program. + */ + @Override + public int[] getCanonicalGenreIds() { + if (mCanonicalGenres == null) { + return null; + } + int[] genreIds = new int[mCanonicalGenres.length]; + for (int i = 0; i < mCanonicalGenres.length; i++) { + genreIds[i] = GenreItems.getId(mCanonicalGenres[i]); + } + return genreIds; + } + @Override public long getChannelId() { return mChannelId; @@ -622,6 +642,21 @@ public class RecordedProgram extends BaseProgram { } } + @Nullable + public String getEpisodeDisplayNumber(Context context) { + if (!TextUtils.isEmpty(mEpisodeNumber)) { + if (TextUtils.equals(mSeasonNumber, "0")) { + // Do not show "S0: ". + return String.format(context.getResources().getString( + R.string.display_episode_number_format_no_season_number), mEpisodeNumber); + } else { + return String.format(context.getResources().getString( + R.string.display_episode_number_format), mSeasonNumber, mEpisodeNumber); + } + } + return null; + } + public long getExpireTimeUtcMillis() { return mExpireTimeUtcMillis; } @@ -678,6 +713,7 @@ public class RecordedProgram extends BaseProgram { return mSearchable; } + @Override public String getSeriesId() { return mSeriesId; } @@ -822,4 +858,11 @@ public class RecordedProgram extends BaseProgram { private static String safeEncode(@Nullable String[] genres) { return genres == null ? null : TvContract.Programs.Genres.encode(genres); } + + /** + * Returns an array containing all of the elements in the list. + */ + public static RecordedProgram[] toArray(Collection<RecordedProgram> recordedPrograms) { + return recordedPrograms.toArray(new RecordedProgram[recordedPrograms.size()]); + } } diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/RecordingTask.java index 2373f15c..c3d236b0 100644 --- a/src/com/android/tv/dvr/RecordingTask.java +++ b/src/com/android/tv/dvr/RecordingTask.java @@ -41,6 +41,7 @@ import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper; import com.android.tv.util.Clock; import com.android.tv.util.Utils; +import java.util.Comparator; import java.util.concurrent.TimeUnit; /** @@ -57,6 +58,39 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback private static final String TAG = "RecordingTask"; private static final boolean DEBUG = false; + /** + * Compares the end time in ascending order. + */ + public static final Comparator<RecordingTask> END_TIME_COMPARATOR + = new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getEndTimeMs(), rhs.getEndTimeMs()); + } + }; + + /** + * Compares ID in ascending order. + */ + public static final Comparator<RecordingTask> ID_COMPARATOR + = new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getScheduleId(), rhs.getScheduleId()); + } + }; + + /** + * Compares the priority in ascending order. + */ + public static final Comparator<RecordingTask> PRIORITY_COMPARATOR + = new Comparator<RecordingTask>() { + @Override + public int compare(RecordingTask lhs, RecordingTask rhs) { + return Long.compare(lhs.getPriority(), rhs.getPriority()); + } + }; + @VisibleForTesting static final int MSG_INITIALIZE = 1; @VisibleForTesting @@ -169,6 +203,14 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback } @Override + public void onConnectionFailed(String inputId) { + if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")"); + if (mRecordingSession != null) { + failAndQuit(); + } + } + + @Override public void onTuned(Uri channelUri) { if (DEBUG) Log.d(TAG, "onTuned"); if (mRecordingSession == null) { @@ -252,7 +294,8 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback String inputId = mChannel.getInputId(); mRecordingSession = mSessionManager.createRecordingSession(inputId, - "recordingTask-" + mScheduledRecording.getId(), this, mHandler); + "recordingTask-" + mScheduledRecording.getId(), this, + mHandler, mScheduledRecording.getEndTimeMs()); mState = State.SESSION_ACQUIRED; mDvrManager.addListener(this, mHandler); mRecordingSession.tune(inputId, mChannel.getUri()); @@ -302,11 +345,15 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback private void handleUpdateSchedule(ScheduledRecording schedule) { mScheduledRecording = schedule; // Check end time only. The start time is checked in InputTaskScheduler. - if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs() - && mState == State.RECORDING_STARTED) { - mHandler.removeMessages(MSG_STOP_RECORDING); - if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { - failAndQuit(); + if (schedule.getEndTimeMs() != mScheduledRecording.getEndTimeMs()) { + if (mRecordingSession != null) { + mRecordingSession.setEndTimeMs(schedule.getEndTimeMs()); + } + if (mState == State.RECORDING_STARTED) { + mHandler.removeMessages(MSG_STOP_RECORDING); + if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) { + failAndQuit(); + } } } } @@ -316,6 +363,10 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback return mState; } + private long getScheduleId() { + return mScheduledRecording.getId(); + } + /** * Returns the priority. */ @@ -359,7 +410,7 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state); mScheduledRecording = ScheduledRecording.buildFrom(mScheduledRecording).setState(state) .build(); - mMainThreadHandler.post(new Runnable() { + runOnMainThread(new Runnable() { @Override public void run() { ScheduledRecording schedule = mDataManager.getScheduledRecording( @@ -429,6 +480,19 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback removeRecordedProgram(); } + /** + * Clean up the task. + */ + public void cleanUp() { + if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) { + updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED); + } + release(); + if (mHandler != null) { + mHandler.removeCallbacksAndMessages(null); + } + } + @Override public String toString() { return getClass().getName() + "(" + mScheduledRecording + ")"; diff --git a/src/com/android/tv/dvr/ScheduledRecording.java b/src/com/android/tv/dvr/ScheduledRecording.java index a9673b40..2bda10ea 100644 --- a/src/com/android/tv/dvr/ScheduledRecording.java +++ b/src/com/android/tv/dvr/ScheduledRecording.java @@ -31,6 +31,7 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.dvr.provider.DvrContract.Schedules; +import com.android.tv.util.CompositeComparator; import com.android.tv.util.Utils; import java.lang.annotation.Retention; @@ -56,6 +57,9 @@ public final class ScheduledRecording implements Parcelable { */ public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1; + /** + * Compares the start time in ascending order. + */ public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR = new Comparator<ScheduledRecording>() { @Override @@ -65,7 +69,7 @@ public final class ScheduledRecording implements Parcelable { }; /** - * Compare the end time in ascending order. + * Compares the end time in ascending order. */ public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR = new Comparator<ScheduledRecording>() { @@ -76,34 +80,36 @@ public final class ScheduledRecording implements Parcelable { }; /** - * Compare priority in descending order. + * Compares ID in ascending order. The schedule with the larger ID was created later. */ - public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR + public static final Comparator<ScheduledRecording> ID_COMPARATOR = new Comparator<ScheduledRecording>() { @Override public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - int value = Long.compare(rhs.mPriority, lhs.mPriority); - if (value == 0) { - // New recording has the higher priority. - value = Long.compare(rhs.mId, lhs.mId); - } - return value; + return Long.compare(lhs.mId, rhs.mId); } }; - public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_COMPARATOR + /** + * Compares the priority in ascending order. + */ + public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR = new Comparator<ScheduledRecording>() { @Override public int compare(ScheduledRecording lhs, ScheduledRecording rhs) { - int value = START_TIME_COMPARATOR.compare(lhs, rhs); - if (value == 0) { - value = PRIORITY_COMPARATOR.compare(lhs, rhs); - } - return value; + return Long.compare(lhs.mPriority, rhs.mPriority); } }; /** + * Compares start time in ascending order and then priority in descending order and then ID in + * descending order. + */ + public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + = new CompositeComparator<>(START_TIME_COMPARATOR, PRIORITY_COMPARATOR.reversed(), + ID_COMPARATOR.reversed()); + + /** * Builds scheduled recordings from programs. */ public static Builder builder(String inputId, Program p) { @@ -285,6 +291,7 @@ public final class ScheduledRecording implements Parcelable { .setChannelId(orig.mChannelId) .setEndTimeMs(orig.mEndTimeMs) .setSeriesRecordingId(orig.mSeriesRecordingId) + .setPriority(orig.mPriority) .setProgramId(orig.mProgramId) .setProgramTitle(orig.mProgramTitle) .setStartTimeMs(orig.mStartTimeMs) @@ -766,6 +773,13 @@ public final class ScheduledRecording implements Parcelable { return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower(); } + /** + * Checks if the {@code schedule} overlaps with this schedule. + */ + public boolean isOverLapping(ScheduledRecording schedule) { + return mStartTimeMs < schedule.getEndTimeMs() && mEndTimeMs > schedule.getStartTimeMs(); + } + @Override public String toString() { return "ScheduledRecording[" + mId @@ -775,8 +789,8 @@ public final class ScheduledRecording implements Parcelable { + ",programId=" + mProgramId + ",programTitle=" + mProgramTitle + ",type=" + mType - + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) - + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) + + ",startTime=" + Utils.toIsoDateTimeString(mStartTimeMs) + "(" + mStartTimeMs + ")" + + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs) + "(" + mEndTimeMs + ")" + ",seasonNumber=" + mSeasonNumber + ",episodeNumber=" + mEpisodeNumber + ",episodeTitle=" + mEpisodeTitle diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/Scheduler.java index 25904ee4..ce78e1be 100644 --- a/src/com/android/tv/dvr/Scheduler.java +++ b/src/com/android/tv/dvr/Scheduler.java @@ -123,6 +123,9 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList * Stops the scheduler. */ public void stop() { + for (InputTaskScheduler inputTaskScheduler : mInputSchedulerMap.values()) { + inputTaskScheduler.stop(); + } mInputManager.removeCallback(this); mDataManager.removeScheduledRecordingListener(this); } @@ -173,13 +176,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList } boolean needToUpdateAlarm = false; for (ScheduledRecording schedule : schedules) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId()); - if (input == null) { - Log.e(TAG, "Can't find input for " + schedule); - mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); - continue; - } - InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); + InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); if (scheduler != null) { scheduler.removeSchedule(schedule); needToUpdateAlarm = true; @@ -198,12 +195,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList } // Update the recordings. for (ScheduledRecording schedule : schedules) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId()); - if (input == null) { - Log.e(TAG, "Can't find input for " + schedule); - continue; - } - InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); + InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); if (scheduler != null) { scheduler.updateSchedule(schedule); } @@ -228,7 +220,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList } private void scheduleRecordingSoon(ScheduledRecording schedule) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, schedule.getChannelId()); + TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId()); if (input == null) { Log.e(TAG, "Can't find input for " + schedule); mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); @@ -260,7 +252,7 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList Intent intent = new Intent(mContext, DvrStartRecordingReceiver.class); PendingIntent alarmIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0); // This will cancel the previous alarm. - mAlarmManager.set(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); + mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeAt, alarmIntent); } else { if (DEBUG) Log.d(TAG, "No future recording, alarm not set"); } diff --git a/src/com/android/tv/dvr/SeriesRecording.java b/src/com/android/tv/dvr/SeriesRecording.java index fc68eaf7..f0690f5f 100644 --- a/src/com/android/tv/dvr/SeriesRecording.java +++ b/src/com/android/tv/dvr/SeriesRecording.java @@ -21,10 +21,10 @@ import android.database.Cursor; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; -import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; +import com.android.tv.data.BaseProgram; import com.android.tv.data.Program; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; import com.android.tv.util.Utils; @@ -69,8 +69,8 @@ public class SeriesRecording implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, - value = {STATE_SERIES_NORMAL, STATE_SERIES_CANCELED}) - private @interface SeriesState {} + value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED}) + public @interface SeriesState {} /** * The state indicates that the series recording is a normal one. @@ -78,9 +78,9 @@ public class SeriesRecording implements Parcelable { public static final int STATE_SERIES_NORMAL = 0; /** - * The state indicates that the series recording is canceled. + * The state indicates that the series recording is stopped. */ - public static final int STATE_SERIES_CANCELED = 1; + public static final int STATE_SERIES_STOPPED = 1; /** * Compare priority in descending order. @@ -110,9 +110,9 @@ public class SeriesRecording implements Parcelable { }; /** - * Creates a new Builder with the values set from the series information of {@link Program}. + * Creates a new Builder with the values set from the series information of {@link BaseProgram}. */ - public static Builder builder(String inputId, Program p) { + public static Builder builder(String inputId, BaseProgram p) { return new Builder() .setInputId(inputId) .setSeriesId(p.getSeriesId()) @@ -190,7 +190,7 @@ public class SeriesRecording implements Parcelable { .setCanonicalGenreIds(c.getString(++index)) .setPosterUri(c.getString(++index)) .setPhotoUri(c.getString(++index)) - .setState(seriesRecordingCanceled(c.getString(++index))) + .setState(seriesRecordingState(c.getString(++index))) .build(); } @@ -220,7 +220,7 @@ public class SeriesRecording implements Parcelable { Utils.getCanonicalGenre(r.getCanonicalGenreIds())); values.put(SeriesRecordings.COLUMN_POSTER_URI, r.getPosterUri()); values.put(SeriesRecordings.COLUMN_PHOTO_URI, r.getPhotoUri()); - values.put(SeriesRecordings.COLUMN_STATE, seriesRecordingCanceled(r.getState())); + values.put(SeriesRecordings.COLUMN_STATE, seriesRecordingState(r.getState())); return values; } @@ -244,22 +244,22 @@ public class SeriesRecording implements Parcelable { return OPTION_CHANNEL_ONE; } - private static String seriesRecordingCanceled(@SeriesState int state) { + private static String seriesRecordingState(@SeriesState int state) { switch (state) { case STATE_SERIES_NORMAL: return SeriesRecordings.STATE_SERIES_NORMAL; - case STATE_SERIES_CANCELED: - return SeriesRecordings.STATE_SERIES_CANCELED; + case STATE_SERIES_STOPPED: + return SeriesRecordings.STATE_SERIES_STOPPED; } return SeriesRecordings.STATE_SERIES_NORMAL; } - @SeriesState private static int seriesRecordingCanceled(String state) { + @SeriesState private static int seriesRecordingState(String state) { switch (state) { case SeriesRecordings.STATE_SERIES_NORMAL: return STATE_SERIES_NORMAL; - case SeriesRecordings.STATE_SERIES_CANCELED: - return STATE_SERIES_CANCELED; + case SeriesRecordings.STATE_SERIES_STOPPED: + return STATE_SERIES_STOPPED; } return STATE_SERIES_NORMAL; } @@ -331,6 +331,7 @@ public class SeriesRecording implements Parcelable { mInputId = inputId; return this; } + /** * @see #getChannelId() */ @@ -478,7 +479,7 @@ public class SeriesRecording implements Parcelable { } /** - * The channelId to match. + * The channelId to match. The channel ID might not be valid when the channel option is "ALL". */ public long getChannelId() { return mChannelId; @@ -534,7 +535,6 @@ public class SeriesRecording implements Parcelable { * * <p>SeriesId is an opaque but stable string. */ - @NonNull public String getSeriesId() { return mSeriesId; } @@ -590,6 +590,13 @@ public class SeriesRecording implements Parcelable { return mState; } + /** + * Checks whether the series recording is stopped or not. + */ + public boolean isStopped() { + return mState == STATE_SERIES_STOPPED; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -695,7 +702,7 @@ public class SeriesRecording implements Parcelable { * episode constraints. */ public boolean matchProgram(Program program) { - return matchProgram(program, true); + return matchProgram(program, mChannelOption); } /** @@ -703,13 +710,12 @@ public class SeriesRecording implements Parcelable { * episode constraints. It checks the channel option only if {@code checkChannelOption} is * {@code true}. */ - public boolean matchProgram(Program program, boolean checkChannelOption) { + public boolean matchProgram(Program program, @ChannelOption int channelOption) { String seriesId = program.getSeriesId(); long channelId = program.getChannelId(); String seasonNumber = program.getSeasonNumber(); String episodeNumber = program.getEpisodeNumber(); - if (!mSeriesId.equals(seriesId) || (checkChannelOption - && mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE + if (!mSeriesId.equals(seriesId) || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE && mChannelId != channelId)) { return false; } diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/SeriesRecordingScheduler.java index 9e9b3add..5ed12ce8 100644 --- a/src/com/android/tv/dvr/SeriesRecordingScheduler.java +++ b/src/com/android/tv/dvr/SeriesRecordingScheduler.java @@ -20,19 +20,14 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.SharedPreferences; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.media.tv.TvContract.Programs; -import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.MainThread; -import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import android.util.LongSparseArray; import com.android.tv.ApplicationSingletons; import com.android.tv.TvApplication; @@ -43,10 +38,8 @@ import com.android.tv.data.Program; import com.android.tv.data.epg.EpgFetcher; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; +import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode; import com.android.tv.experiments.Experiments; -import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask; -import com.android.tv.util.AsyncDbTask.CursorFilter; -import com.android.tv.util.PermissionUtils; import java.util.ArrayList; import java.util.Arrays; @@ -59,7 +52,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.Set; /** @@ -70,20 +63,7 @@ import java.util.Set; @TargetApi(Build.VERSION_CODES.N) public class SeriesRecordingScheduler { private static final String TAG = "SeriesRecordingSchd"; - - private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID); - private static final int RECORDING_PROHIBITED_INDEX = - Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED); - - private static final String PARAM_START_TIME = "start_time"; - private static final String PARAM_END_TIME = "end_time"; - - private static final String PROGRAM_SELECTION = - Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND (" + - Programs.COLUMN_SEASON_DISPLAY_NUMBER + " IS NOT NULL OR " + - Programs.COLUMN_EPISODE_DISPLAY_NUMBER + " IS NOT NULL) AND " + - Programs.COLUMN_RECORDING_PROHIBITED + "=0"; - private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?"; + private static final boolean DEBUG = false; private static final String KEY_FETCHED_SERIES_IDS = "SeriesRecordingScheduler.fetched_series_ids"; @@ -109,6 +89,11 @@ public class SeriesRecordingScheduler { private final Set<String> mFetchedSeriesIds = new ArraySet<>(); private final SharedPreferences mSharedPreferences; private boolean mStarted; + private boolean mPaused; + private final Set<Long> mPendingSeriesRecordings = new ArraySet<>(); + private final Set<OnSeriesRecordingUpdatedListener> mOnSeriesRecordingUpdatedListeners = + new CopyOnWriteArraySet<>(); + private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() { @Override @@ -124,7 +109,7 @@ public class SeriesRecordingScheduler { for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); iter.hasNext(); ) { SeriesRecordingUpdateTask task = iter.next(); - if (CollectionUtils.subtract(task.mSeriesRecordings, seriesRecordings, + if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR).isEmpty()) { task.cancel(true); iter.remove(); @@ -134,7 +119,21 @@ public class SeriesRecordingScheduler { @Override public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { - updateSchedules(Arrays.asList(seriesRecordings)); + List<SeriesRecording> stopped = new ArrayList<>(); + List<SeriesRecording> normal = new ArrayList<>(); + for (SeriesRecording r : seriesRecordings) { + if (r.isStopped()) { + stopped.add(r); + } else { + normal.add(r); + } + } + if (!stopped.isEmpty()) { + onSeriesRecordingRemoved(SeriesRecording.toArray(stopped)); + } + if (!normal.isEmpty()) { + updateSchedules(normal); + } } }; @@ -174,8 +173,6 @@ public class SeriesRecordingScheduler { Set<Long> seriesRecordingIds = new HashSet<>(); for (ScheduledRecording r : schedules) { if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { - SoftPreconditions.checkState(r.getState() - != ScheduledRecording.STATE_RECORDING_FINISHED); seriesRecordingIds.add(r.getSeriesRecordingId()); } } @@ -214,6 +211,7 @@ public class SeriesRecordingScheduler { if (mStarted) { return; } + if (DEBUG) Log.d(TAG, "start"); mStarted = true; mDataManager.addSeriesRecordingListener(mSeriesRecordingListener); mDataManager.addScheduledRecordingListener(mScheduledRecordingListener); @@ -226,13 +224,16 @@ public class SeriesRecordingScheduler { if (!mStarted) { return; } + if (DEBUG) Log.d(TAG, "stop"); mStarted = false; for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) { task.cancel(true); } + mFetchSeriesInfoTasks.clear(); for (SeriesRecordingUpdateTask task : mScheduleTasks) { task.cancel(true); } + mScheduleTasks.clear(); mDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); mDataManager.removeSeriesRecordingListener(mSeriesRecordingListener); } @@ -254,26 +255,81 @@ public class SeriesRecordingScheduler { } /** - * Creates/Updates the schedules for all the series recordings. + * Pauses the updates of the series recordings. */ - @MainThread - public void updateSchedules() { + public void pauseUpdate() { + if (DEBUG) Log.d(TAG, "Schedule paused"); + if (mPaused) { + return; + } + mPaused = true; if (!mStarted) { return; } - updateSchedules(mDataManager.getSeriesRecordings()); + for (SeriesRecordingUpdateTask task : mScheduleTasks) { + for (SeriesRecording r : task.getSeriesRecordings()) { + mPendingSeriesRecordings.add(r.getId()); + } + task.cancel(true); + } + } + + /** + * Resumes the updates of the series recordings. + */ + public void resumeUpdate() { + if (DEBUG) Log.d(TAG, "Schedule resumed"); + if (!mPaused) { + return; + } + mPaused = false; + if (!mStarted) { + return; + } + if (!mPendingSeriesRecordings.isEmpty()) { + List<SeriesRecording> seriesRecordings = new ArrayList<>(); + for (long seriesRecordingId : mPendingSeriesRecordings) { + SeriesRecording seriesRecording = + mDataManager.getSeriesRecording(seriesRecordingId); + if (seriesRecording != null) { + seriesRecordings.add(seriesRecording); + } + } + if (!seriesRecordings.isEmpty()) { + updateSchedules(seriesRecordings); + } + } } - private void updateSchedules(Collection<SeriesRecording> seriesRecordings) { + /** + * Update schedules for the given series recordings. If it's paused, the update will be done + * after it's resumed. + */ + public void updateSchedules(Collection<SeriesRecording> seriesRecordings) { + if (DEBUG) Log.d(TAG, "updateSchedules:" + seriesRecordings); + if (!mStarted) { + if (DEBUG) Log.d(TAG, "Not started yet."); + return; + } + if (mPaused) { + for (SeriesRecording r : seriesRecordings) { + mPendingSeriesRecordings.add(r.getId()); + } + if (DEBUG) { + Log.d(TAG, "The scheduler has been paused. Adding to the pending list. size=" + + mPendingSeriesRecordings.size()); + } + return; + } Set<SeriesRecording> previousSeriesRecordings = new HashSet<>(); for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); iter.hasNext(); ) { SeriesRecordingUpdateTask task = iter.next(); - if (CollectionUtils.containsAny(task.mSeriesRecordings, seriesRecordings, + if (CollectionUtils.containsAny(task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR)) { // The task is affected by the seriesRecordings task.cancel(true); - previousSeriesRecordings.addAll(task.mSeriesRecordings); + previousSeriesRecordings.addAll(task.getSeriesRecordings()); iter.remove(); } } @@ -281,38 +337,44 @@ public class SeriesRecordingScheduler { previousSeriesRecordings, SeriesRecording.ID_COMPARATOR); for (Iterator<SeriesRecording> iter = seriesRecordingsToUpdate.iterator(); iter.hasNext(); ) { - if (mDataManager.getSeriesRecording(iter.next().getId()) == null) { - // Series recording has been removed. + SeriesRecording seriesRecording = mDataManager.getSeriesRecording(iter.next().getId()); + if (seriesRecording == null || seriesRecording.isStopped()) { + // Series recording has been removed or stopped. iter.remove(); } } if (seriesRecordingsToUpdate.isEmpty()) { return; } - List<SeriesRecordingUpdateTask> tasksToRun = new ArrayList<>(); if (needToReadAllChannels(seriesRecordingsToUpdate)) { - SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask(seriesRecordingsToUpdate, - createSqlParams(seriesRecordingsToUpdate, null)); - tasksToRun.add(task); + SeriesRecordingUpdateTask task = + new SeriesRecordingUpdateTask(seriesRecordingsToUpdate); mScheduleTasks.add(task); + if (DEBUG) Log.d(TAG, "Added schedule task: " + task); + task.execute(); } else { for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { SeriesRecordingUpdateTask task = new SeriesRecordingUpdateTask( - Collections.singletonList(seriesRecording), - createSqlParams(Collections.singletonList(seriesRecording), null)); - tasksToRun.add(task); + Collections.singletonList(seriesRecording)); mScheduleTasks.add(task); + if (DEBUG) Log.d(TAG, "Added schedule task: " + task); + task.execute(); } } - if (mDataManager.isDvrScheduleLoadFinished()) { - runTasks(tasksToRun); - } } - private void runTasks(List<SeriesRecordingUpdateTask> tasks) { - for (SeriesRecordingUpdateTask task : tasks) { - task.executeOnDbThread(); - } + /** + * Adds {@link OnSeriesRecordingUpdatedListener}. + */ + public void addOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) { + mOnSeriesRecordingUpdatedListeners.add(listener); + } + + /** + * Removes {@link OnSeriesRecordingUpdatedListener}. + */ + public void removeOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) { + mOnSeriesRecordingUpdatedListeners.remove(listener); } private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) { @@ -325,94 +387,6 @@ public class SeriesRecordingScheduler { } /** - * Queries the programs which are related to the series. - * <p> - * This is called from the UI when the series recording is created. - */ - public void queryPrograms(SeriesRecording series, ProgramLoadCallback callback) { - SoftPreconditions.checkState(mDataManager.isInitialized()); - Set<ScheduledEpisode> scheduledEpisodes = new HashSet<>(); - for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) { - if (series.getSeriesId().equals(recordedProgram.getSeriesId())) { - scheduledEpisodes.add(new ScheduledEpisode(series.getId(), - recordedProgram.getSeasonNumber(), recordedProgram.getEpisodeNumber())); - } - } - SqlParams sqlParams = createSqlParams(Collections.singletonList(series), scheduledEpisodes); - new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri, sqlParams.selection, - sqlParams.selectionArgs, null, sqlParams.filter) { - @Override - protected void onPostExecute(List<Program> programs) { - SoftPreconditions.checkNotNull(programs); - if (programs == null) { - Log.e(TAG, "Creating schedules for series recording failed: " + series); - callback.onProgramLoadFinished(Collections.emptyList()); - } else { - Map<Long, List<Program>> seriesProgramMap = pickOneProgramPerEpisode( - Collections.singletonList(series), programs); - callback.onProgramLoadFinished(seriesProgramMap.get(series.getId())); - } - } - }.executeOnDbThread(); - // To shorten the response time from UI, cancel and restart the background job. - restartTasks(); - } - - private void restartTasks() { - Set<SeriesRecording> seriesRecordings = new HashSet<>(); - for (SeriesRecordingUpdateTask task : mScheduleTasks) { - seriesRecordings.addAll(task.mSeriesRecordings); - task.cancel(true); - } - mScheduleTasks.clear(); - updateSchedules(seriesRecordings); - } - - private SqlParams createSqlParams(List<SeriesRecording> seriesRecordings, - Set<ScheduledEpisode> scheduledEpisodes) { - SqlParams sqlParams = new SqlParams(); - if (PermissionUtils.hasAccessAllEpg(mContext)) { - sqlParams.uri = Programs.CONTENT_URI; - if (needToReadAllChannels(seriesRecordings)) { - sqlParams.selection = PROGRAM_SELECTION; - sqlParams.selectionArgs = new String[] {Long.toString(System.currentTimeMillis())}; - } else { - SoftPreconditions.checkArgument(seriesRecordings.size() == 1); - sqlParams.selection = PROGRAM_SELECTION + " AND " + CHANNEL_ID_PREDICATE; - sqlParams.selectionArgs = new String[] {Long.toString(System.currentTimeMillis()), - Long.toString(seriesRecordings.get(0).getChannelId())}; - } - sqlParams.filter = new SeriesRecordingCursorFilter(seriesRecordings, scheduledEpisodes); - } else { - if (needToReadAllChannels(seriesRecordings)) { - sqlParams.uri = Programs.CONTENT_URI.buildUpon() - .appendQueryParameter(PARAM_START_TIME, - String.valueOf(System.currentTimeMillis())) - .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE)) - .build(); - } else { - SoftPreconditions.checkArgument(seriesRecordings.size() == 1); - sqlParams.uri = TvContract.buildProgramsUriForChannel( - seriesRecordings.get(0).getChannelId(), - System.currentTimeMillis(), Long.MAX_VALUE); - } - sqlParams.selection = null; - sqlParams.selectionArgs = null; - sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(seriesRecordings, - scheduledEpisodes); - } - return sqlParams; - } - - @VisibleForTesting - static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes, - ScheduledEpisode episode) { - // The episode whose season number or episode number is null will always be scheduled. - return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber) - && !TextUtils.isEmpty(episode.episodeNumber); - } - - /** * Pick one program per an episode. * * <p>Note that the programs which has been already scheduled have the highest priority, and all @@ -421,7 +395,7 @@ public class SeriesRecordingScheduler { * <p>If there are no existing schedules for an episode, one program which starts earlier is * picked. */ - private Map<Long, List<Program>> pickOneProgramPerEpisode( + private LongSparseArray<List<Program>> pickOneProgramPerEpisode( List<SeriesRecording> seriesRecordings, List<Program> programs) { return pickOneProgramPerEpisode(mDataManager, seriesRecordings, programs); } @@ -430,10 +404,11 @@ public class SeriesRecordingScheduler { * @see #pickOneProgramPerEpisode(List, List) */ @VisibleForTesting - static Map<Long, List<Program>> pickOneProgramPerEpisode(DvrDataManager dataManager, - List<SeriesRecording> seriesRecordings, List<Program> programs) { + static LongSparseArray<List<Program>> pickOneProgramPerEpisode( + DvrDataManager dataManager, List<SeriesRecording> seriesRecordings, + List<Program> programs) { // Initialize. - Map<Long, List<Program>> result = new HashMap<>(); + LongSparseArray<List<Program>> result = new LongSparseArray<>(); Map<String, Long> seriesRecordingIds = new HashMap<>(); for (SeriesRecording seriesRecording : seriesRecordings) { result.put(seriesRecording.getId(), new ArrayList<>()); @@ -508,27 +483,27 @@ public class SeriesRecordingScheduler { * This works only for the existing series recordings. Do not use this task for the * "adding series recording" UI. */ - private class SeriesRecordingUpdateTask extends AsyncProgramQueryTask { - private final List<SeriesRecording> mSeriesRecordings = new ArrayList<>(); - - SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings, SqlParams sqlParams) { - super(mContext.getContentResolver(), sqlParams.uri, sqlParams.selection, - sqlParams.selectionArgs, null, sqlParams.filter); - mSeriesRecordings.addAll(seriesRecordings); + private class SeriesRecordingUpdateTask extends EpisodicProgramLoadTask { + SeriesRecordingUpdateTask(List<SeriesRecording> seriesRecordings) { + super(mContext, seriesRecordings); } @Override protected void onPostExecute(List<Program> programs) { + if (DEBUG) Log.d(TAG, "onPostExecute: updating schedules with programs:" + programs); mScheduleTasks.remove(this); if (programs == null) { - Log.e(TAG, "Creating schedules for series recording failed: " + mSeriesRecordings); + Log.e(TAG, "Creating schedules for series recording failed: " + + getSeriesRecordings()); return; } - Map<Long, List<Program>> seriesProgramMap = pickOneProgramPerEpisode( - mSeriesRecordings, programs); - for (SeriesRecording seriesRecording : mSeriesRecordings) { + LongSparseArray<List<Program>> seriesProgramMap = pickOneProgramPerEpisode( + getSeriesRecordings(), programs); + for (SeriesRecording seriesRecording : getSeriesRecordings()) { // Check the series recording is still valid. - if (mDataManager.getSeriesRecording(seriesRecording.getId()) == null) { + SeriesRecording actualSeriesRecording = mDataManager.getSeriesRecording( + seriesRecording.getId()); + if (actualSeriesRecording == null || actualSeriesRecording.isStopped()) { continue; } List<Program> programsToSchedule = seriesProgramMap.get(seriesRecording.getId()); @@ -537,122 +512,25 @@ public class SeriesRecordingScheduler { mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule); } } - } - - @Override - protected void onCancelled(List<Program> programs) { - mScheduleTasks.remove(this); - } - } - - /** - * Filter the programs which match the series recording. The episodes which the schedules are - * already created for are filtered out too. - */ - private class SeriesRecordingCursorFilter implements CursorFilter { - private final List<SeriesRecording> mSeriesRecording = new ArrayList<>(); - private final Set<Long> mDisallowedProgramIds = new HashSet<>(); - private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>(); - - SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings, - Set<ScheduledEpisode> scheduledEpisodes) { - mSeriesRecording.addAll(seriesRecordings); - mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds()); - Set<Long> seriesRecordingIds = new HashSet<>(); - for (SeriesRecording r : seriesRecordings) { - seriesRecordingIds.add(r.getId()); - } - if (scheduledEpisodes != null) { - mScheduledEpisodes.addAll(scheduledEpisodes); - } - for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) { - if (seriesRecordingIds.contains(r.getSeriesRecordingId()) - && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED - && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { - mScheduledEpisodes.add(new ScheduledEpisode(r)); + if (!mOnSeriesRecordingUpdatedListeners.isEmpty()) { + for (OnSeriesRecordingUpdatedListener listener + : mOnSeriesRecordingUpdatedListeners) { + listener.onSeriesRecordingUpdated( + SeriesRecording.toArray(getSeriesRecordings())); } } } @Override - @WorkerThread - public boolean filter(Cursor c) { - if (mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) { - return false; - } - Program program = Program.fromCursor(c); - for (SeriesRecording seriesRecording : mSeriesRecording) { - boolean programMatches = seriesRecording.matchProgram(program); - if (programMatches && !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode( - seriesRecording.getId(), program.getSeasonNumber(), - program.getEpisodeNumber()))) { - return true; - } - } - return false; - } - } - - private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter { - SeriesRecordingCursorFilterForNonSystem(List<SeriesRecording> seriesRecordings, - Set<ScheduledEpisode> scheduledEpisodes) { - super(seriesRecordings, scheduledEpisodes); - } - - @Override - public boolean filter(Cursor c) { - return c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c); - } - } - - private static class SqlParams { - public Uri uri; - public String selection; - public String[] selectionArgs; - public CursorFilter filter; - } - - @VisibleForTesting - static class ScheduledEpisode { - public final long seriesRecordingId; - public final String seasonNumber; - public final String episodeNumber; - - /** - * Create a new Builder with the values set from an existing {@link ScheduledRecording}. - */ - ScheduledEpisode(ScheduledRecording r) { - this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); - } - - public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) { - this.seriesRecordingId = seriesRecordingId; - this.seasonNumber = seasonNumber; - this.episodeNumber = episodeNumber; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof ScheduledEpisode)) return false; - ScheduledEpisode that = (ScheduledEpisode) o; - return seriesRecordingId == that.seriesRecordingId - && Objects.equals(seasonNumber, that.seasonNumber) - && Objects.equals(episodeNumber, that.episodeNumber); - } - - @Override - public int hashCode() { - return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber); + protected void onCancelled(List<Program> programs) { + mScheduleTasks.remove(this); } @Override public String toString() { - return "ScheduledEpisode{" + - "seriesRecordingId=" + seriesRecordingId + - ", seasonNumber='" + seasonNumber + - ", episodeNumber=" + episodeNumber + - '}'; + return "SeriesRecordingUpdateTask:{" + + "series_recordings=" + getSeriesRecordings() + + "}"; } } @@ -663,10 +541,6 @@ public class SeriesRecordingScheduler { mSeriesRecording = seriesRecording; } - String getSeriesId() { - return mSeriesRecording.getSeriesId(); - } - @Override protected SeriesInfo doInBackground(Void... voids) { return EpgFetcher.createEpgReader(mContext) @@ -697,9 +571,9 @@ public class SeriesRecordingScheduler { } /** - * Called when the program loading is finished for the series recording. + * A listener to notify when series recording are updated. */ - public interface ProgramLoadCallback { - void onProgramLoadFinished(@NonNull List<Program> programs); + public interface OnSeriesRecordingUpdatedListener { + void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings); } } diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java index 382f7112..bf72d912 100644 --- a/src/com/android/tv/dvr/WritableDvrDataManager.java +++ b/src/com/android/tv/dvr/WritableDvrDataManager.java @@ -44,9 +44,13 @@ interface WritableDvrDataManager extends DvrDataManager { void removeScheduledRecording(ScheduledRecording... scheduledRecordings); /** + * Removes recordings. If {@code forceRemove} is {@code true}, the schedule will be permanently + * removed instead of changing the state to DELETED. + */ + void removeScheduledRecording(boolean forceRemove, ScheduledRecording... scheduledRecordings); + + /** * Removes series recordings. - * - * <p>Note that the finished or failed schedules are not deleted. */ void removeSeriesRecording(SeriesRecording... seasonSchedules); @@ -64,4 +68,11 @@ interface WritableDvrDataManager extends DvrDataManager { * Changes the state of the recording. */ void changeState(ScheduledRecording scheduledRecording, @RecordingState int newState); + + /** + * Remove all the records related to the input. + * <p> + * Note that this should be called after the input was removed. + */ + void forgetStorage(String inputId); } diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java index 3fe2d211..f0aca18e 100644 --- a/src/com/android/tv/dvr/provider/DvrContract.java +++ b/src/com/android/tv/dvr/provider/DvrContract.java @@ -233,9 +233,9 @@ public final class DvrContract { public static final String STATE_SERIES_NORMAL = "STATE_SERIES_NORMAL"; /** - * The state indicates that it is a canceled one. + * The state indicates that it is stopped. */ - public static final String STATE_SERIES_CANCELED = "STATE_SERIES_CANCELED"; + public static final String STATE_SERIES_STOPPED = "STATE_SERIES_STOPPED"; /** * The priority of this recording. @@ -380,7 +380,7 @@ public final class DvrContract { * The state of whether the series recording be canceled or not. * * <p>This value should be one of the followings: {@link #STATE_SERIES_NORMAL} and - * {@link #STATE_SERIES_CANCELED}. The default value is STATE_SERIES_NORMAL. + * {@link #STATE_SERIES_STOPPED}. The default value is STATE_SERIES_NORMAL. * * <p>Type: TEXT */ diff --git a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java index d6e17161..175f05bc 100644 --- a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java +++ b/src/com/android/tv/dvr/ui/DetailsContentPresenter.java @@ -16,27 +16,285 @@ package com.android.tv.dvr.ui; -import android.content.Context; -import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; +import android.app.Activity; +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; +import android.support.v17.leanback.widget.Presenter; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.android.tv.R; +import com.android.tv.ui.ViewUtils; import com.android.tv.util.Utils; /** - * Presents a {@link DetailsContent}. + * An {@link Presenter} for rendering a detailed description of an DVR item. + * Typically this Presenter will be used in a {@link DetailsOverviewRowPresenter}. + * Most codes of this class is originated from + * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}. + * The latter class are re-used to provide a customized version of + * {@link android.support.v17.leanback.widget.DetailsOverviewRow}. */ -public class DetailsContentPresenter extends AbstractDetailsDescriptionPresenter { +public class DetailsContentPresenter extends Presenter { + /** + * The ViewHolder for the {@link DetailsContentPresenter}. + */ + public static class ViewHolder extends Presenter.ViewHolder { + final TextView mTitle; + final TextView mSubtitle; + final LinearLayout mDescriptionContainer; + final TextView mBody; + final TextView mReadMoreView; + final int mTitleMargin; + final int mUnderTitleBaselineMargin; + final int mUnderSubtitleBaselineMargin; + final int mTitleLineSpacing; + final int mBodyLineSpacing; + final int mBodyMaxLines; + final int mBodyMinLines; + final FontMetricsInt mTitleFontMetricsInt; + final FontMetricsInt mSubtitleFontMetricsInt; + final FontMetricsInt mBodyFontMetricsInt; + final int mTitleMaxLines; + + private Activity mActivity; + private boolean mFullTextMode; + private int mFullTextAnimationDuration; + private boolean mIsListeningToPreDraw; + + private ViewTreeObserver.OnPreDrawListener mPreDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (mSubtitle.getVisibility() == View.VISIBLE + && mSubtitle.getTop() > view.getHeight() + && mTitle.getLineCount() > 1) { + mTitle.setMaxLines(mTitle.getLineCount() - 1); + return false; + } + final int bodyLines = mBody.getLineCount(); + final int maxLines = mFullTextMode ? bodyLines : + (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines); + if (bodyLines > maxLines) { + mReadMoreView.setVisibility(View.VISIBLE); + mDescriptionContainer.setFocusable(true); + mDescriptionContainer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mFullTextMode = true; + mReadMoreView.setVisibility(View.GONE); + mDescriptionContainer.setFocusable(false); + mDescriptionContainer.setOnClickListener(null); + mBody.setMaxLines(bodyLines); + // Minus 1 from line difference to eliminate the space + // originally occupied by "READ MORE" + showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing); + } + }); + } + if (mBody.getMaxLines() != maxLines) { + mBody.setMaxLines(maxLines); + return false; + } else { + removePreDrawListener(); + return true; + } + } + }; + + public ViewHolder(final View view) { + super(view); + mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title); + mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle); + mBody = (TextView) view.findViewById(R.id.dvr_details_description_body); + mDescriptionContainer = + (LinearLayout) view.findViewById(R.id.dvr_details_description_container); + mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more); + + FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle); + final int titleAscent = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_title_baseline); + // Ascent is negative + mTitleMargin = titleAscent + titleFontMetricsInt.ascent; + + mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_under_title_baseline_margin); + mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_under_subtitle_baseline_margin); + + mTitleLineSpacing = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_title_line_spacing); + mBodyLineSpacing = view.getResources().getDimensionPixelSize( + R.dimen.lb_details_description_body_line_spacing); + + mBodyMaxLines = view.getResources().getInteger( + R.integer.lb_details_description_body_max_lines); + mBodyMinLines = view.getResources().getInteger( + R.integer.lb_details_description_body_min_lines); + mTitleMaxLines = mTitle.getMaxLines(); + + mTitleFontMetricsInt = getFontMetricsInt(mTitle); + mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle); + mBodyFontMetricsInt = getFontMetricsInt(mBody); + } + + void addPreDrawListener() { + if (!mIsListeningToPreDraw) { + mIsListeningToPreDraw = true; + view.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); + } + } + + void removePreDrawListener() { + if (mIsListeningToPreDraw) { + view.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); + mIsListeningToPreDraw = false; + } + } + + public TextView getTitle() { + return mTitle; + } + + public TextView getSubtitle() { + return mSubtitle; + } + + public TextView getBody() { + return mBody; + } + + private FontMetricsInt getFontMetricsInt(TextView textView) { + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setTextSize(textView.getTextSize()); + paint.setTypeface(textView.getTypeface()); + return paint.getFontMetricsInt(); + } + + private void showFullText(int heightDiff) { + final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame); + int nowHeight = ViewUtils.getLayoutHeight(detailsFrame); + Animator expandAnimator = ViewUtils.createHeightAnimator( + detailsFrame, nowHeight, nowHeight + heightDiff); + expandAnimator.setDuration(mFullTextAnimationDuration); + Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame, + PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, + 0f, -(heightDiff / 2))); + shiftAnimator.setDuration(mFullTextAnimationDuration); + AnimatorSet fullTextAnimator = new AnimatorSet(); + fullTextAnimator.playTogether(expandAnimator, shiftAnimator); + fullTextAnimator.start(); + } + } + + private final Activity mActivity; + private final int mFullTextAnimationDuration; + + public DetailsContentPresenter(Activity activity) { + super(); + mActivity = activity; + mFullTextAnimationDuration = mActivity.getResources() + .getInteger(R.integer.dvr_details_full_text_animation_duration); + } + + @Override + public final ViewHolder onCreateViewHolder(ViewGroup parent) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.dvr_details_description, parent, false); + return new ViewHolder(v); + } + @Override - protected void onBindDescription(final ViewHolder viewHolder, Object itemData) { - DetailsContent detailsContent = (DetailsContent) itemData; - Context context = viewHolder.view.getContext(); - viewHolder.getTitle().setText(detailsContent.getTitle()); + public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { + final ViewHolder vh = (ViewHolder) viewHolder; + final DetailsContent detailsContent = (DetailsContent) item; + + vh.mActivity = mActivity; + vh.mFullTextAnimationDuration = mFullTextAnimationDuration; + + boolean hasTitle = true; + if (TextUtils.isEmpty(detailsContent.getTitle())) { + vh.mTitle.setVisibility(View.GONE); + hasTitle = false; + } else { + vh.mTitle.setText(detailsContent.getTitle()); + vh.mTitle.setVisibility(View.VISIBLE); + vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight() + + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier()); + vh.mTitle.setMaxLines(vh.mTitleMaxLines); + } + setTopMargin(vh.mTitle, vh.mTitleMargin); + + boolean hasSubtitle = true; if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) { - String playTime = Utils.getDurationString(context, + vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(), detailsContent.getStartTimeUtcMillis(), - detailsContent.getEndTimeUtcMillis(), false); - viewHolder.getSubtitle().setText(playTime); + detailsContent.getEndTimeUtcMillis(), false)); + vh.mSubtitle.setVisibility(View.VISIBLE); + if (hasTitle) { + setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin + + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent); + } else { + setTopMargin(vh.mSubtitle, 0); + } + } else { + vh.mSubtitle.setVisibility(View.GONE); + hasSubtitle = false; } - viewHolder.getBody().setText(detailsContent.getDescription()); + + if (TextUtils.isEmpty(detailsContent.getDescription())) { + vh.mBody.setVisibility(View.GONE); + } else { + vh.mBody.setText(detailsContent.getDescription()); + vh.mBody.setVisibility(View.VISIBLE); + vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight() + + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier()); + if (hasSubtitle) { + setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); + } else if (hasTitle) { + setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin + + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent + - vh.mBody.getPaddingTop()); + } else { + setTopMargin(vh.mDescriptionContainer, 0); + } + } + } + + @Override + public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { } + + @Override + public void onViewAttachedToWindow(Presenter.ViewHolder holder) { + // In case predraw listener was removed in detach, make sure + // we have the proper layout. + ViewHolder vh = (ViewHolder) holder; + vh.addPreDrawListener(); + super.onViewAttachedToWindow(holder); + } + + @Override + public void onViewDetachedFromWindow(Presenter.ViewHolder holder) { + ViewHolder vh = (ViewHolder) holder; + vh.removePreDrawListener(); + super.onViewDetachedFromWindow(holder); + } + + private void setTopMargin(View view, int topMargin) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); + lp.topMargin = topMargin; + view.setLayoutParams(lp); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java index 37f152f9..6714ecd3 100644 --- a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java +++ b/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java @@ -76,13 +76,17 @@ public class DetailsViewBackgroundHelper { * Sets the background color. */ public void setBackgroundColor(int color) { - mBackgroundManager.setColor(color); + if (mBackgroundManager.isAttached()) { + mBackgroundManager.setColor(color); + } } /** * Sets the background scrim. */ public void setScrim(int color) { - mBackgroundManager.setDimLayer(new ColorDrawable(color)); + if (mBackgroundManager.isAttached()) { + mBackgroundManager.setDimLayer(new ColorDrawable(color)); + } } } diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java index d7c2de88..9df228d1 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java @@ -96,7 +96,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment { if (action.getId() == ACTION_RECORD_ANYWAY) { getDvrManager().addSchedule(mProgram); } else if (action.getId() == ACTION_WATCH) { - DvrUiHelper.startDetailsActivity(getActivity(), mDuplicate, null); + DvrUiHelper.startDetailsActivity(getActivity(), mDuplicate, null, false); } dismissDialog(); } diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java index 74d0ba0b..a6dd31d1 100644 --- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java +++ b/src/com/android/tv/dvr/ui/DvrBrowseFragment.java @@ -26,23 +26,25 @@ import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ListRowPresenter; +import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.TitleViewAdapter; import android.text.TextUtils; import android.util.Log; +import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.GenreItems; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; -import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; +import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; +import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.RecordedProgram; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.SeriesRecording; -import com.android.tv.util.TvInputManagerHelper; import java.util.ArrayList; import java.util.Arrays; @@ -64,7 +66,7 @@ public class DvrBrowseFragment extends BrowseFragment implements private RecordedProgramAdapter mRecentAdapter; private ScheduleAdapter mScheduleAdapter; - private RecordedProgramAdapter mSeriesAdapter; + private SeriesAdapter mSeriesAdapter; private RecordedProgramAdapter[] mGenreAdapters = new RecordedProgramAdapter[GenreItems.getGenreCount() + 1]; private ListRow mRecentRow; @@ -72,7 +74,7 @@ public class DvrBrowseFragment extends BrowseFragment implements private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1]; private List<String> mGenreLabels; private DvrDataManager mDvrDataManager; - private TvInputManagerHelper mTvInputManagerHelper; + private DvrScheduleManager mDvrScheudleManager; private ArrayObjectAdapter mRowsAdapter; private ClassPresenterSelector mPresenterSelector; private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>(); @@ -107,7 +109,7 @@ public class DvrBrowseFragment extends BrowseFragment implements public int compare(Object lhs, Object rhs) { if (lhs instanceof ScheduledRecording) { if (rhs instanceof ScheduledRecording) { - return ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); } else { return -1; @@ -120,36 +122,15 @@ public class DvrBrowseFragment extends BrowseFragment implements } }; - private final TvInputCallback mTvInputCallback = new TvInputCallback() { + private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener = + new DvrScheduleManager.OnConflictStateChangeListener() { @Override - public void onInputAdded(String inputId) { - List<ScheduledRecording> scheduleRecordings = - mDvrDataManager.getScheduledRecordings(inputId); - if (!scheduleRecordings.isEmpty()) { - onScheduledRecordingStatusChanged(ScheduledRecording.toArray(scheduleRecordings)); - } - handleSeriesRecordingsChanged(mDvrDataManager.getSeriesRecordings(inputId)); - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - if (TextUtils.equals(recordedProgram.getInputId(), inputId)) { - handleRecordedProgramChanged(recordedProgram); + public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { + if (mScheduleAdapter != null) { + for (ScheduledRecording schedule : schedules) { + onScheduledRecordingStatusChanged(schedule); } } - postUpdateRows(); - } - - @Override - public void onInputRemoved(String inputId) { - List<ScheduledRecording> scheduleRecordings = - mDvrDataManager.getScheduledRecordings(inputId); - onScheduledRecordingRemoved( - scheduleRecordings.toArray(new ScheduledRecording[scheduleRecordings.size()])); - handleSeriesRecordingsRemoved(mDvrDataManager.getSeriesRecordings(inputId)); - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - if (TextUtils.equals(recordedProgram.getInputId(), inputId)) { - handleRecordedProgramRemoved(recordedProgram); - } - } - postUpdateRows(); } }; @@ -165,8 +146,9 @@ public class DvrBrowseFragment extends BrowseFragment implements if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); Context context = getContext(); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper(); + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrScheudleManager = singletons.getDvrScheduleManager(); mPresenterSelector = new ClassPresenterSelector() .addClassPresenter(ScheduledRecording.class, new ScheduledRecordingPresenter(context)) @@ -177,7 +159,7 @@ public class DvrBrowseFragment extends BrowseFragment implements mGenreLabels.add(getString(R.string.dvr_main_others)); setupUiElements(); setupAdapters(); - mTvInputManagerHelper.addCallback(mTvInputCallback); + mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); prepareEntranceTransition(); if (mDvrDataManager.isInitialized()) { startEntranceTransition(); @@ -194,9 +176,8 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy"); - super.onDestroy(); mHandler.removeCallbacks(mUpdateRowsRunnable); - mTvInputManagerHelper.removeCallback(mTvInputCallback); + mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener); mDvrDataManager.removeRecordedProgramListener(this); mDvrDataManager.removeScheduledRecordingListener(this); mDvrDataManager.removeSeriesRecordingListener(this); @@ -204,6 +185,12 @@ public class DvrBrowseFragment extends BrowseFragment implements mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); mRowsAdapter.clear(); mSeriesId2LatestProgram.clear(); + for (Presenter presenter : mPresenterSelector.getPresenters()) { + if (presenter instanceof DvrItemPresenter) { + ((DvrItemPresenter) presenter).unbindAllViewHolders(); + } + } + super.onDestroy(); } @Override @@ -221,9 +208,7 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public void onRecordedProgramLoadFinished() { for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - if (isInputExist(recordedProgram.getInputId())) { - handleRecordedProgramAdded(recordedProgram, true); - } + handleRecordedProgramAdded(recordedProgram, true); } updateRows(); if (mDvrDataManager.isInitialized()) { @@ -233,27 +218,27 @@ public class DvrBrowseFragment extends BrowseFragment implements } @Override - public void onRecordedProgramAdded(RecordedProgram recordedProgram) { - if (isInputExist(recordedProgram.getInputId())) { + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { handleRecordedProgramAdded(recordedProgram, true); - postUpdateRows(); } + postUpdateRows(); } @Override - public void onRecordedProgramChanged(RecordedProgram recordedProgram) { - if (isInputExist(recordedProgram.getInputId())) { + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { handleRecordedProgramChanged(recordedProgram); - postUpdateRows(); } + postUpdateRows(); } @Override - public void onRecordedProgramRemoved(RecordedProgram recordedProgram) { - if (isInputExist(recordedProgram.getInputId())) { + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { handleRecordedProgramRemoved(recordedProgram); - postUpdateRows(); } + postUpdateRows(); } // No need to call updateRows() during ScheduledRecordings' change because @@ -320,7 +305,7 @@ public class DvrBrowseFragment extends BrowseFragment implements private void setupAdapters() { mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); - mSeriesAdapter = new RecordedProgramAdapter(); + mSeriesAdapter = new SeriesAdapter(); for (int i = 0; i < mGenreAdapters.length; i++) { mGenreAdapters[i] = new RecordedProgramAdapter(); } @@ -330,9 +315,7 @@ public class DvrBrowseFragment extends BrowseFragment implements mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); // Recorded Programs. for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - if (isInputExist(recordedProgram.getInputId())) { - handleRecordedProgramAdded(recordedProgram, false); - } + handleRecordedProgramAdded(recordedProgram, false); } // Series Recordings. Series recordings should be added after recorded programs, because // we build series recordings' latest program information while adding recorded programs. @@ -426,13 +409,11 @@ public class DvrBrowseFragment extends BrowseFragment implements private void handleSeriesRecordingsAdded(List<SeriesRecording> seriesRecordings) { for (SeriesRecording seriesRecording : seriesRecordings) { - if (isInputExist(seriesRecording.getInputId())) { - mSeriesAdapter.add(seriesRecording); - if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - for (RecordedProgramAdapter adapter - : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { - adapter.add(seriesRecording); - } + mSeriesAdapter.add(seriesRecording); + if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { + for (RecordedProgramAdapter adapter + : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) { + adapter.add(seriesRecording); } } } @@ -450,15 +431,13 @@ public class DvrBrowseFragment extends BrowseFragment implements private void handleSeriesRecordingsChanged(List<SeriesRecording> seriesRecordings) { for (SeriesRecording seriesRecording : seriesRecordings) { - if (isInputExist(seriesRecording.getInputId())) { - mSeriesAdapter.change(seriesRecording); - if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { - updateGenreAdapters(getGenreAdapters( - seriesRecording.getCanonicalGenreIds()), seriesRecording); - } else { - // Remove series recording from all genre rows if it has no recorded program - updateGenreAdapters(new ArrayList<>(), seriesRecording); - } + mSeriesAdapter.change(seriesRecording); + if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) { + updateGenreAdapters(getGenreAdapters( + seriesRecording.getCanonicalGenreIds()), seriesRecording); + } else { + // Remove series recording from all genre rows if it has no recorded program + updateGenreAdapters(new ArrayList<>(), seriesRecording); } } } @@ -545,23 +524,18 @@ public class DvrBrowseFragment extends BrowseFragment implements } } - private boolean isInputExist(String inputId) { - return mTvInputManagerHelper.getTvInputInfo(inputId) != null; - } - private boolean needToShowScheduledRecording(ScheduledRecording recording) { int state = recording.getState(); - return isInputExist(recording.getInputId()) - && (state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS - || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED); + return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS + || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED; } private void updateLatestRecordedProgram(SeriesRecording seriesRecording) { RecordedProgram latestProgram = null; for (RecordedProgram program : mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) { - if (isInputExist(program.getInputId()) && (latestProgram == null || RecordedProgram - .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0)) { + if (latestProgram == null || RecordedProgram + .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0) { latestProgram = program; } } @@ -583,6 +557,27 @@ public class DvrBrowseFragment extends BrowseFragment implements } } + private class SeriesAdapter extends SortedArrayAdapter<SeriesRecording> { + SeriesAdapter() { + super(mPresenterSelector, new Comparator<SeriesRecording>() { + @Override + public int compare(SeriesRecording lhs, SeriesRecording rhs) { + if (lhs.isStopped() && !rhs.isStopped()) { + return 1; + } else if (!lhs.isStopped() && rhs.isStopped()) { + return -1; + } + return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs); + } + }); + } + + @Override + public long getId(SeriesRecording item) { + return item.getId(); + } + } + private class RecordedProgramAdapter extends SortedArrayAdapter<Object> { RecordedProgramAdapter() { this(Integer.MAX_VALUE); diff --git a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java deleted file mode 100644 index 78f73fd5..00000000 --- a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingFragment.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.app.DialogFragment; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.support.v17.leanback.widget.GuidanceStylist; -import android.support.v17.leanback.widget.GuidedAction; - -import com.android.tv.R; - -import java.util.List; - -/** - * A fragment which asks the user to cancel all series schedules recordings. - */ -public class DvrCancelAllSeriesRecordingFragment extends DvrGuidedStepFragment { - private static final int ACTION_CANCEL_ALL = 1; - private static final int ACTION_BACK = 2; - - @Override - public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString(R.string.dvr_series_schedules_dialog_cancel_all); - Drawable icon = getContext().getDrawable(R.drawable.ic_dvr_delete); - return new GuidanceStylist.Guidance(title, null, null, icon); - } - - @Override - public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL_ALL) - .title(getResources().getString(R.string.dvr_series_schedules_cancel_all)) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_BACK) - .title(getResources().getString(R.string.dvr_series_schedules_dialog_back)) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - DvrSchedulesActivity activity = (DvrSchedulesActivity) getActivity(); - if (action.getId() == ACTION_CANCEL_ALL) { - activity.onCancelAllClicked(); - } - dismissDialog(); - } -} diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java index fe65eebd..837d8ab2 100644 --- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java @@ -26,7 +26,6 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; -import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelRecordConflictFragment; @@ -38,7 +37,6 @@ import java.util.concurrent.TimeUnit; public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragment { private final List<Long> mDurations = new ArrayList<>(); private Channel mChannel; - private Program mProgram; @Override public void onCreate(Bundle savedInstanceState) { diff --git a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java index b273c85c..806c775c 100644 --- a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrDetailsActivity.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; import com.android.tv.R; +import com.android.tv.TvApplication; /** * Activity to show details view in DVR. @@ -69,6 +70,7 @@ public class DvrDetailsActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_details); long recordId = getIntent().getLongExtra(RECORDING_ID, -1); diff --git a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java index be995fcb..21f9c4b4 100644 --- a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/DvrDetailsFragment.java @@ -17,10 +17,14 @@ package com.android.tv.dvr.ui; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.media.tv.TvContentRating; +import android.media.tv.TvInputManager; +import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v17.leanback.app.DetailsFragment; @@ -36,11 +40,22 @@ import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; +import android.widget.Toast; import com.android.tv.R; +import com.android.tv.TvApplication; import com.android.tv.data.BaseProgram; import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.DvrPlaybackActivity; +import com.android.tv.dvr.RecordedProgram; +import com.android.tv.parental.ParentalControlSettings; import com.android.tv.util.ImageLoader; +import com.android.tv.util.ToastUtils; +import com.android.tv.util.Utils; + +import java.io.File; abstract class DvrDetailsFragment extends DetailsFragment { private static final int LOAD_LOGO_IMAGE = 1; @@ -59,6 +74,7 @@ abstract class DvrDetailsFragment extends DetailsFragment { } mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity()); setupAdapter(); + onCreateInternal(); } @Override @@ -74,8 +90,8 @@ abstract class DvrDetailsFragment extends DetailsFragment { } private void setupAdapter() { - DetailsOverviewRowPresenter rowPresenter = - new DetailsOverviewRowPresenter(new DetailsContentPresenter()); + DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter( + new DetailsContentPresenter(getActivity())); rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background, null)); rowPresenter.setSharedElementEnterTransition(getActivity(), @@ -105,13 +121,22 @@ abstract class DvrDetailsFragment extends DetailsFragment { /** * Creates and returns presenter selector will be used by rows adaptor. */ - protected PresenterSelector onCreatePresenterSelector(DetailsOverviewRowPresenter rowPresenter) { + protected PresenterSelector onCreatePresenterSelector( + DetailsOverviewRowPresenter rowPresenter) { ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); return presenterSelector; } /** + * Does customized initialization of subclasses. Since {@link #onCreate(Bundle)} might finish + * activity early when it cannot fetch valid recordings, subclasses' onCreate method should not + * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to + * do after the super class did onCreate, it should override this method and put the codes here. + */ + protected void onCreateInternal() { } + + /** * Updates actions of details overview. */ protected void updateActions() { @@ -198,6 +223,84 @@ abstract class DvrDetailsFragment extends DetailsFragment { } } + protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) { + if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) && + !isDataUriAccessible(recordedProgram.getDataUri())) { + // Since cleaning RecordedProgram from forgotten storage will take some time, + // ignore playback until cleaning is finished. + ToastUtils.show(getContext(), + getContext().getResources().getString(R.string.dvr_toast_recording_deleted), + Toast.LENGTH_SHORT); + return; + } + ParentalControlSettings parental = TvApplication.getSingletons(getActivity()) + .getTvInputManagerHelper().getParentalControlSettings(); + if (!parental.isParentalControlsEnabled()) { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + return; + } + ChannelDataManager channelDataManager = + TvApplication.getSingletons(getActivity()).getChannelDataManager(); + Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId()); + if (channel != null && channel.isLocked()) { + checkPinToPlay(recordedProgram, seekTimeMs); + return; + } + String ratingString = recordedProgram.getContentRating(); + if (TextUtils.isEmpty(ratingString)) { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + return; + } + String[] ratingList = ratingString.split(","); + TvContentRating[] programRatings = new TvContentRating[ratingList.length]; + for (int i = 0; i < ratingList.length; i++) { + programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); + } + TvContentRating blockRatings = parental.getBlockedRating(programRatings); + if (blockRatings != null) { + checkPinToPlay(recordedProgram, seekTimeMs); + } else { + launchPlaybackActivity(recordedProgram, seekTimeMs, false); + } + } + + private boolean isDataUriAccessible(Uri dataUri) { + if (dataUri == null || dataUri.getPath() == null) { + return false; + } + try { + File recordedProgramPath = new File(dataUri.getPath()); + if (recordedProgramPath.exists()) { + return true; + } + } catch (SecurityException e) { + } + return false; + } + + private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { + new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, + new PinDialogFragment.ResultListener() { + @Override + public void done(boolean success) { + if (success) { + launchPlaybackActivity(recordedProgram, seekTimeMs, true); + } + } + }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); + } + + private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs, + boolean pinChecked) { + Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); + if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); + } + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); + getActivity().startActivity(intent); + } + private static class MyImageLoaderCallback extends ImageLoader.ImageLoaderCallback<DvrDetailsFragment> { private final Context mContext; diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java index 6f287c70..73ddcdd0 100644 --- a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java @@ -28,8 +28,6 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; import java.util.List; @@ -77,14 +75,7 @@ public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment { return; } DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - DvrDataManager dataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); - List<SeriesRecording> seriesRecordings = dataManager.getSeriesRecordings(mInputId); - for(SeriesRecording series : seriesRecordings) { - dvrManager.removeSeriesRecording(series.getId()); - } - List<ScheduledRecording> scheduledRecordings = dataManager.getScheduledRecordings(mInputId); - dvrManager.removeScheduledRecording(ScheduledRecording.toArray(scheduledRecordings)); - dvrManager.removeRecordedProgramByMissingStorage(mInputId); + dvrManager.forgetStorage(mInputId); Activity activity = getActivity(); if (activity instanceof DvrDetailsActivity) { // Since we removed everything, just finish the activity. diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java index eaccd8ed..d26e6836 100644 --- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java +++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java @@ -20,6 +20,7 @@ import android.app.DialogFragment; import android.content.Context; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; import android.view.View; @@ -30,9 +31,11 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ui.HalfSizedDialogFragment.OnActionClickListener; public class DvrGuidedStepFragment extends GuidedStepFragment { private DvrManager mDvrManager; + private OnActionClickListener mOnActionClickListener; protected DvrManager getDvrManager() { return mDvrManager; @@ -60,6 +63,14 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { return R.style.Theme_TV_Dvr_GuidedStep; } + @Override + public void onGuidedActionClicked(GuidedAction action) { + if (mOnActionClickListener != null) { + mOnActionClickListener.onActionClick(action.getId()); + } + dismissDialog(); + } + protected void dismissDialog() { if (getActivity() instanceof MainActivity) { SafeDismissDialogFragment currentDialog = @@ -71,4 +82,8 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { ((DialogFragment) getParentFragment()).dismiss(); } } -} + + protected void setOnActionClickListener(OnActionClickListener listener) { + mOnActionClickListener = listener; + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java index 50187a56..2b132db8 100644 --- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java @@ -20,17 +20,21 @@ import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidedAction; +import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.data.ParcelableList; +import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.guide.ProgramGuide; +import java.util.List; + public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** * Key for input ID. @@ -43,11 +47,6 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { */ public static final String KEY_PROGRAM = "DvrHalfSizedDialogFragment.program"; /** - * Key for the programs. - * Type: {@link ParcelableList}. - */ - public static final String KEY_PROGRAMS = "DvrHalfSizedDialogFragment.programs"; - /** * Key for the channel ID. * Type: long. */ @@ -90,23 +89,35 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } public abstract static class DvrGuidedStepDialogFragment extends DvrHalfSizedDialogFragment { + private DvrGuidedStepFragment mFragment; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); - GuidedStepFragment fragment = onCreateGuidedStepFragment(); - fragment.setArguments(getArguments()); - GuidedStepFragment.add(getChildFragmentManager(), fragment, R.id.halfsized_dialog_host); + mFragment = onCreateGuidedStepFragment(); + mFragment.setArguments(getArguments()); + mFragment.setOnActionClickListener(getOnActionClickListener()); + GuidedStepFragment.add(getChildFragmentManager(), + mFragment, R.id.halfsized_dialog_host); return view; } - protected abstract GuidedStepFragment onCreateGuidedStepFragment(); + @Override + public void setOnActionClickListener(OnActionClickListener listener) { + super.setOnActionClickListener(listener); + if (mFragment != null) { + mFragment.setOnActionClickListener(listener); + } + } + + protected abstract DvrGuidedStepFragment onCreateGuidedStepFragment(); } /** A dialog fragment for {@link DvrScheduleFragment}. */ public static class DvrScheduleDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrScheduleFragment(); } } @@ -114,7 +125,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** A dialog fragment for {@link DvrProgramConflictFragment}. */ public static class DvrProgramConflictDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrProgramConflictFragment(); } } @@ -122,7 +133,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** A dialog fragment for {@link DvrChannelWatchConflictFragment}. */ public static class DvrChannelWatchConflictDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrChannelWatchConflictFragment(); } } @@ -131,7 +142,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { public static class DvrChannelRecordDurationOptionDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrChannelRecordDurationOptionFragment(); } } @@ -140,7 +151,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { public static class DvrInsufficientSpaceErrorDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrInsufficientSpaceErrorFragment(); } } @@ -149,15 +160,52 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { public static class DvrMissingStorageErrorDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrMissingStorageErrorFragment(); } } + /** + * A dialog fragment to show error message when the current storage is too small to + * support DVR + */ + public static class DvrSmallSizedStorageErrorDialogFragment + extends DvrGuidedStepDialogFragment { + @Override + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { + return new DvrGuidedStepFragment() { + @Override + public Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getResources().getString( + R.string.dvr_error_small_sized_storage_title); + String description = getResources().getString( + R.string.dvr_error_small_sized_storage_description, + DvrStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES / 1024 + / 1024 / 1024); + return new Guidance(title, description, null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(GuidedAction.ACTION_ID_OK) + .title(android.R.string.ok) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + dismissDialog(); + } + }; + } + } + /** A dialog fragment for {@link DvrStopRecordingFragment}. */ public static class DvrStopRecordingDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrStopRecordingFragment(); } } @@ -165,7 +213,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** A dialog fragment for {@link DvrAlreadyScheduledFragment}. */ public static class DvrAlreadyScheduledDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrAlreadyScheduledFragment(); } } @@ -173,8 +221,8 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** A dialog fragment for {@link DvrAlreadyRecordedFragment}. */ public static class DvrAlreadyRecordedDialogFragment extends DvrGuidedStepDialogFragment { @Override - protected GuidedStepFragment onCreateGuidedStepFragment() { + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { return new DvrAlreadyRecordedFragment(); } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/DvrItemPresenter.java new file mode 100644 index 00000000..339e5d2f --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrItemPresenter.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.app.Activity; +import android.support.annotation.CallSuper; +import android.support.v17.leanback.widget.Presenter; +import android.view.View; +import android.view.View.OnClickListener; + +import com.android.tv.dvr.DvrUiHelper; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in + * {@link DvrBrowseFragment}. DVR items might include: {@link ScheduledRecording}, + * {@link RecordedProgram}, and {@link SeriesRecording}. + */ +public abstract class DvrItemPresenter extends Presenter { + private final Set<ViewHolder> mBoundViewHolders = new HashSet<>(); + private final OnClickListener mOnClickListener = onCreateOnClickListener(); + + @Override + @CallSuper + public void onBindViewHolder(ViewHolder viewHolder, Object o) { + viewHolder.view.setTag(o); + viewHolder.view.setOnClickListener(mOnClickListener); + mBoundViewHolders.add(viewHolder); + } + + @Override + @CallSuper + public void onUnbindViewHolder(ViewHolder viewHolder) { + mBoundViewHolders.remove(viewHolder); + } + + /** + * Unbinds all bound view holders. + */ + public void unbindAllViewHolders() { + // When browse fragments are destroyed, RecyclerView would not call presenters' + // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks. + for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) { + onUnbindViewHolder(viewHolder); + } + } + + /** + * Creates {@link OnClickListener} for DVR library's card views. + */ + protected OnClickListener onCreateOnClickListener() { + return new OnClickListener() { + @Override + public void onClick(View view) { + if (view instanceof RecordingCardView) { + RecordingCardView v = (RecordingCardView) view; + DvrUiHelper.startDetailsActivity((Activity) v.getContext(), + v.getTag(), v.getImageView(), false); + } + } + }; + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java index 7be92f1e..8c4c856c 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java +++ b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java @@ -22,6 +22,7 @@ import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import com.android.tv.R; @@ -36,8 +37,6 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { private static final String TAG = "DvrPlaybackCardPresenter"; private static final boolean DEBUG = false; - private int mSelectedBackgroundColor = -1; - private int mDefaultBackgroundColor = -1; private final int mRelatedRecordingCardWidth; private final int mRelatedRecordingCardHeight; @@ -58,12 +57,17 @@ public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { } @Override - public void onClick(View v) { - long programId = (long) v.getTag(); - if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); - Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); - getContext().startActivity(intent); + protected OnClickListener onCreateOnClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + long programId = ((RecordedProgram) v.getTag()).getId(); + if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); + Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); + getContext().startActivity(intent); + } + }; } @Override diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java index 1a3ae43c..0bc4ecb1 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java +++ b/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java @@ -126,6 +126,13 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (mReadyToControl) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE && event.getAction() == KeyEvent.ACTION_DOWN + && (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING + || mPlaybackState == PlaybackState.STATE_REWINDING)) { + // Workaround of b/31489271. Clicks play/pause button first to reset play controls + // to "play" state. Then we can pass MEDIA_PAUSE to let playback be paused. + onActionClicked(getControlsRow().getActionForKeyCode(keyCode)); + } return super.onKey(v, keyCode, event); } return false; @@ -134,10 +141,7 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { @Override public boolean hasValidMedia() { PlaybackState playbackState = mMediaController.getPlaybackState(); - if (playbackState == null) { - return false; - } - return true; + return playbackState != null; } @Override diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java index 9184f4f7..51ec93b8 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java +++ b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java @@ -33,6 +33,7 @@ import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.PlaybackControlsRow; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; +import android.support.v17.leanback.widget.SinglePresenterSelector; import android.view.Display; import android.view.View; import android.view.ViewGroup; @@ -42,6 +43,7 @@ import android.util.Log; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.data.BaseProgram; import com.android.tv.dvr.RecordedProgram; import com.android.tv.dialog.PinDialogFragment; import com.android.tv.dvr.DvrDataManager; @@ -63,7 +65,8 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { private DvrPlaybackMediaSessionHelper mMediaSessionHelper; private DvrPlaybackControlHelper mPlaybackControlHelper; private ArrayObjectAdapter mRowsAdapter; - private ArrayObjectAdapter mRelatedRecordingsRowAdapter; + private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter; + private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; private DvrDataManager mDvrDataManager; private ContentRatingsManager mContentRatingsManager; private TvView mTvView; @@ -108,7 +111,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); mBlockScreenView = getActivity().findViewById(R.id.block_screen); mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( - getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView)); + getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView), this); mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); setUpRows(); preparePlayback(getActivity().getIntent()); @@ -166,9 +169,10 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy"); - super.onDestroy(); mPlaybackControlHelper.unregisterCallback(); mMediaSessionHelper.release(); + mRelatedRecordingCardPresenter.unbindAllViewHolders(); + super.onDestroy(); } /** @@ -196,6 +200,15 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { updateAspectRatio(mAppliedAspectRatio); } + public RecordedProgram getNextEpisode(RecordedProgram program) { + int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); + if (position == mRelatedRecordingsRowAdapter.size()) { + return null; + } else { + return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); + } + } + void onMediaControllerUpdated() { mRowsAdapter.notifyArrayItemRangeChanged(0, 1); } @@ -261,8 +274,8 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { } private ListRow getRelatedRecordingsRow() { - mRelatedRecordingsRowAdapter = - new ArrayObjectAdapter(new DvrPlaybackCardPresenter(getActivity())); + mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); + mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); HeaderItem header = new HeaderItem(0, getActivity().getString(R.string.dvr_playback_related_recordings)); return new ListRow(header, mRelatedRecordingsRowAdapter); @@ -277,4 +290,15 @@ public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, TvInputManager.TIME_SHIFT_INVALID_TIME); } + + private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> { + RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { + super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); + } + + @Override + long getId(BaseProgram item) { + return item.getId(); + } + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java index a907b198..da6d1637 100644 --- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java +++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java @@ -17,7 +17,6 @@ package com.android.tv.dvr.ui; import android.annotation.TargetApi; -import android.app.ProgressDialog; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Build; @@ -27,7 +26,6 @@ import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.text.format.DateUtils; -import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; @@ -37,10 +35,10 @@ import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.SeriesRecordingScheduler.ProgramLoadCallback; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.util.Utils; +import java.util.Collections; import java.util.List; /** @@ -50,6 +48,8 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrScheduleFragment extends DvrGuidedStepFragment { + private static final String TAG = "DvrScheduleFragment"; + private static final int ACTION_RECORD_EPISODE = 1; private static final int ACTION_RECORD_SERIES = 2; @@ -62,8 +62,12 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); } DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic() - && dvrManager.getSeriesRecording(mProgram) == null); + SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic(), TAG, + "The program should be episodic: " + mProgram); + SeriesRecording seriesRecording = dvrManager.getSeriesRecording(mProgram); + SoftPreconditions.checkArgument(seriesRecording == null + || seriesRecording.isStopped(), TAG, + "The series recording should be stopped or null: " + seriesRecording); super.onCreate(savedInstanceState); } @@ -122,19 +126,22 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { R.id.halfsized_dialog_host); } } else if (action.getId() == ACTION_RECORD_SERIES) { - ProgressDialog dialog = ProgressDialog.show(getContext(), null, - getString(R.string.dvr_schedule_progress_message_reading_programs)); - getDvrManager().queryProgramsForSeries(mProgram, new ProgramLoadCallback() { - @Override - public void onProgramLoadFinished(@NonNull List<Program> programs) { - dialog.dismiss(); - // TODO: Create series recording in series settings fragment. - SeriesRecording seriesRecording = - getDvrManager().addSeriesRecording(mProgram, programs); - DvrUiHelper.startSeriesSettingsActivity(getContext(), seriesRecording.getId()); - dismissDialog(); - } - }); + SeriesRecording seriesRecording = TvApplication.getSingletons(getContext()) + .getDvrDataManager().getSeriesRecording(mProgram.getSeriesId()); + if (seriesRecording == null) { + seriesRecording = getDvrManager().addSeriesRecording(mProgram, + Collections.emptyList(), SeriesRecording.STATE_SERIES_STOPPED); + } else { + // Reset priority to the highest. + seriesRecording = SeriesRecording.buildFrom(seriesRecording) + .setPriority(TvApplication.getSingletons(getContext()) + .getDvrScheduleManager().suggestNewSeriesPriority()) + .build(); + getDvrManager().updateSeriesRecording(seriesRecording); + } + DvrUiHelper.startSeriesSettingsActivity(getContext(), + seriesRecording.getId(), null, true, true, true); + dismissDialog(); } } } diff --git a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java index 316cb381..f6e6ac26 100644 --- a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java @@ -17,16 +17,24 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.app.ProgressDialog; import android.os.Bundle; import android.support.annotation.IntDef; -import android.support.v17.leanback.app.DetailsFragment; import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Program; +import com.android.tv.dvr.EpisodicProgramLoadTask; +import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.SeriesRecordingScheduler; import com.android.tv.dvr.ui.list.DvrSchedulesFragment; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Activity to show the list of recording schedules. @@ -49,46 +57,48 @@ public class DvrSchedulesActivity extends Activity { * A type which means the activity will display a scheduled recording list of a series * recording. */ - public final static int TYPE_SERIES_SCHEDULE = 1; - - private Runnable mCancelAllClickedRunnable; + public static final int TYPE_SERIES_SCHEDULE = 1; @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + public void onCreate(final Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); + // Pass null to prevent automatically re-creating fragments + super.onCreate(null); setContentView(R.layout.activity_dvr_schedules); - if (savedInstanceState == null) { - int schedulesType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE); - DetailsFragment schedulesFragment = null; - if (schedulesType == TYPE_FULL_SCHEDULE) { - schedulesFragment = new DvrSchedulesFragment(); - schedulesFragment.setArguments(getIntent().getExtras()); - } else if (schedulesType == TYPE_SERIES_SCHEDULE) { - schedulesFragment = new DvrSeriesSchedulesFragment(); - schedulesFragment.setArguments(getIntent().getExtras()); - } - if (schedulesFragment != null) { - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); - } else { - finish(); - } - } - } - - /** - * Sets cancel all runnable which will implement operations after clicking cancel all dialog. - */ - public void setCancelAllClickedRunnable(Runnable cancelAllClickedRunnable) { - mCancelAllClickedRunnable = cancelAllClickedRunnable; - } - - /** - * Operations after clicking the cancel all. - */ - public void onCancelAllClicked() { - if (mCancelAllClickedRunnable != null) { - mCancelAllClickedRunnable.run(); + int scheduleType = getIntent().getIntExtra(KEY_SCHEDULES_TYPE, TYPE_FULL_SCHEDULE); + if (scheduleType == TYPE_FULL_SCHEDULE) { + DvrSchedulesFragment schedulesFragment = new DvrSchedulesFragment(); + schedulesFragment.setArguments(getIntent().getExtras()); + getFragmentManager().beginTransaction().add( + R.id.fragment_container, schedulesFragment).commit(); + } else if (scheduleType == TYPE_SERIES_SCHEDULE) { + final ProgressDialog dialog = ProgressDialog.show(this, null, getString( + R.string.dvr_series_schedules_progress_message_reading_programs)); + SeriesRecording seriesRecording = getIntent().getExtras() + .getParcelable(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_RECORDING); + // To get programs faster, hold the update of the series schedules. + SeriesRecordingScheduler.getInstance(this).pauseUpdate(); + new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { + @Override + protected void onPostExecute(List<Program> programs) { + SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this).resumeUpdate(); + dialog.dismiss(); + Bundle args = getIntent().getExtras(); + args.putParcelableArrayList(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, new ArrayList<>(programs)); + DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); + schedulesFragment.setArguments(args); + getFragmentManager().beginTransaction().add( + R.id.fragment_container, schedulesFragment).commit(); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true) + .execute(); + } else { + finish(); } } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index ab695234..f57e4b05 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java @@ -37,6 +37,7 @@ public class DvrSeriesDeletionActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_series_settings); // Check savedInstanceState to prevent that activity is being showed with animation. diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java new file mode 100644 index 00000000..1a0d13d3 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledDialogActivity.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v17.leanback.app.GuidedStepFragment; + +import com.android.tv.R; + +public class DvrSeriesScheduledDialogActivity extends Activity { + /** + * Name of series recording id added to the Intent. + */ + public static final String SERIES_RECORDING_ID = "series_recording_id"; + + /** + * Name of flag to check if the dialog should show view schedule option. + */ + public static final String SHOW_VIEW_SCHEDULE_OPTION = "show_view_schedule_option"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.halfsized_dialog); + if (savedInstanceState == null) { + DvrSeriesScheduledFragment dvrSeriesScheduledFragment = + new DvrSeriesScheduledFragment(); + dvrSeriesScheduledFragment.setArguments(getIntent().getExtras()); + GuidedStepFragment.addAsRoot(this, dvrSeriesScheduledFragment, + R.id.halfsized_dialog_host); + } + } +} diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java new file mode 100644 index 00000000..1173df46 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.v17.leanback.widget.GuidanceStylist; +import android.support.v17.leanback.widget.GuidedAction; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; + +import java.util.List; + +public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { + private final static long SERIES_RECORDING_ID_NOT_SET = -1; + + private final static int ACTION_VIEW_SCHEDULES = 1; + + private DvrScheduleManager mDvrScheduleManager; + private SeriesRecording mSeriesRecording; + private boolean mShowViewScheduleOption; + + private int mSchedulesAddedCount = 0; + private boolean mHasConflict = false; + private int mInThisSeriesConflictCount = 0; + private int mOutThisSeriesConflictCount = 0; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + long seriesRecordingId = getArguments().getLong( + DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, SERIES_RECORDING_ID_NOT_SET); + if (seriesRecordingId == SERIES_RECORDING_ID_NOT_SET) { + getActivity().finish(); + return; + } + mShowViewScheduleOption = getArguments().getBoolean( + DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION); + mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager(); + mSeriesRecording = TvApplication.getSingletons(context).getDvrDataManager() + .getSeriesRecording(seriesRecordingId); + if (mSeriesRecording == null) { + getActivity().finish(); + return; + } + mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager() + .getAvailableScheduledRecording(mSeriesRecording.getId()).size(); + List<ScheduledRecording> conflictingRecordings = + mDvrScheduleManager.getConflictingSchedules(mSeriesRecording); + mHasConflict = !conflictingRecordings.isEmpty(); + for (ScheduledRecording recording : conflictingRecordings) { + if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) { + ++mInThisSeriesConflictCount; + } else { + ++mOutThisSeriesConflictCount; + } + } + } + + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getString(R.string.dvr_series_recording_dialog_title); + Drawable icon; + if (!mHasConflict) { + icon = getResources().getDrawable(R.drawable.ic_check_circle_white_48dp, null); + } else { + icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); + } + return new GuidanceStylist.Guidance(title, getDescription(), null, icon); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Context context = getContext(); + actions.add(new GuidedAction.Builder(context) + .clickAction(GuidedAction.ACTION_ID_OK) + .build()); + if (mShowViewScheduleOption) { + actions.add(new GuidedAction.Builder(context) + .id(ACTION_VIEW_SCHEDULES) + .title(R.string.dvr_action_view_schedules) + .build()); + } + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + if (action.getId() == ACTION_VIEW_SCHEDULES) { + Intent intent = new Intent(getActivity(), DvrSchedulesActivity.class); + intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity + .TYPE_SERIES_SCHEDULE); + intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, + mSeriesRecording); + startActivity(intent); + } + getActivity().finish(); + } + + private String getDescription() { + if (!mHasConflict) { + return getResources().getQuantityString( + R.plurals.dvr_series_recording_scheduled_no_conflict, mSchedulesAddedCount, + mSchedulesAddedCount, mSeriesRecording.getTitle()); + } else { + // mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means + // mHasConflict is false. So we don't need to check that case. + if (mInThisSeriesConflictCount != 0 && mOutThisSeriesConflictCount != 0) { + return getResources().getQuantityString(R.plurals + .dvr_series_recording_scheduled_this_and_other_series_conflict, + mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), + mInThisSeriesConflictCount + mOutThisSeriesConflictCount); + } else if (mInThisSeriesConflictCount != 0) { + return getResources().getQuantityString(R.plurals + .dvr_series_recording_scheduled_only_this_series_conflict, + mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), + mInThisSeriesConflictCount); + } else { + if (mOutThisSeriesConflictCount == 1) { + return getResources().getQuantityString(R.plurals + .dvr_series_recording_scheduled_only_other_series_one_conflict, + mSchedulesAddedCount, mSchedulesAddedCount, + mSeriesRecording.getTitle()); + } else { + return getResources().getQuantityString(R.plurals + .dvr_series_recording_scheduled_only_other_series_conflict, + mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), + mOutThisSeriesConflictCount); + } + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java index 2af78081..3f7671b3 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java @@ -17,13 +17,14 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; -import com.android.tv.ui.sidepanel.SettingsFragment; /** * Activity to show details view in DVR. @@ -31,22 +32,51 @@ import com.android.tv.ui.sidepanel.SettingsFragment; public class DvrSeriesSettingsActivity extends Activity { /** * Name of series id added to the Intent. + * Type: Long */ public static final String SERIES_RECORDING_ID = "series_recording_id"; + /** + * Name of the boolean flag to decide if the series recording with empty schedule and recording + * will be removed. + */ + public static final String REMOVE_EMPTY_SERIES_RECORDING = "remove_empty_series_recording"; + /** + * Name of the boolean flag to decide if the setting fragment should be translucent. + */ + public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent"; + /** + * Name of the channel id list. If the channel list is given, we show the channels + * from the values in channel option. + * Type: Long array + */ + public static final String CHANNEL_ID_LIST = "channel_id_list"; + + /** + * Name of the boolean flag to check if the confirm dialog should show view schedule option. + */ + public static final String SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG = + "show_view_schedule_option_in_dialog"; @Override public void onCreate(Bundle savedInstanceState) { + TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); setContentView(R.layout.activity_dvr_series_settings); long seriesRecordingId = getIntent().getLongExtra(SERIES_RECORDING_ID, -1); SoftPreconditions.checkArgument(seriesRecordingId != -1); if (savedInstanceState == null) { - Bundle args = new Bundle(); - args.putLong(SeriesSettingsFragment.SERIES_RECORDING_ID, seriesRecordingId); SeriesSettingsFragment settingFragment = new SeriesSettingsFragment(); - settingFragment.setArguments(args); + settingFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame); } } -} + + @Override + public void onAttachedToWindow() { + if (!getIntent().getExtras().getBoolean(IS_WINDOW_TRANSLUCENT, true)) { + getWindow().setBackgroundDrawable( + new ColorDrawable(getColor(R.color.common_tv_background))); + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java index c0e21a18..c3867886 100644 --- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java @@ -21,16 +21,22 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; +import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; +import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.ScheduledRecording; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -41,10 +47,33 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrStopRecordingFragment extends DvrGuidedStepFragment { - private static final int ACTION_STOP = 1; + /** + * The action ID for the stop action. + */ + public static final int ACTION_STOP = 1; + /** + * Key for the program. + * Type: {@link com.android.tv.data.Program}. + */ + public static final String KEY_REASON = "DvrStopRecordingFragment.type"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({REASON_USER_STOP, REASON_ON_CONFLICT}) + public @interface ReasonType {} + /** + * The dialog is shown because users want to stop some currently recording program. + */ + public static final int REASON_USER_STOP = 1; + /** + * The dialog is shown because users want to record some program that is conflict to the + * current recording program. + */ + public static final int REASON_ON_CONFLICT = 2; private ScheduledRecording mSchedule; private DvrDataManager mDvrDataManager; + private @ReasonType int mStopReason; + private final ScheduledRecordingListener mScheduledRecordingListener = new ScheduledRecordingListener() { @Override @@ -85,6 +114,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { } mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener); + mStopReason = args.getInt(KEY_REASON); } @Override @@ -99,7 +129,20 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getString(R.string.dvr_stop_recording_dialog_title); - String description = getString(R.string.dvr_stop_recording_dialog_description); + String description; + if (mStopReason == REASON_ON_CONFLICT) { + String programTitle = mSchedule.getProgramTitle(); + if (TextUtils.isEmpty(programTitle)) { + ChannelDataManager channelDataManager = + TvApplication.getSingletons(getActivity()).getChannelDataManager(); + Channel channel = channelDataManager.getChannel(mSchedule.getChannelId()); + programTitle = channel.getDisplayName(); + } + description = getString(R.string.dvr_stop_recording_dialog_description_on_conflict, + mSchedule.getProgramTitle()); + } else { + description = getString(R.string.dvr_stop_recording_dialog_description); + } Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null); return new Guidance(title, description, null, image); } @@ -115,12 +158,4 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { .clickAction(GuidedAction.ACTION_ID_CANCEL) .build()); } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_STOP) { - getDvrManager().stopRecording(mSchedule); - } - dismissDialog(); - } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java index d1cf57a6..5b880bd6 100644 --- a/src/com/android/tv/dvr/ui/DvrCancelAllSeriesRecordingDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingDialogFragment.java @@ -26,16 +26,16 @@ import android.view.ViewGroup; import com.android.tv.R; /** - * A dialog fragment which contains {@link DvrCancelAllSeriesRecordingFragment}. + * A dialog fragment which contains {@link DvrStopSeriesRecordingFragment}. */ -public class DvrCancelAllSeriesRecordingDialogFragment extends DialogFragment { +public class DvrStopSeriesRecordingDialogFragment extends DialogFragment { public static final String DIALOG_TAG = "dialog_tag"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.halfsized_dialog, container, false); - GuidedStepFragment fragment = new DvrCancelAllSeriesRecordingFragment(); + GuidedStepFragment fragment = new DvrStopSeriesRecordingFragment(); fragment.setArguments(getArguments()); GuidedStepFragment.add(getChildFragmentManager(), fragment, R.id.halfsized_dialog_host); return view; diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java new file mode 100644 index 00000000..feaa2357 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui; + +import android.app.Activity; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v17.leanback.widget.GuidanceStylist; +import android.support.v17.leanback.widget.GuidedAction; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.SeriesRecording; + +import java.util.ArrayList; +import java.util.List; + +/** + * A fragment which asks the user to stop series recording. + */ +public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment { + /** + * Key for the series recording to be stopped. + */ + public static final String KEY_SERIES_RECORDING = "key_series_recoridng"; + + private static final int ACTION_STOP_SERIES_RECORDING = 1; + + private SeriesRecording mSeriesRecording; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mSeriesRecording = getArguments().getParcelable(KEY_SERIES_RECORDING); + return super.onCreateView(inflater, container, savedInstanceState); + } + + @NonNull + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getString(R.string.dvr_series_schedules_stop_dialog_title); + String description = getString(R.string.dvr_series_schedules_stop_dialog_description); + Drawable icon = getContext().getDrawable(R.drawable.ic_dvr_delete); + return new GuidanceStylist.Guidance(title, description, null, icon); + } + + @Override + public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_STOP_SERIES_RECORDING) + .title(R.string.dvr_series_schedules_stop_dialog_action_stop) + .build()); + actions.add(new GuidedAction.Builder(activity) + .clickAction(GuidedAction.ACTION_ID_CANCEL) + .build()); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + if (action.getId() == ACTION_STOP_SERIES_RECORDING) { + ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + DvrManager dvrManager = singletons.getDvrManager(); + DvrDataManager dataManager = singletons.getDvrDataManager(); + List<ScheduledRecording> toDelete = new ArrayList<>(); + for (ScheduledRecording r : dataManager.getAvailableScheduledRecordings()) { + if (r.getSeriesRecordingId() == mSeriesRecording.getId()) { + if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + toDelete.add(r); + } else { + dvrManager.stopRecording(r); + } + } + } + if (!toDelete.isEmpty()) { + dvrManager.forceRemoveScheduledRecording(ScheduledRecording.toArray(toDelete)); + } + dvrManager.updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording) + .setState(SeriesRecording.STATE_SERIES_STOPPED).build()); + } + dismissDialog(); + } +} diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java index fcf0925b..d320816e 100644 --- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java @@ -35,6 +35,8 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); + private OnActionClickListener mOnActionClickListener; + private Handler mHandler = new Handler(); private Runnable mAutoDismisser = new Runnable() { @Override @@ -63,6 +65,16 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { } @Override + public void onPause() { + super.onPause(); + if (mOnActionClickListener != null) { + // Dismisses the dialog to prevent the callback being forgotten during + // fragment re-creating. + dismiss(); + } + } + + @Override public void onStop() { super.onStop(); mHandler.removeCallbacks(mAutoDismisser); @@ -77,4 +89,29 @@ public class HalfSizedDialogFragment extends SafeDismissDialogFragment { public String getTrackerLabel() { return TRACKER_LABEL; } + + /** + * Sets {@link OnActionClickListener} for the dialog fragment. If listener is set, the dialog + * will be automatically closed when it's paused to prevent the fragment being re-created by + * the framework, which will result the listener being forgotten. + */ + public void setOnActionClickListener(OnActionClickListener listener) { + mOnActionClickListener = listener; + } + + /** + * Returns {@link OnActionClickListener} for sub-classes or any inner fragments. + */ + protected OnActionClickListener getOnActionClickListener() { + return mOnActionClickListener; + } + + /** + * An interface to provide callbacks for half-sized dialogs. Subclasses or inner fragments + * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier + * of the action user clicked. + */ + public interface OnActionClickListener { + void onActionClick(long actionId); + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java index 9f78985f..158bd824 100644 --- a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java +++ b/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java @@ -52,7 +52,6 @@ public class PrioritySettingsFragment extends GuidedStepFragment { // button action's IDs are negative. private static final long ACTION_ID_SAVE = -100L; - private DvrDataManager mDvrDataManager; private final List<SeriesRecording> mSeriesRecordings = new ArrayList<>(); private SeriesRecording mSelectedRecording; @@ -64,23 +63,23 @@ public class PrioritySettingsFragment extends GuidedStepFragment { @Override public void onAttach(Context context) { super.onAttach(context); - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); mSeriesRecordings.clear(); mSeriesRecordings.add(new SeriesRecording.Builder() .setTitle(getString(R.string.dvr_priority_action_one_time_recording)) .setPriority(Long.MAX_VALUE) .setId(ONE_TIME_RECORDING_ID) .build()); + DvrDataManager dvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); long comeFromSeriesRecordingId = getArguments().getLong(COME_FROM_SERIES_RECORDING_ID, -1); - for (SeriesRecording series : mDvrDataManager.getSeriesRecordings()) { + for (SeriesRecording series : dvrDataManager.getSeriesRecordings()) { if (series.getState() == SeriesRecording.STATE_SERIES_NORMAL || series.getId() == comeFromSeriesRecordingId) { mSeriesRecordings.add(series); } } mSeriesRecordings.sort(SeriesRecording.PRIORITY_COMPARATOR); - mComeFromSeriesRecording = mDvrDataManager.getSeriesRecording(comeFromSeriesRecordingId); + mComeFromSeriesRecording = dvrDataManager.getSeriesRecording(comeFromSeriesRecordingId); mSelectedActionElevation = getResources().getDimension(R.dimen.card_elevation_normal); mActionColor = getResources().getColor(R.color.dvr_guided_step_action_text_color, null); mSelectedActionColor = diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java index 9eb7e385..e698b8a2 100644 --- a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java @@ -16,11 +16,8 @@ package com.android.tv.dvr.ui; -import android.content.Intent; import android.content.res.Resources; -import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; -import android.net.Uri; import android.os.Bundle; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.OnActionClickedListener; @@ -30,40 +27,38 @@ import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; -import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrPlaybackActivity; -import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.RecordedProgram; -import com.android.tv.parental.ParentalControlSettings; -import com.android.tv.util.TvInputManagerHelper; -import com.android.tv.util.Utils; - -import java.io.File; /** * {@link DetailsFragment} for recorded program in DVR. */ -public class RecordedProgramDetailsFragment extends DvrDetailsFragment { +public class RecordedProgramDetailsFragment extends DvrDetailsFragment + implements DvrDataManager.RecordedProgramListener { private static final int ACTION_RESUME_PLAYING = 1; private static final int ACTION_PLAY_FROM_BEGINNING = 2; private static final int ACTION_DELETE_RECORDING = 3; private DvrWatchedPositionManager mDvrWatchedPositionManager; - private TvInputManagerHelper mTvInputManagerHelper; private RecordedProgram mRecordedProgram; private DetailsContent mDetailsContent; private boolean mPaused; + private DvrDataManager mDvrDataManager; @Override public void onCreate(Bundle savedInstanceState) { + mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager(); + mDvrDataManager.addRecordedProgramListener(this); super.onCreate(savedInstanceState); + } + + @Override + public void onCreateInternal() { mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) .getDvrWatchedPositionManager(); - mTvInputManagerHelper = TvApplication.getSingletons(getActivity()) - .getTvInputManagerHelper(); setDetailsOverviewRow(mDetailsContent); } @@ -83,10 +78,15 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment { } @Override + public void onDestroy() { + mDvrDataManager.removeRecordedProgramListener(this); + super.onDestroy(); + } + + @Override protected boolean onLoadRecordingDetails(Bundle args) { long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mRecordedProgram = TvApplication.getSingletons(getActivity()).getDvrDataManager() - .getRecordedProgram(recordedProgramId); + mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId); if (mRecordedProgram == null) { // notify super class to end activity before initializing anything return false; @@ -114,8 +114,8 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment { SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); Resources res = getResources(); - if (mDvrWatchedPositionManager.getWatchedPosition(mRecordedProgram.getId()) - != TvInputManager.TIME_SHIFT_INVALID_TIME) { + if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram) + == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { adapter.set(ACTION_RESUME_PLAYING, new Action(ACTION_RESUME_PLAYING, res.getString(R.string.dvr_detail_resume_play), null, res.getDrawable(R.drawable.lb_ic_play))); @@ -139,9 +139,9 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment { @Override public void onActionClicked(Action action) { if (action.getId() == ACTION_PLAY_FROM_BEGINNING) { - startPlayback(TvInputManager.TIME_SHIFT_INVALID_TIME); + startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME); } else if (action.getId() == ACTION_RESUME_PLAYING) { - startPlayback(mDvrWatchedPositionManager + startPlayback(mRecordedProgram, mDvrWatchedPositionManager .getWatchedPosition(mRecordedProgram.getId())); } else if (action.getId() == ACTION_DELETE_RECORDING) { DvrManager dvrManager = TvApplication @@ -153,66 +153,18 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment { }; } - private boolean isDataUriAccessible(Uri dataUri) { - if (dataUri == null || dataUri.getPath() == null) { - return false; - } - try { - File recordedProgramPath = new File(dataUri.getPath()); - if (recordedProgramPath.exists()) { - return true; - } - } catch (SecurityException e) { - } - return false; - } + @Override + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { } - private void startPlayback(long seekTimeMs) { - if (Utils.isInBundledPackageSet(mRecordedProgram.getPackageName()) - && !isDataUriAccessible(mRecordedProgram.getDataUri())) { - // Currently missing storage is handled only for TunerTvInput. - DvrUiHelper.showDvrMissingStorageErrorDialog(getActivity(), - mRecordedProgram.getInputId()); - return; - } - ParentalControlSettings parental = mTvInputManagerHelper.getParentalControlSettings(); - if (!parental.isParentalControlsEnabled()) { - launchPlaybackActivity(seekTimeMs, false); - return; - } - String ratingString = mRecordedProgram.getContentRating(); - if (TextUtils.isEmpty(ratingString)) { - launchPlaybackActivity(seekTimeMs, false); - return; - } - String[] ratingList = ratingString.split(","); - TvContentRating[] programRatings = new TvContentRating[ratingList.length]; - for (int i = 0; i < ratingList.length; i++) { - programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); - } - TvContentRating blockRatings = parental.getBlockedRating(programRatings); - if (blockRatings != null) { - new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, - new PinDialogFragment.ResultListener() { - @Override - public void done(boolean success) { - if (success) { - launchPlaybackActivity(seekTimeMs, true); - } - } - }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); - } else { - launchPlaybackActivity(seekTimeMs, false); - } - } + @Override + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { } - private void launchPlaybackActivity(long seekTimeMs, boolean pinChecked) { - Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); - if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); + @Override + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (recordedProgram.getId() == mRecordedProgram.getId()) { + getActivity().finish(); + } } - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); - getActivity().startActivity(intent); } } diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java index 704d3a3f..1bf34310 100644 --- a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java +++ b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java @@ -21,13 +21,11 @@ import android.content.Context; import android.media.tv.TvContract; import android.media.tv.TvInputManager; import android.net.Uri; -import android.support.v17.leanback.widget.Presenter; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import com.android.tv.R; @@ -35,7 +33,6 @@ import com.android.tv.TvApplication; import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; import com.android.tv.util.Utils; @@ -45,7 +42,7 @@ import java.util.concurrent.TimeUnit; /** * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. */ -public class RecordedProgramPresenter extends Presenter implements OnClickListener { +public class RecordedProgramPresenter extends DvrItemPresenter { private final ChannelDataManager mChannelDataManager; private final DvrWatchedPositionManager mDvrWatchedPositionManager; private final Context mContext; @@ -108,20 +105,16 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen public void onBindViewHolder(ViewHolder viewHolder, Object o) { final RecordedProgram program = (RecordedProgram) o; final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - cardView.setTag(program); Channel channel = mChannelDataManager.getChannel(program.getChannelId()); - SpannableString title; - if (mShowEpisodeTitle) { - title = new SpannableString(program.getEpisodeDisplayTitle(mContext)); - } else { - String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(mContext); - title = titleWithEpisodeNumber == null ? null - : new SpannableString(titleWithEpisodeNumber); - } + String titleString = mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext) + : program.getTitleWithEpisodeNumber(mContext); + SpannableString title = titleString == null ? null : new SpannableString(titleString); if (TextUtils.isEmpty(title)) { title = new SpannableString(channel != null ? channel.getDisplayName() : mContext.getResources().getString(R.string.no_program_information)); } else if (!mShowEpisodeTitle) { + // TODO: Some translation may add delimiters in-between program titles, we should use + // a more robust way to get the span range. String programTitle = program.getTitle(); title.setSpan(new TextAppearanceSpan(mContext, R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 @@ -144,7 +137,6 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen String durationString = getContext().getResources().getQuantityString( R.plurals.dvr_program_duration, durationMinutes, durationMinutes); cardView.setContent(getDescription(program), durationString); - viewHolder.view.setOnClickListener(this); if (viewHolder instanceof RecordedProgramViewHolder) { RecordedProgramViewHolder cardViewHolder = (RecordedProgramViewHolder) viewHolder; cardViewHolder.setProgram(program); @@ -152,6 +144,7 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen cardViewHolder .setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId())); } + super.onBindViewHolder(viewHolder, o); } @Override @@ -161,14 +154,7 @@ public class RecordedProgramPresenter extends Presenter implements OnClickListen ((RecordedProgramViewHolder) viewHolder).mProgram.getId()); } ((RecordingCardView) viewHolder.view).reset(); - } - - @Override - public void onClick(View v) { - if (v instanceof RecordingCardView) { - DvrUiHelper.startDetailsActivity((Activity) mContext, (RecordedProgram) v.getTag(), - ((RecordingCardView) v).getImageView()); - } + super.onUnbindViewHolder(viewHolder); } /** diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/RecordingCardView.java index fa4562fd..51c3b03b 100644 --- a/src/com/android/tv/dvr/ui/RecordingCardView.java +++ b/src/com/android/tv/dvr/ui/RecordingCardView.java @@ -48,6 +48,8 @@ class RecordingCardView extends BaseCardView { private final TextView mMajorContentView; private final TextView mMinorContentView; private final ProgressBar mProgressBar; + private final View mAffiliatedIconContainer; + private final ImageView mAffiliatedIcon; private final Drawable mDefaultImage; RecordingCardView(Context context) { @@ -71,6 +73,8 @@ class RecordingCardView extends BaseCardView { mImageWidth = imageWidth; mImageHeight = imageHeight; mProgressBar = (ProgressBar) findViewById(R.id.recording_progress); + mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container); + mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon); mTitleView = (TextView) findViewById(R.id.title); mMajorContentView = (TextView) findViewById(R.id.content_major); mMinorContentView = (TextView) findViewById(R.id.content_minor); @@ -138,6 +142,15 @@ class RecordingCardView extends BaseCardView { } } + public void setAffiliatedIcon(int imageResId) { + if (imageResId > 0) { + mAffiliatedIconContainer.setVisibility(View.VISIBLE); + mAffiliatedIcon.setImageResource(imageResId); + } else { + mAffiliatedIconContainer.setVisibility(View.INVISIBLE); + } + } + /** * Returns image view. */ diff --git a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java index 2271d932..4e19ec3f 100644 --- a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java @@ -33,13 +33,10 @@ import com.android.tv.dvr.ScheduledRecording; */ abstract class RecordingDetailsFragment extends DvrDetailsFragment { private ScheduledRecording mRecording; - private DetailsContent mDetailsContent; @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mDetailsContent = createDetailsContent(); - setDetailsOverviewRow(mDetailsContent); + protected void onCreateInternal() { + setDetailsOverviewRow(createDetailsContent()); } @Override @@ -47,11 +44,7 @@ abstract class RecordingDetailsFragment extends DvrDetailsFragment { long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager() .getScheduledRecording(scheduledRecordingId); - if (mRecording == null) { - // notify super class to end activity before initializing anything - return false; - } - return true; + return mRecording != null; } /** diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java index 5c1ba48c..60816bb5 100644 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java @@ -89,7 +89,7 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment private int getScheduleIconId() { if (mDvrManager.isConflicting(getRecording())) { - return R.drawable.ic_warning_white_36dp; + return R.drawable.ic_warning_white_32dp; } else { return R.drawable.ic_schedule_32dp; } diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java index 1f67bbe3..5f447f13 100644 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java @@ -20,12 +20,10 @@ import android.app.Activity; import android.content.Context; import android.media.tv.TvContract; import android.os.Handler; -import android.support.v17.leanback.widget.Presenter; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.TextAppearanceSpan; -import android.view.View; import android.view.ViewGroup; import com.android.tv.ApplicationSingletons; @@ -33,7 +31,7 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.util.Utils; @@ -42,10 +40,11 @@ import java.util.concurrent.TimeUnit; /** * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. */ -public class ScheduledRecordingPresenter extends Presenter { +public class ScheduledRecordingPresenter extends DvrItemPresenter { private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); private final ChannelDataManager mChannelDataManager; + private final DvrManager mDvrManager; private final Context mContext; private final int mProgressBarColor; @@ -95,6 +94,7 @@ public class ScheduledRecordingPresenter extends Presenter { public ScheduledRecordingPresenter(Context context) { ApplicationSingletons singletons = TvApplication.getSingletons(context); mChannelDataManager = singletons.getChannelDataManager(); + mDvrManager = singletons.getDvrManager(); mContext = context; mProgressBarColor = context.getResources() .getColor(R.color.play_controls_recording_icon_color_on_focus); @@ -129,19 +129,15 @@ public class ScheduledRecordingPresenter extends Presenter { cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(), recording.getStartTimeMs(), false, true, false, 0), null); } + if (mDvrManager.isConflicting(recording)) { + cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); + } else { + cardView.setAffiliatedIcon(0); + } viewHolder.updateProgressBar(); - View.OnClickListener clickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - if (v instanceof RecordingCardView) { - DvrUiHelper.startDetailsActivity((Activity) v.getContext(), recording, - ((RecordingCardView) v).getImageView(), false); - } - } - }; - baseHolder.view.setOnClickListener(clickListener); viewHolder.mScheduledRecording = recording; viewHolder.startUpdateProgressBar(); + super.onBindViewHolder(viewHolder, o); } @Override @@ -151,6 +147,7 @@ public class ScheduledRecordingPresenter extends Presenter { final RecordingCardView cardView = (RecordingCardView) viewHolder.view; viewHolder.mScheduledRecording = null; cardView.reset(); + super.onUnbindViewHolder(viewHolder); } private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) { diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java index c29d62ae..36e3cfc1 100644 --- a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java +++ b/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java @@ -37,6 +37,7 @@ import com.android.tv.dvr.RecordedProgram; import com.android.tv.dvr.SeriesRecording; import com.android.tv.ui.GuidedActionsStylistWithDivider; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -145,17 +146,19 @@ public class SeriesDeletionFragment extends GuidedStepFragment { public void onGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == ACTION_ID_DELETE) { - int deletionCount = 0; - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + List<Long> idsToDelete = new ArrayList<>(); for (GuidedAction guidedAction : getActions()) { if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID && guidedAction.isChecked()) { - dvrManager.removeRecordedProgram(guidedAction.getId()); - deletionCount++; + idsToDelete.add(guidedAction.getId()); } } + if (!idsToDelete.isEmpty()) { + DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + dvrManager.removeRecordedPrograms(idsToDelete); + } Toast.makeText(getContext(), getResources().getQuantityString( - R.plurals.dvr_msg_episodes_deleted, deletionCount, deletionCount, + R.plurals.dvr_msg_episodes_deleted, idsToDelete.size(), idsToDelete.size(), mRecordings.size()), Toast.LENGTH_LONG).show(); finishGuidedStepFragments(); } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java index 0156e9d9..e9e391d4 100644 --- a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java @@ -17,6 +17,8 @@ package com.android.tv.dvr.ui; import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.media.tv.TvInputManager; import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; import android.support.v17.leanback.widget.Action; @@ -37,8 +39,11 @@ import com.android.tv.TvApplication; import com.android.tv.data.BaseProgram; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.SeriesRecording; import java.util.Collections; @@ -50,24 +55,44 @@ import java.util.List; */ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implements DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener { - private static final int ACTION_SERIES_SCHEDULES = 1; - private static final int ACTION_DELETE = 2; + private static final int ACTION_WATCH = 1; + private static final int ACTION_SERIES_SCHEDULES = 2; + private static final int ACTION_DELETE = 3; + private DvrWatchedPositionManager mDvrWatchedPositionManager; private DvrDataManager mDvrDataManager; private SeriesRecording mSeries; // NOTICE: mRecordedPrograms should only be used in creating details fragments. // After fragments are created, it should be cleared to save resources. private List<RecordedProgram> mRecordedPrograms; + private RecordedProgram mRecommendRecordedProgram; private DetailsContent mDetailsContent; private int mSeasonRowCount; private SparseArrayObjectAdapter mActionsAdapter; private Action mDeleteAction; + private boolean mPaused; + private long mInitialPlaybackPositionMs; + private String mWatchLabel; + private String mResumeLabel; + private Drawable mWatchDrawable; + private RecordedProgramPresenter mRecordedProgramPresenter; + @Override public void onCreate(Bundle savedInstanceState) { mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); + mWatchLabel = getString(R.string.dvr_detail_watch); + mResumeLabel = getString(R.string.dvr_detail_series_resume); + mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); + mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true); super.onCreate(savedInstanceState); + } + + @Override + protected void onCreateInternal() { + mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) + .getDvrWatchedPositionManager(); setDetailsOverviewRow(mDetailsContent); setupRecordedProgramsRow(); mDvrDataManager.addSeriesRecordingListener(this); @@ -76,6 +101,45 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement } @Override + public void onResume() { + super.onResume(); + if (mPaused) { + updateWatchAction(); + mPaused = false; + } + } + + @Override + public void onPause() { + super.onPause(); + mPaused = true; + } + + private void updateWatchAction() { + List<RecordedProgram> programs = mDvrDataManager.getRecordedPrograms(mSeries.getId()); + Collections.sort(programs, RecordedProgram.EPISODE_COMPARATOR); + mRecommendRecordedProgram = getRecommendProgram(programs); + if (mRecommendRecordedProgram == null) { + mActionsAdapter.clear(ACTION_WATCH); + } else { + String episodeStatus; + if(mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram) + == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + episodeStatus = mResumeLabel; + mInitialPlaybackPositionMs = mDvrWatchedPositionManager + .getWatchedPosition(mRecommendRecordedProgram.getId()); + } else { + episodeStatus = mWatchLabel; + mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; + } + String episodeDisplayNumber = mRecommendRecordedProgram.getEpisodeDisplayNumber( + getContext()); + mActionsAdapter.set(ACTION_WATCH, new Action(ACTION_WATCH, + episodeStatus, episodeDisplayNumber, mWatchDrawable)); + } + } + + @Override protected boolean onLoadRecordingDetails(Bundle args) { long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID); mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager() @@ -114,6 +178,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement protected SparseArrayObjectAdapter onCreateActionsAdapter() { mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); Resources res = getResources(); + updateWatchAction(); mActionsAdapter.set(ACTION_SERIES_SCHEDULES, new Action(ACTION_SERIES_SCHEDULES, getString(R.string.dvr_detail_view_schedule), null, res.getDrawable(R.drawable.ic_schedule_32dp, null))); @@ -137,11 +202,13 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement super.onDestroy(); mDvrDataManager.removeSeriesRecordingListener(this); mDvrDataManager.removeRecordedProgramListener(this); - if (mSeries.getState() == SeriesRecording.STATE_SERIES_CANCELED - && mDvrDataManager.getRecordedPrograms(mSeries.getId()).isEmpty()) { - TvApplication.getSingletons(getActivity()).getDvrManager() - .removeSeriesRecording(mSeries.getId()); + if (mSeries != null) { + DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) { + dvrManager.removeSeriesRecording(mSeries.getId()); + } } + mRecordedProgramPresenter.unbindAllViewHolders(); } @Override @@ -149,7 +216,9 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement return new OnActionClickedListener() { @Override public void onActionClicked(Action action) { - if (action.getId() == ACTION_SERIES_SCHEDULES) { + if (action.getId() == ACTION_WATCH) { + startPlayback(mRecommendRecordedProgram, mInitialPlaybackPositionMs); + } else if (action.getId() == ACTION_SERIES_SCHEDULES) { DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries); } else if (action.getId() == ACTION_DELETE) { DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId()); @@ -158,6 +227,28 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement }; } + /** + * The programs are sorted by season number and episode number. + */ + private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) { + for (int i = programs.size() - 1 ; i >= 0 ; i--) { + RecordedProgram program = programs.get(i); + int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program); + if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) { + continue; + } + if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) { + return program; + } + if (i == programs.size() - 1) { + return program; + } else { + return programs.get(i + 1); + } + } + return programs.isEmpty() ? null : programs.get(0); + } + @Override public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } @@ -166,43 +257,57 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement for (SeriesRecording series : seriesRecordings) { if (mSeries.getId() == series.getId()) { mSeries = series; - // TODO: change action label. } } } @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { } + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + for (SeriesRecording series : seriesRecordings) { + if (series.getId() == mSeries.getId()) { + mSeries = null; + getActivity().finish(); + return; + } + } + } @Override - public void onRecordedProgramAdded(RecordedProgram recordedProgram) { - if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { - addProgram(recordedProgram); - if (mActionsAdapter.lookup(ACTION_DELETE) == null) { - mActionsAdapter.set(ACTION_DELETE, mDeleteAction); + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { + addProgram(recordedProgram); + if (mActionsAdapter.lookup(ACTION_DELETE) == null) { + mActionsAdapter.set(ACTION_DELETE, mDeleteAction); + } } } } @Override - public void onRecordedProgramChanged(RecordedProgram recordedProgram) { + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { // Do nothing } @Override - public void onRecordedProgramRemoved(RecordedProgram recordedProgram) { - if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { - ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false); - if (row != null) { - SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter(); - adapter.remove(recordedProgram); - if (adapter.isEmpty()) { - getRowsAdapter().remove(row); - if (getRowsAdapter().size() == 1) { - // No season rows left. Only DetailsOverviewRow - mActionsAdapter.clear(ACTION_DELETE); + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) { + ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false); + if (row != null) { + SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter(); + adapter.remove(recordedProgram); + if (adapter.isEmpty()) { + getRowsAdapter().remove(row); + if (getRowsAdapter().size() == 1) { + // No season rows left. Only DetailsOverviewRow + mActionsAdapter.clear(ACTION_DELETE); + } } } + if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) { + updateWatchAction(); + } } } } @@ -224,7 +329,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement for (int i = rowsAdaptor.size() - 1; i >= 0; i--) { Object row = rowsAdaptor.get(i); if (row instanceof ListRow) { - int compareResult = RecordedProgram.numberCompare(seasonNumber, + int compareResult = BaseProgram.numberCompare(seasonNumber, ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber); if (compareResult == 0) { return (ListRow) row; @@ -241,8 +346,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement : getString(R.string.dvr_detail_series_season_title, seasonNumber); HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle); ClassPresenterSelector selector = new ClassPresenterSelector(); - selector.addClassPresenter(RecordedProgram.class, - new RecordedProgramPresenter(getContext(), true)); + selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter); ListRow row = new ListRow(header, new SeasonRowAdapter(selector, new Comparator<RecordedProgram>() { @Override diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java index d2f26dd1..c2c0f596 100644 --- a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java @@ -18,24 +18,20 @@ package com.android.tv.dvr.ui; import android.app.Activity; import android.content.Context; -import android.content.Intent; import android.media.tv.TvContract; import android.media.tv.TvInputManager; -import android.os.Bundle; -import android.support.v17.leanback.widget.Presenter; -import android.support.v4.app.ActivityOptionsCompat; import android.text.TextUtils; -import android.view.View; import android.view.ViewGroup; -import com.android.tv.R; import com.android.tv.ApplicationSingletons; +import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; +import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; import com.android.tv.dvr.RecordedProgram; @@ -45,11 +41,12 @@ import com.android.tv.dvr.SeriesRecording; import java.util.List; /** - * Presents a {@link SeriesRecording} in the {@link DvrBrowseFragment}. + * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. */ -public class SeriesRecordingPresenter extends Presenter { +public class SeriesRecordingPresenter extends DvrItemPresenter { private final ChannelDataManager mChannelDataManager; private final DvrDataManager mDvrDataManager; + private final DvrManager mDvrManager; private final DvrWatchedPositionManager mWatchedPositionManager; private static final class SeriesRecordingViewHolder extends ViewHolder implements @@ -57,13 +54,15 @@ public class SeriesRecordingPresenter extends Presenter { private SeriesRecording mSeriesRecording; private RecordingCardView mCardView; private DvrDataManager mDvrDataManager; + private DvrManager mDvrManager; private DvrWatchedPositionManager mWatchedPositionManager; SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager, - DvrWatchedPositionManager watchedPositionManager) { + DvrManager dvrManager, DvrWatchedPositionManager watchedPositionManager) { super(view); mCardView = view; mDvrDataManager = dvrDataManager; + mDvrManager = dvrManager; mWatchedPositionManager = watchedPositionManager; } @@ -96,27 +95,41 @@ public class SeriesRecordingPresenter extends Presenter { } @Override - public void onRecordedProgramAdded(RecordedProgram recordedProgram) { - if (TextUtils.equals(recordedProgram.getTitle(), mSeriesRecording.getTitle())) { - mDvrDataManager.removeScheduledRecordingListener(this); - mWatchedPositionManager.addListener(this, recordedProgram.getId()); + public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { + boolean needToUpdateCardView = false; + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), + mSeriesRecording.getSeriesId())) { + mDvrDataManager.removeScheduledRecordingListener(this); + mWatchedPositionManager.addListener(this, recordedProgram.getId()); + needToUpdateCardView = true; + } + } + if (needToUpdateCardView) { updateCardViewContent(); } } @Override - public void onRecordedProgramRemoved(RecordedProgram recordedProgram) { - if (TextUtils.equals(recordedProgram.getTitle(), mSeriesRecording.getTitle())) { - if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) - == TvInputManager.TIME_SHIFT_INVALID_TIME) { - mWatchedPositionManager.removeListener(this, recordedProgram.getId()); + public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) { + boolean needToUpdateCardView = false; + for (RecordedProgram recordedProgram : recordedPrograms) { + if (TextUtils.equals(recordedProgram.getSeriesId(), + mSeriesRecording.getSeriesId())) { + if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId()) + == TvInputManager.TIME_SHIFT_INVALID_TIME) { + mWatchedPositionManager.removeListener(this, recordedProgram.getId()); + } + needToUpdateCardView = true; } + } + if (needToUpdateCardView) { updateCardViewContent(); } } @Override - public void onRecordedProgramChanged(RecordedProgram recordedProgram) { + public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { // Do nothing } @@ -151,7 +164,7 @@ public class SeriesRecordingPresenter extends Presenter { List<RecordedProgram> recordedPrograms = mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId()); if (recordedPrograms.size() == 0) { - count = mDvrDataManager.getScheduledRecordings(mSeriesRecording.getId()).size(); + count = mDvrManager.getAvailableScheduledRecording(mSeriesRecording.getId()).size(); quantityStringID = R.plurals.dvr_count_scheduled_recordings; } else { for (RecordedProgram recordedProgram : recordedPrograms) { @@ -176,6 +189,7 @@ public class SeriesRecordingPresenter extends Presenter { ApplicationSingletons singletons = TvApplication.getSingletons(context); mChannelDataManager = singletons.getChannelDataManager(); mDvrDataManager = singletons.getDvrDataManager(); + mDvrManager = singletons.getDvrManager(); mWatchedPositionManager = singletons.getDvrWatchedPositionManager(); } @@ -183,7 +197,8 @@ public class SeriesRecordingPresenter extends Presenter { public ViewHolder onCreateViewHolder(ViewGroup parent) { Context context = parent.getContext(); RecordingCardView view = new RecordingCardView(context); - return new SeriesRecordingViewHolder(view, mDvrDataManager, mWatchedPositionManager); + return new SeriesRecordingViewHolder(view, mDvrDataManager, mDvrManager, + mWatchedPositionManager); } @Override @@ -193,33 +208,14 @@ public class SeriesRecordingPresenter extends Presenter { final RecordingCardView cardView = (RecordingCardView) viewHolder.view; viewHolder.onBound(seriesRecording); setTitleAndImage(cardView, seriesRecording); - View.OnClickListener clickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - showSeriesRecordingDetails(v, seriesRecording); - } - }; - baseHolder.view.setOnClickListener(clickListener); - } - - private void showSeriesRecordingDetails(View view, SeriesRecording seriesRecording) { - if (view instanceof RecordingCardView) { - Context context = view.getContext(); - Intent intent = new Intent(context, DvrDetailsActivity.class); - intent.putExtra(DvrDetailsActivity.RECORDING_ID, seriesRecording.getId()); - intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, - DvrDetailsActivity.SERIES_RECORDING_VIEW); - Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) context, - ((RecordingCardView) view).getImageView(), - DvrDetailsActivity.SHARED_ELEMENT_NAME).toBundle(); - context.startActivity(intent, bundle); - } + super.onBindViewHolder(baseHolder, o); } @Override public void onUnbindViewHolder(ViewHolder viewHolder) { ((RecordingCardView) viewHolder.view).reset(); ((SeriesRecordingViewHolder) viewHolder).onUnbound(); + super.onUnbindViewHolder(viewHolder); } private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) { diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java index c550935c..6c05c9c6 100644 --- a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java +++ b/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java @@ -17,45 +17,64 @@ package com.android.tv.dvr.ui; import android.app.FragmentManager; +import android.app.ProgressDialog; import android.content.Context; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.GuidedActionsStylist; +import android.util.Log; +import android.util.LongSparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ProgressBar; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Channel; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.EpisodicProgramLoadTask; import com.android.tv.dvr.SeriesRecording; import com.android.tv.dvr.SeriesRecording.ChannelOption; - +import com.android.tv.dvr.SeriesRecordingScheduler; +import com.android.tv.dvr.SeriesRecordingScheduler.OnSeriesRecordingUpdatedListener; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Fragment for DVR series recording settings. */ public class SeriesSettingsFragment extends GuidedStepFragment implements DvrDataManager.SeriesRecordingListener { - /** - * Name of series recording id added to the bundle. - * Type: Long - */ - public static final String SERIES_RECORDING_ID = "series_recording_id"; + private static final String TAG = "SeriesSettingsFragment"; + private static final boolean DEBUG = false; private static final long ACTION_ID_PRIORITY = 10; private static final long ACTION_ID_CHANNEL = 11; - private static final long SUB_ACTION_ID_CHANNEL_ONE = 101; private static final long SUB_ACTION_ID_CHANNEL_ALL = 102; + // Each channel's action id = SUB_ACTION_ID_CHANNEL_ONE_BASE + channel id + private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; private DvrDataManager mDvrDataManager; + private ChannelDataManager mChannelDataManager; + private DvrManager mDvrManager; private SeriesRecording mSeriesRecording; - private Channel mChannel; private long mSeriesRecordingId; @ChannelOption int mChannelOption; + private Comparator<Channel> mChannelComparator; + private long mSelectedChannelId; + private int mBackStackCount; + private boolean mShowViewScheduleOptionInDialog; private String mFragmentTitle; private String mProrityActionTitle; @@ -63,6 +82,9 @@ public class SeriesSettingsFragment extends GuidedStepFragment private String mProrityActionLowestText; private String mChannelsActionTitle; private String mChannelsActionAllText; + private LongSparseArray<Channel> mId2Channel = new LongSparseArray<>(); + private List<Channel> mChannels = new ArrayList<>(); + private EpisodicProgramLoadTask mEpisodicProgramLoadTask; private GuidedAction mPriorityGuidedAction; private GuidedAction mChannelsGuidedAction; @@ -70,14 +92,49 @@ public class SeriesSettingsFragment extends GuidedStepFragment @Override public void onAttach(Context context) { super.onAttach(context); + mBackStackCount = getFragmentManager().getBackStackEntryCount(); mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mSeriesRecordingId = getArguments().getLong(SERIES_RECORDING_ID); + mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID); mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); + if (mSeriesRecording == null) { + getActivity().finish(); + return; + } + mDvrManager = TvApplication.getSingletons(context).getDvrManager(); + mShowViewScheduleOptionInDialog = getArguments().getBoolean( + DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); mDvrDataManager.addSeriesRecordingListener(this); + long[] channelIds = getArguments().getLongArray(DvrSeriesSettingsActivity.CHANNEL_ID_LIST); + mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); + if (channelIds == null) { + Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); + if (channel != null) { + mId2Channel.put(channel.getId(), channel); + mChannels.add(channel); + } + collectChannelsInBackground(); + } else { + for (long channelId : channelIds) { + Channel channel = mChannelDataManager.getChannel(channelId); + if (channel != null) { + mId2Channel.put(channel.getId(), channel); + mChannels.add(channel); + } + } + } mChannelOption = mSeriesRecording.getChannelOption(); - mChannel = TvApplication.getSingletons(context).getChannelDataManager() - .getChannel(mSeriesRecording.getChannelId()); - // TODO: Handle when channel is null. + mSelectedChannelId = Channel.INVALID_ID; + if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { + Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); + if (channel != null) { + mSelectedChannelId = channel.getId(); + } else { + mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; + } + } + mChannelComparator = new Channel.DefaultComparator(context, + TvApplication.getSingletons(context).getTvInputManagerHelper()); + mChannels.sort(mChannelComparator); mFragmentTitle = getString(R.string.dvr_series_settings_title); mProrityActionTitle = getString(R.string.dvr_series_settings_priority); mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest); @@ -90,6 +147,22 @@ public class SeriesSettingsFragment extends GuidedStepFragment public void onDetach() { super.onDetach(); mDvrDataManager.removeSeriesRecordingListener(this); + if (mEpisodicProgramLoadTask != null) { + mEpisodicProgramLoadTask.cancel(true); + mEpisodicProgramLoadTask = null; + } + } + + @Override + public void onDestroy() { + DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); + if (getFragmentManager().getBackStackEntryCount() == mBackStackCount + && getArguments() + .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING) + && dvrManager.canRemoveSeriesRecording(mSeriesRecordingId)) { + dvrManager.removeSeriesRecording(mSeriesRecordingId); + } + super.onDestroy(); } @Override @@ -108,19 +181,10 @@ public class SeriesSettingsFragment extends GuidedStepFragment updatePriorityGuidedAction(false); actions.add(mPriorityGuidedAction); - List<GuidedAction> channelSubActions = new ArrayList<GuidedAction>(); - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ONE) - .title(mChannel.getDisplayText()) - .build()); - channelSubActions.add(new GuidedAction.Builder(getActivity()) - .id(SUB_ACTION_ID_CHANNEL_ALL) - .title(mChannelsActionAllText) - .build()); mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) .id(ACTION_ID_CHANNEL) .title(mChannelsActionTitle) - .subActions(channelSubActions) + .subActions(buildChannelSubAction()) .build(); actions.add(mChannelsGuidedAction); updateChannelsGuidedAction(false); @@ -140,13 +204,45 @@ public class SeriesSettingsFragment extends GuidedStepFragment public void onGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == GuidedAction.ACTION_ID_OK) { - if (mChannelOption != mSeriesRecording.getChannelOption()) { + if (mEpisodicProgramLoadTask != null) { + mEpisodicProgramLoadTask.cancel(true); + mEpisodicProgramLoadTask = null; + } + if (mChannelOption != mSeriesRecording.getChannelOption() + || mSeriesRecording.isStopped() + || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE + && mSeriesRecording.getChannelId() != mSelectedChannelId)) { + SeriesRecording.Builder builder = SeriesRecording.buildFrom(mSeriesRecording) + .setChannelOption(mChannelOption) + .setState(SeriesRecording.STATE_SERIES_NORMAL); + if (mSelectedChannelId != Channel.INVALID_ID) { + builder.setChannelId(mSelectedChannelId); + } TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(SeriesRecording.buildFrom(mSeriesRecording) - .setChannelOption(mChannelOption) - .build()); + .updateSeriesRecording(builder.build()); + SeriesRecordingScheduler scheduler = + SeriesRecordingScheduler.getInstance(getContext()); + // Since dialog is used even after the fragment is closed, we should + // use application context. + ProgressDialog dialog = ProgressDialog.show(getContext(), null, getString( + R.string.dvr_series_schedules_progress_message_updating_programs)); + scheduler.addOnSeriesRecordingUpdatedListener( + new OnSeriesRecordingUpdatedListener() { + @Override + public void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings) { + for (SeriesRecording seriesRecording : seriesRecordings) { + if (seriesRecording.getId() == mSeriesRecordingId) { + dialog.dismiss(); + scheduler.removeOnSeriesRecordingUpdatedListener(this); + showConfirmDialog(); + return; + } + } + } + }); + } else { + showConfirmDialog(); } - finishGuidedStepFragments(); } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { finishGuidedStepFragments(); } else if (actionId == ACTION_ID_PRIORITY) { @@ -165,10 +261,12 @@ public class SeriesSettingsFragment extends GuidedStepFragment long actionId = action.getId(); if (actionId == SUB_ACTION_ID_CHANNEL_ALL) { mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; + mSelectedChannelId = Channel.INVALID_ID; updateChannelsGuidedAction(true); return true; - } else if (actionId == SUB_ACTION_ID_CHANNEL_ONE) { + } else if (actionId > SUB_ACTION_ID_CHANNEL_ONE_BASE) { mChannelOption = SeriesRecording.OPTION_CHANNEL_ONE; + mSelectedChannelId = actionId - SUB_ACTION_ID_CHANNEL_ONE_BASE; updateChannelsGuidedAction(true); return true; } @@ -184,7 +282,8 @@ public class SeriesSettingsFragment extends GuidedStepFragment if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { mChannelsGuidedAction.setDescription(mChannelsActionAllText); } else { - mChannelsGuidedAction.setDescription(mChannel.getDisplayText()); + mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) + .getDisplayText()); } if (notifyActionChanged) { notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); @@ -210,13 +309,75 @@ public class SeriesSettingsFragment extends GuidedStepFragment } else if (priorityOrder >= totalSeriesCount - 1) { mPriorityGuidedAction.setDescription(mProrityActionLowestText); } else { - mPriorityGuidedAction.setDescription(Integer.toString(priorityOrder + 1)); + mPriorityGuidedAction.setDescription(getString( + R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); } if (notifyActionChanged) { notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); } } + private void collectChannelsInBackground() { + if (mEpisodicProgramLoadTask != null) { + mEpisodicProgramLoadTask.cancel(true); + } + mEpisodicProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { + @Override + protected void onPostExecute(List<Program> programs) { + mEpisodicProgramLoadTask = null; + Set<Long> channelIds = new HashSet<>(); + for (Program program : programs) { + channelIds.add(program.getChannelId()); + } + boolean channelAdded = false; + for (Long channelId : channelIds) { + if (mId2Channel.get(channelId) != null) { + continue; + } + Channel channel = mChannelDataManager.getChannel(channelId); + if (channel != null) { + channelAdded = true; + mId2Channel.put(channelId, channel); + mChannels.add(channel); + if (DEBUG) Log.d(TAG, "Added channel: " + channel); + } + } + if (!channelAdded) { + return; + } + mChannels.sort(mChannelComparator); + mChannelsGuidedAction.setSubActions(buildChannelSubAction()); + notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); + if (DEBUG) Log.d(TAG, "Complete EpisodicProgramLoadTask"); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true); + mEpisodicProgramLoadTask.execute(); + } + + private List<GuidedAction> buildChannelSubAction() { + List<GuidedAction> channelSubActions = new ArrayList<>(); + channelSubActions.add(new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ALL) + .title(mChannelsActionAllText) + .build()); + for (Channel channel : mChannels) { + channelSubActions.add(new GuidedAction.Builder(getActivity()) + .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId()) + .title(channel.getDisplayText()) + .build()); + } + return channelSubActions; + } + + private void showConfirmDialog() { + DvrUiHelper.StartSeriesScheduledDialogActivity( + getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog); + finishGuidedStepFragments(); + } + @Override public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java index 3a57d72e..393a5ff3 100644 --- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java +++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java @@ -148,7 +148,10 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { return -1; } - private int findInsertPosition(T item) { + /** + * Finds the position that the given item should be inserted to keep the sorted order. + */ + public int findInsertPosition(T item) { for (int i = size() - mExtraItemCount - 1; i >=0; i--) { T r = (T) get(i); if (mComparator.compare(r, item) <= 0) { diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java index 61de5764..d28f026c 100644 --- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java @@ -22,12 +22,13 @@ import android.support.v17.leanback.widget.ClassPresenterSelector; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.widget.TextView; +import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.ScheduledRecording; /** @@ -35,32 +36,39 @@ import com.android.tv.dvr.ScheduledRecording; */ public abstract class BaseDvrSchedulesFragment extends DetailsFragment implements DvrDataManager.ScheduledRecordingListener, - SchedulesHeaderRowPresenter.SchedulesHeaderRowListener, - ScheduleRowPresenter.ScheduleRowClickListener { + DvrScheduleManager.OnConflictStateChangeListener { /** * The key for scheduled recording which has be selected in the list. */ public static String SCHEDULES_KEY_SCHEDULED_RECORDING = "schedules_key_scheduled_recording"; - private SchedulesHeaderRowPresenter mHeaderRowPresenter; - private ScheduleRowPresenter mRowPresenter; private ScheduleRowAdapter mRowsAdapter; + private TextView mEmptyInfoScreenView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); - mHeaderRowPresenter = onCreateHeaderRowPresenter(); - mHeaderRowPresenter.addListener(this); - mRowPresenter = onCreateRowPresenter(); - mRowPresenter.addListener(this); - presenterSelector.addClassPresenter(SchedulesHeaderRow.class, mHeaderRowPresenter); - presenterSelector.addClassPresenter(ScheduleRow.class, mRowPresenter); + presenterSelector.addClassPresenter(SchedulesHeaderRow.class, onCreateHeaderRowPresenter()); + presenterSelector.addClassPresenter(ScheduleRow.class, onCreateRowPresenter()); mRowsAdapter = onCreateRowsAdapter(presenterSelector); setAdapter(mRowsAdapter); mRowsAdapter.start(); - TvApplication.getSingletons(getContext()).getDvrDataManager() - .addScheduledRecordingListener(this); + ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + singletons.getDvrDataManager().addScheduledRecordingListener(this); + singletons.getDvrScheduleManager().addOnConflictStateChangeListener(this); + mEmptyInfoScreenView = (TextView) getActivity().findViewById(R.id.empty_info_screen); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = super.onCreateView(inflater, container, savedInstanceState); + int firstItemPosition = getFirstItemPosition(); + if (firstItemPosition != -1) { + getRowsFragment().setSelectedPosition(firstItemPosition, false); + } + return view; } /** @@ -73,34 +81,20 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment /** * Shows the empty message. */ - protected void showEmptyMessage(int message) { - TextView emptyInfoScreenView = (TextView) getActivity().findViewById( - R.id.empty_info_screen); - emptyInfoScreenView.setText(message); - emptyInfoScreenView.setVisibility(View.VISIBLE); + void showEmptyMessage(int messageId) { + mEmptyInfoScreenView.setText(messageId); + if (mEmptyInfoScreenView.getVisibility() != View.VISIBLE) { + mEmptyInfoScreenView.setVisibility(View.VISIBLE); + } } - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = super.onCreateView(inflater, container, savedInstanceState); - // setSelectedPosition works only after the view is attached to a window. - view.getViewTreeObserver().addOnWindowAttachListener( - new ViewTreeObserver.OnWindowAttachListener() { - @Override - public void onWindowAttached() { - int firstItemPosition = getFirstItemPosition(); - if (firstItemPosition != -1) { - setSelectedPosition(firstItemPosition, false); - } - view.getViewTreeObserver().removeOnWindowAttachListener(this); - } - - @Override - public void onWindowDetached() { - } - }); - return view; + /** + * Hides the empty message. + */ + void hideEmptyMessage() { + if (mEmptyInfoScreenView.getVisibility() == View.VISIBLE) { + mEmptyInfoScreenView.setVisibility(View.GONE); + } } @Override @@ -112,10 +106,9 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment @Override public void onDestroy() { - TvApplication.getSingletons(getContext()).getDvrDataManager() - .removeScheduledRecordingListener(this); - mHeaderRowPresenter.removeListener(this); - mRowPresenter.removeListener(this); + ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + singletons.getDvrScheduleManager().removeOnConflictStateChangeListener(this); + singletons.getDvrDataManager().removeScheduledRecordingListener(this); mRowsAdapter.stop(); super.onDestroy(); } @@ -139,16 +132,6 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment * Gets the first focus position in schedules list. */ protected int getFirstItemPosition() { - Bundle args = getArguments(); - ScheduledRecording recording = null; - if (args != null) { - recording = args.getParcelable(SCHEDULES_KEY_SCHEDULED_RECORDING); - } - final int selectedPostion = mRowsAdapter.indexOf( - mRowsAdapter.findRowByScheduledRecording(recording)); - if (selectedPostion != -1) { - return selectedPostion; - } for (int i = 0; i < mRowsAdapter.size(); i++) { if (mRowsAdapter.get(i) instanceof ScheduleRow) { return i; @@ -159,11 +142,8 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment @Override public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording recording : scheduledRecordings) { - if (mRowPresenter != null) { - mRowPresenter.onScheduledRecordingAdded(recording); - } - if (mRowsAdapter != null) { + if (mRowsAdapter != null) { + for (ScheduledRecording recording : scheduledRecordings) { mRowsAdapter.onScheduledRecordingAdded(recording); } } @@ -171,11 +151,8 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment @Override public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording recording : scheduledRecordings) { - if (mRowPresenter != null) { - mRowPresenter.onScheduledRecordingRemoved(recording); - } - if (mRowsAdapter != null) { + if (mRowsAdapter != null) { + for (ScheduledRecording recording : scheduledRecordings) { mRowsAdapter.onScheduledRecordingRemoved(recording); } } @@ -183,27 +160,19 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment @Override public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { - for (ScheduledRecording recording : scheduledRecordings) { - if (mRowPresenter != null) { - mRowPresenter.onScheduledRecordingUpdated(recording); - } - if (mRowsAdapter != null) { - mRowsAdapter.onScheduledRecordingUpdated(recording); + if (mRowsAdapter != null) { + for (ScheduledRecording recording : scheduledRecordings) { + mRowsAdapter.onScheduledRecordingUpdated(recording, false); } } } @Override - public void onUpdateAllScheduleRows() { - if (getRowsAdapter() != null) { - getRowsAdapter().notifyArrayItemRangeChanged(0, getRowsAdapter().size()); - } - } - - @Override - public void onDeleteClicked(ScheduleRow scheduleRow) { + public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { if (mRowsAdapter != null) { - mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size()); + for (ScheduledRecording recording : schedules) { + mRowsAdapter.onScheduledRecordingUpdated(recording, true); + } } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java index f361ede3..722c9b6e 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java @@ -18,8 +18,12 @@ package com.android.tv.dvr.ui.list; import android.os.Bundle; import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; import com.android.tv.R; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter; /** @@ -48,4 +52,35 @@ public class DvrSchedulesFragment extends BaseDvrSchedulesFragment { public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelecor) { return new ScheduleRowAdapter(getContext(), presenterSelecor); } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { + super.onScheduledRecordingAdded(scheduledRecordings); + if (getRowsAdapter().size() > 0) { + hideEmptyMessage(); + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { + super.onScheduledRecordingRemoved(scheduledRecordings); + if (getRowsAdapter().size() == 0) { + showEmptyMessage(R.string.dvr_schedules_empty_state); + } + } + + @Override + protected int getFirstItemPosition() { + Bundle args = getArguments(); + ScheduledRecording recording = null; + if (args != null) { + recording = args.getParcelable(SCHEDULES_KEY_SCHEDULED_RECORDING); + } + final int selectedPostion = getRowsAdapter().indexOf( + getRowsAdapter().findRowByScheduledRecording(recording)); + if (selectedPostion != -1) { + return selectedPostion; + } + return super.getFirstItemPosition(); + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java index ba8b0c36..42a1e72b 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java @@ -16,71 +16,154 @@ package com.android.tv.dvr.ui.list; +import android.annotation.TargetApi; +import android.database.ContentObserver; +import android.media.tv.TvContract.Programs; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.transition.Fade; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; +import com.android.tv.dvr.EpisodicProgramLoadTask; import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.ui.DvrSchedulesActivity; -import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter; +import java.util.List; + /** * A fragment to show the list of series schedule recordings. */ +@TargetApi(Build.VERSION_CODES.N) public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { + private static final String TAG = "DvrSeriesSchedulesFragment"; /** * The key for series recording whose scheduled recording list will be displayed. */ - public static String SERIES_SCHEDULES_KEY_SERIES_RECORDING = + public static final String SERIES_SCHEDULES_KEY_SERIES_RECORDING = "series_schedules_key_series_recording"; + /** + * The key for programs belong to the series recording whose scheduled recording + * list will be displayed. + */ + public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS = + "series_schedules_key_series_programs"; + + private ChannelDataManager mChannelDataManager; + private SeriesRecording mSeriesRecording; + private List<Program> mPrograms; + private EpisodicProgramLoadTask mProgramLoadTask; + + private final SeriesRecordingListener mSeriesRecordingListener = + new SeriesRecordingListener() { + @Override + public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } + + @Override + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + for (SeriesRecording r : seriesRecordings) { + if (r.getId() == mSeriesRecording.getId()) { + getActivity().finish(); + return; + } + } + } + + @Override + public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { + for (SeriesRecording r : seriesRecordings) { + if (r.getId() == mSeriesRecording.getId() + && getRowsAdapter() instanceof SeriesScheduleRowAdapter) { + ((SeriesScheduleRowAdapter) getRowsAdapter()) + .onSeriesRecordingUpdated(r); + return; + } + } + } + }; - private static String TAG = "DvrSeriesSchedulesFragment"; + private final ContentObserver mContentObserver = + new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + executeProgramLoadingTask(); + } + }; + + private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() { + @Override + public void onLoadFinished() { } - private SeriesRecording mSeries; + @Override + public void onChannelListUpdated() { + executeProgramLoadingTask(); + } + + @Override + public void onChannelBrowsableChanged() { } + }; + + public DvrSeriesSchedulesFragment() { + setEnterTransition(new Fade(Fade.IN)); + } @Override public void onCreate(Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { - mSeries = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING); + mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING); + mPrograms = args.getParcelableArrayList(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); } super.onCreate(savedInstanceState); - // "1" means there is only title row in series schedules list. So we should show an empty - // state info view. - if (getRowsAdapter().size() == 1) { - showEmptyMessage(R.string.dvr_series_schedules_empty_state); - } - ((DvrSchedulesActivity) getActivity()).setCancelAllClickedRunnable(new Runnable() { - @Override - public void run() { - SoftPreconditions.checkState(getRowsAdapter().get(0) instanceof - SeriesRecordingHeaderRow, TAG, "First row is not SchedulesHeaderRow"); - SeriesRecordingHeaderRow headerRow = - (SeriesRecordingHeaderRow) getRowsAdapter().get(0); - headerRow.setCancelAllChecked(true); - if (headerRow.getSeriesRecording() != null) { - TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(SeriesRecording.buildFrom( - headerRow.getSeriesRecording()).setState( - SeriesRecording.STATE_SERIES_CANCELED).build()); - } - onUpdateAllScheduleRows(); - } - }); + ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); + singletons.getDvrDataManager().addSeriesRecordingListener(mSeriesRecordingListener); + mChannelDataManager = singletons.getChannelDataManager(); + mChannelDataManager.addListener(mChannelListener); + getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, + mContentObserver); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + onProgramsUpdated(); return super.onCreateView(inflater, container, savedInstanceState); } + private void onProgramsUpdated() { + ((SeriesScheduleRowAdapter) getRowsAdapter()).setPrograms(mPrograms); + if (mPrograms == null || mPrograms.isEmpty()) { + showEmptyMessage(R.string.dvr_series_schedules_empty_state); + } else { + hideEmptyMessage(); + } + } + + @Override + public void onDestroy() { + if (mProgramLoadTask != null) { + mProgramLoadTask.cancel(true); + mProgramLoadTask = null; + } + getContext().getContentResolver().unregisterContentObserver(mContentObserver); + mChannelDataManager.removeListener(mChannelListener); + TvApplication.getSingletons(getContext()).getDvrDataManager() + .removeSeriesRecordingListener(mSeriesRecordingListener); + super.onDestroy(); + } + @Override public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() { return new SeriesRecordingHeaderRowPresenter(getContext()); @@ -93,20 +176,33 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { @Override public ScheduleRowAdapter onCreateRowsAdapter(ClassPresenterSelector presenterSelector) { - return new SeriesScheduleRowAdapter(getContext(), presenterSelector, mSeries); + return new SeriesScheduleRowAdapter(getContext(), presenterSelector, mSeriesRecording); } @Override protected int getFirstItemPosition() { - if (mSeries != null && mSeries.getState() == SeriesRecording.STATE_SERIES_CANCELED) { - return -1; + if (mSeriesRecording != null + && mSeriesRecording.getState() == SeriesRecording.STATE_SERIES_STOPPED) { + return 0; } return super.getFirstItemPosition(); } - @Override - public void onDestroy() { - ((DvrSchedulesActivity) getActivity()).setCancelAllClickedRunnable(null); - super.onDestroy(); + private void executeProgramLoadingTask() { + if (mProgramLoadTask != null) { + mProgramLoadTask.cancel(true); + } + mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { + @Override + protected void onPostExecute(List<Program> programs) { + mPrograms = programs; + onProgramsUpdated(); + } + }; + mProgramLoadTask.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true) + .execute(); } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java new file mode 100644 index 00000000..23aebf59 --- /dev/null +++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui.list; + +import android.content.Context; + +import com.android.tv.data.Program; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.ScheduledRecording.Builder; + +/** + * A class for the episodic program. + */ +public class EpisodicProgramRow extends ScheduleRow { + private final String mInputId; + private final Program mProgram; + + public EpisodicProgramRow(String inputId, Program program, ScheduledRecording recording, + SchedulesHeaderRow headerRow) { + super(recording, headerRow); + mInputId = inputId; + mProgram = program; + } + + /** + * Returns the program. + */ + public Program getProgram() { + return mProgram; + } + + @Override + public long getChannelId() { + return mProgram.getChannelId(); + } + + @Override + public long getStartTimeMs() { + return mProgram.getStartTimeUtcMillis(); + } + + @Override + public long getEndTimeMs() { + return mProgram.getEndTimeUtcMillis(); + } + + @Override + public Builder createNewScheduleBuilder() { + return ScheduledRecording.builder(mInputId, mProgram); + } + + @Override + public String getProgramTitleWithEpisodeNumber(Context context) { + return mProgram.getTitleWithEpisodeNumber(context); + } + + @Override + public String getEpisodeDisplayTitle(Context context) { + return mProgram.getEpisodeDisplayTitle(context); + } + + @Override + public boolean matchSchedule(ScheduledRecording schedule) { + return schedule.getType() == ScheduledRecording.TYPE_PROGRAM + && mProgram.getId() == schedule.getProgramId(); + } + + @Override + public String toString() { + return super.toString() + + "(inputId=" + mInputId + + ",program=" + mProgram + + ")"; + } +} diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java index 1e258d2d..3fc92e8a 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java @@ -16,54 +16,188 @@ package com.android.tv.dvr.ui.list; +import android.content.Context; +import android.support.annotation.Nullable; + +import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.ScheduledRecording; /** * A class for schedule recording row. */ public class ScheduleRow { - private ScheduledRecording mRecording; - private boolean mRemoveScheduleChecked; - private SchedulesHeaderRow mHeaderRow; + private final SchedulesHeaderRow mHeaderRow; + @Nullable private ScheduledRecording mSchedule; + private boolean mStopRecordingRequested; + private boolean mStartRecordingRequested; - public ScheduleRow(ScheduledRecording recording, SchedulesHeaderRow headerRow) { - mRecording = recording; - mRemoveScheduleChecked = false; + public ScheduleRow(@Nullable ScheduledRecording recording, SchedulesHeaderRow headerRow) { + mSchedule = recording; mHeaderRow = headerRow; } /** - * Sets scheduled recording. + * Gets which {@link SchedulesHeaderRow} this schedule row belongs to. */ - public void setRecording(ScheduledRecording recording) { - mRecording = recording; + public SchedulesHeaderRow getHeaderRow() { + return mHeaderRow; } /** - * Sets remove schedule checked status. + * Returns the recording schedule. */ - public void setRemoveScheduleChecked(boolean checked) { - mRemoveScheduleChecked = checked; + @Nullable + public ScheduledRecording getSchedule() { + return mSchedule; } /** - * Gets scheduled recording. + * Checks if the stop recording has been requested or not. */ - public ScheduledRecording getRecording() { - return mRecording; + public boolean isStopRecordingRequested() { + return mStopRecordingRequested; } /** - * Gets remove schedule checked status. + * Sets the flag of stop recording request. */ - public boolean isRemoveScheduleChecked() { - return mRemoveScheduleChecked; + public void setStopRecordingRequested(boolean stopRecordingRequested) { + SoftPreconditions.checkState(!mStartRecordingRequested); + mStopRecordingRequested = stopRecordingRequested; } /** - * Gets which {@link SchedulesHeaderRow} this schedule row belongs to. + * Checks if the start recording has been requested or not. */ - public SchedulesHeaderRow getHeaderRow() { - return mHeaderRow; + public boolean isStartRecordingRequested() { + return mStartRecordingRequested; + } + + /** + * Sets the flag of start recording request. + */ + public void setStartRecordingRequested(boolean startRecordingRequested) { + SoftPreconditions.checkState(!mStopRecordingRequested); + mStartRecordingRequested = startRecordingRequested; + } + + /** + * Sets the recording schedule. + */ + public void setSchedule(@Nullable ScheduledRecording schedule) { + mSchedule = schedule; + } + + /** + * Returns the channel ID. + */ + public long getChannelId() { + return mSchedule != null ? mSchedule.getChannelId() : -1; + } + + /** + * Returns the start time. + */ + public long getStartTimeMs() { + return mSchedule != null ? mSchedule.getStartTimeMs() : -1; + } + + /** + * Returns the end time. + */ + public long getEndTimeMs() { + return mSchedule != null ? mSchedule.getEndTimeMs() : -1; + } + + /** + * Returns the duration. + */ + public final long getDuration() { + return getEndTimeMs() - getStartTimeMs(); + } + + /** + * Checks if the program is on air. + */ + public final boolean isOnAir() { + long currentTimeMs = System.currentTimeMillis(); + return getStartTimeMs() <= currentTimeMs && getEndTimeMs() > currentTimeMs; + } + + /** + * Checks if the schedule is not started. + */ + public final boolean isRecordingNotStarted() { + return mSchedule != null + && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED; + } + + /** + * Checks if the schedule is in progress. + */ + public final boolean isRecordingInProgress() { + return mSchedule != null + && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS; + } + + /** + * Checks if the schedule has been canceled or not. + */ + public final boolean isScheduleCanceled() { + return mSchedule != null + && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED; + } + + public boolean isRecordingFinished() { + return mSchedule != null + && (mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED + || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED + || mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED); + } + + /** + * Creates and returns the new schedule with the existing information. + */ + public ScheduledRecording.Builder createNewScheduleBuilder() { + return mSchedule != null ? ScheduledRecording.buildFrom(mSchedule) : null; + } + + /** + * Returns the program title with episode number. + */ + public String getProgramTitleWithEpisodeNumber(Context context) { + return mSchedule != null ? mSchedule.getProgramTitleWithEpisodeNumber(context) : null; + } + + /** + * Returns the program title including the season/episode number. + */ + public String getEpisodeDisplayTitle(Context context) { + return mSchedule != null ? mSchedule.getEpisodeDisplayTitle(context) : null; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "(schedule=" + mSchedule + + ",stopRecordingRequested=" + mStopRecordingRequested + + ",startRecordingRequested=" + mStartRecordingRequested + + ")"; + } + + /** + * Checks if the {@code schedule} is for the program or channel. + */ + public boolean matchSchedule(ScheduledRecording schedule) { + if (mSchedule == null) { + return false; + } + if (mSchedule.getType() == ScheduledRecording.TYPE_TIMED) { + return mSchedule.getChannelId() == schedule.getChannelId() + && mSchedule.getStartTimeMs() == schedule.getStartTimeMs() + && mSchedule.getEndTimeMs() == schedule.getEndTimeMs(); + } else { + return mSchedule.getProgramId() == schedule.getProgramId(); + } } } diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java index 3e2630c7..9cc82653 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java @@ -17,12 +17,19 @@ package com.android.tv.dvr.ui.list; import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.text.format.DateUtils; +import android.util.ArraySet; +import android.util.Log; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; import com.android.tv.util.Utils; @@ -30,16 +37,34 @@ import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; /** * An adapter for {@link ScheduleRow}. */ public class ScheduleRowAdapter extends ArrayObjectAdapter { + private static final String TAG = "ScheduleRowAdapter"; + private static final boolean DEBUG = false; + private final static long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); + private static final int MSG_UPDATE_ROW = 1; + private Context mContext; private final List<String> mTitles = new ArrayList<>(); + private final Set<ScheduleRow> mPendingUpdate = new ArraySet<>(); + + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_UPDATE_ROW) { + long currentTimeMs = System.currentTimeMillis(); + handleUpdateRow(currentTimeMs); + sendNextUpdateMessage(currentTimeMs); + } + } + }; public ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector) { super(classPresenterSelector); @@ -64,7 +89,8 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { .getDvrDataManager().getNonStartedScheduledRecordings(); recordingList.addAll(TvApplication.getSingletons(mContext).getDvrDataManager() .getStartedRecordings()); - Collections.sort(recordingList, ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR); + Collections.sort(recordingList, + ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); long deadLine = Utils.getLastMillisecondOfDay(System.currentTimeMillis()); for (int i = 0; i < recordingList.size();) { ArrayList<ScheduledRecording> section = new ArrayList<>(); @@ -83,6 +109,7 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { } deadLine += ONE_DAY_MS; } + sendNextUpdateMessage(System.currentTimeMillis()); } private String calculateHeaderDate(long deadLine) { @@ -93,8 +120,8 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { headerDate = mTitles.get(titleIndex); } else { headerDate = DateUtils.formatDateTime(getContext(), deadLine, - DateUtils.FORMAT_SHOW_WEEKDAY| DateUtils.FORMAT_ABBREV_WEEKDAY - | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH); + DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_ABBREV_MONTH); } return headerDate; } @@ -103,13 +130,13 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { * Stops schedules row adapter. */ public void stop() { - // TODO: Deal with other type of operation. + mHandler.removeCallbacksAndMessages(null); + DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); for (int i = 0; i < size(); i++) { if (get(i) instanceof ScheduleRow) { - ScheduleRow scheduleRow = (ScheduleRow) get(i); - if (scheduleRow.isRemoveScheduleChecked()) { - TvApplication.getSingletons(mContext).getDvrManager() - .removeScheduledRecording(scheduleRow.getRecording()); + ScheduleRow row = (ScheduleRow) get(i); + if (row.isScheduleCanceled()) { + dvrManager.removeScheduledRecording(row.getSchedule()); } } } @@ -124,8 +151,8 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { } for (int i = 0; i < size(); i++) { Object item = get(i); - if (item instanceof ScheduleRow) { - if (((ScheduleRow) item).getRecording().getId() == recording.getId()) { + if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) { + if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) { return (ScheduleRow) item; } } @@ -133,19 +160,32 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { return null; } - /** - * Adds a {@link ScheduleRow} by {@link ScheduledRecording} and update - * {@link SchedulesHeaderRow} information. - */ - protected void addScheduleRow(ScheduledRecording recording) { + private ScheduleRow findRowWithStartRequest(ScheduledRecording schedule) { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (!(item instanceof ScheduleRow)) { + continue; + } + ScheduleRow row = (ScheduleRow) item; + if (row.getSchedule() != null && row.isStartRecordingRequested() + && row.matchSchedule(schedule)) { + return row; + } + } + return null; + } + + private void addScheduleRow(ScheduledRecording recording) { + // This method must not be called from inherited class. + SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class)); if (recording != null) { int pre = -1; int index = 0; for (; index < size(); index++) { if (get(index) instanceof ScheduleRow) { ScheduleRow scheduleRow = (ScheduleRow) get(index); - if (ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR.compare( - scheduleRow.getRecording(), recording) > 0) { + if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.compare( + scheduleRow.getSchedule(), recording) > 0) { break; } pre = index; @@ -157,11 +197,13 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { headerRow.setItemCount(headerRow.getItemCount() + 1); ScheduleRow addedRow = new ScheduleRow(recording, headerRow); add(++pre, addedRow); + updateHeaderDescription(headerRow); } else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) { SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow(); headerRow.setItemCount(headerRow.getItemCount() + 1); ScheduleRow addedRow = new ScheduleRow(recording, headerRow); add(index, addedRow); + updateHeaderDescription(headerRow); } else { SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine), mContext.getResources().getQuantityString( @@ -177,11 +219,11 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow()); } - /** - * Removes {@link ScheduleRow} and update {@link SchedulesHeaderRow} information. - */ - protected void removeScheduleRow(ScheduleRow scheduleRow) { + private void removeScheduleRow(ScheduleRow scheduleRow) { + // This method must not be called from inherited class. + SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class)); if (scheduleRow != null) { + scheduleRow.setSchedule(null); SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow(); remove(scheduleRow); // Changes the count information of header which the removed row belongs to. @@ -191,58 +233,193 @@ public class ScheduleRowAdapter extends ArrayObjectAdapter { if (headerRow.getItemCount() == 0) { remove(headerRow); } else { - headerRow.setDescription(mContext.getResources().getQuantityString( - R.plurals.dvr_schedules_section_subtitle, - headerRow.getItemCount(), headerRow.getItemCount())); replace(indexOf(headerRow), headerRow); + updateHeaderDescription(headerRow); } } } } + private void updateHeaderDescription(SchedulesHeaderRow headerRow) { + headerRow.setDescription(mContext.getResources().getQuantityString( + R.plurals.dvr_schedules_section_subtitle, + headerRow.getItemCount(), headerRow.getItemCount())); + } + /** * Called when a schedule recording is added to dvr date manager. */ - public void onScheduledRecordingAdded(ScheduledRecording recording) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - addScheduleRow(recording); - notifyArrayItemRangeChanged(0, size()); + public void onScheduledRecordingAdded(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule); + ScheduleRow row = findRowWithStartRequest(schedule); + // If the start recording is requested, onScheduledRecordingAdded is called with NOT_STARTED + // state. And then onScheduleRecordingUpdated will be called with IN_PROGRESS. + // It happens in a short time and causes blinking. To avoid this intermediate state change, + // update the row in onScheduleRecordingUpdated when the state changes to IN_PROGRESS + // instead of in this method. + if (row == null) { + addScheduleRow(schedule); + sendNextUpdateMessage(System.currentTimeMillis()); } } /** * Called when a schedule recording is removed from dvr date manager. */ - public void onScheduledRecordingRemoved(ScheduledRecording recording) { - ScheduleRow scheduleRow = findRowByScheduledRecording(recording); - if (scheduleRow != null) { - removeScheduleRow(scheduleRow); + public void onScheduledRecordingRemoved(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); + ScheduleRow row = findRowByScheduledRecording(schedule); + if (row != null) { + removeScheduleRow(row); + notifyArrayItemRangeChanged(indexOf(row), 1); + sendNextUpdateMessage(System.currentTimeMillis()); } - notifyArrayItemRangeChanged(0, size()); } /** * Called when a schedule recording is updated in dvr date manager. */ - public void onScheduledRecordingUpdated(ScheduledRecording recording) { - ScheduleRow scheduleRow = findRowByScheduledRecording(recording); - if (scheduleRow != null) { - scheduleRow.setRecording(recording); - if (!willBeKept(recording)) { - removeScheduleRow(scheduleRow); + public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); + ScheduleRow row = findRowByScheduledRecording(schedule); + if (row != null) { + if (conflictChange && isStartOrStopRequested()) { + // Delay the conflict update until it gets the response of the start/stop request. + // The purpose is to avoid the intermediate conflict change. + addPendingUpdate(row); + return; + } + if (row.isStopRecordingRequested()) { + // Wait until the recording is finished + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + row.setStopRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); + } + row.setSchedule(schedule); + } + } else { + row.setSchedule(schedule); + if (!willBeKept(schedule)) { + removeScheduleRow(row); + } + } + notifyArrayItemRangeChanged(indexOf(row), 1); + sendNextUpdateMessage(System.currentTimeMillis()); + } else { + row = findRowWithStartRequest(schedule); + // When the start recording was requested, we give the highest priority. So it is + // guaranteed that the state will be changed from NOT_STARTED to the other state. + // Update the row with the next state not to show the intermediate state which causes + // blinking. + if (row != null + && schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + // This can be called multiple times, so do not call + // ScheduleRow.setStartRecordingRequested(false) here. + row.setStartRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); + } + row.setSchedule(schedule); + notifyArrayItemRangeChanged(indexOf(row), 1); + sendNextUpdateMessage(System.currentTimeMillis()); } - } else if (willBeKept(recording)) { - addScheduleRow(recording); } - notifyArrayItemRangeChanged(0, size()); + } + + /** + * Checks if there is a row which requested start/stop recording. + */ + protected boolean isStartOrStopRequested() { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + ScheduleRow row = (ScheduleRow) item; + if (row.isStartRecordingRequested() || row.isStopRecordingRequested()) { + return true; + } + } + } + return false; + } + + /** + * Delays update of the row. + */ + protected void addPendingUpdate(ScheduleRow row) { + mPendingUpdate.add(row); + } + + /** + * Executes the pending updates. + */ + protected void executePendingUpdate() { + for (ScheduleRow row : mPendingUpdate) { + int index = indexOf(row); + if (index != -1) { + notifyArrayItemRangeChanged(index, 1); + } + } + mPendingUpdate.clear(); } /** * To check whether the recording should be kept or not. */ - protected boolean willBeKept(ScheduledRecording recording) { - return recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS - || recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED; + protected boolean willBeKept(ScheduledRecording schedule) { + // CANCELED state means that the schedule was removed temporarily, which should be shown + // in the list so that the user can reschedule it. + return schedule.getEndTimeMs() > System.currentTimeMillis() + && (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS + || schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED); + } + + /** + * Handle the message to update/remove rows. + */ + protected void handleUpdateRow(long currentTimeMs) { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + ScheduleRow row = (ScheduleRow) item; + if (row.getEndTimeMs() <= currentTimeMs) { + removeScheduleRow(row); + } + } + } + } + + /** + * Returns the next update time. Return {@link Long#MAX_VALUE} if no timer is necessary. + */ + protected long getNextTimerMs(long currentTimeMs) { + long earliest = Long.MAX_VALUE; + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + // If the schedule was finished earlier than the end time, it should be removed + // when it reaches the end time in this class. + ScheduleRow row = (ScheduleRow) item; + if (earliest > row.getEndTimeMs()) { + earliest = row.getEndTimeMs(); + } + } + } + return earliest; + } + + /** + * Send update message at the time returned by {@link #getNextTimerMs}. + */ + protected final void sendNextUpdateMessage(long currentTimeMs) { + mHandler.removeMessages(MSG_UPDATE_ROW); + long nextTime = getNextTimerMs(currentTimeMs); + if (nextTime != Long.MAX_VALUE) { + mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROW, + nextTime - System.currentTimeMillis()); + } } } diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java index 23aaf4c3..1257e725 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java @@ -16,17 +16,20 @@ package com.android.tv.dvr.ui.list; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; -import android.graphics.drawable.Drawable; -import android.media.tv.TvInputInfo; +import android.content.res.Resources; +import android.os.Build; +import android.support.annotation.IntDef; import android.support.v17.leanback.widget.RowPresenter; import android.text.TextUtils; -import android.util.ArraySet; -import android.util.Range; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnFocusChangeListener; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; @@ -37,140 +40,137 @@ import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; +import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.ui.DvrStopRecordingFragment; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; -import java.util.ArrayList; -import java.util.HashMap; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; /** * A RowPresenter for {@link ScheduleRow}. */ +@TargetApi(Build.VERSION_CODES.N) public class ScheduleRowPresenter extends RowPresenter { - private Context mContext; - private Set<ScheduleRowClickListener> mListeners = new ArraySet<>(); - private final Drawable mBeingRecordedDrawable; - - private final Map<String, HashMap<Long, ScheduledRecording>> mInputScheduleMap = new - HashMap<>(); - private final List<ScheduledRecording> mConflicts = new ArrayList<>(); - // TODO: Handle input schedule map and conflicts info in the adapter. - - private final Drawable mOnAirDrawable; - private final Drawable mCancelDrawable; - private final Drawable mScheduleDrawable; + private static final String TAG = "ScheduleRowPresenter"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ACTION_START_RECORDING, ACTION_STOP_RECORDING, ACTION_CREATE_SCHEDULE, + ACTION_REMOVE_SCHEDULE}) + public @interface ScheduleRowAction {} + /** An action to start recording. */ + public static final int ACTION_START_RECORDING = 1; + /** An action to stop recording. */ + public static final int ACTION_STOP_RECORDING = 2; + /** An action to create schedule for the row. */ + public static final int ACTION_CREATE_SCHEDULE = 3; + /** An action to remove the schedule. */ + public static final int ACTION_REMOVE_SCHEDULE = 4; + + private final Context mContext; + private final DvrManager mDvrManager; + private final DvrScheduleManager mDvrScheduleManager; private final String mTunerConflictWillNotBeRecordedInfo; private final String mTunerConflictWillBePartiallyRecordedInfo; - private final String mInfoSeparator; + private final int mAnimationDuration; + + private int mLastFocusedViewId; /** * A ViewHolder for {@link ScheduleRow} */ public static class ScheduleRowViewHolder extends RowPresenter.ViewHolder { + private ScheduleRowPresenter mPresenter; + @ScheduleRowAction private int[] mActions; private boolean mLtr; private LinearLayout mInfoContainer; - private RelativeLayout mScheduleActionContainer; - private RelativeLayout mDeleteActionContainer; + // The first action is on the right of the second action. + private RelativeLayout mSecondActionContainer; + private RelativeLayout mFirstActionContainer; private View mSelectorView; private TextView mTimeView; private TextView mProgramTitleView; private TextView mInfoSeparatorView; private TextView mChannelNameView; private TextView mConflictInfoView; - private ImageView mScheduleActionView; - private ImageView mDeleteActionView; - - private ScheduledRecording mRecording; + private ImageView mSecondActionView; + private ImageView mFirstActionView; + + private Runnable mPendingAnimationRunnable; + + private final int mSelectorTranslationDelta; + private final int mSelectorWidthDelta; + private final int mInfoContainerTargetWidthWithNoAction; + private final int mInfoContainerTargetWidthWithOneAction; + private final int mInfoContainerTargetWidthWithTwoAction; + private final int mRoundRectRadius; + + private final OnFocusChangeListener mOnFocusChangeListener = + new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean focused) { + view.post(new Runnable() { + @Override + public void run() { + if (view.isFocused()) { + mPresenter.mLastFocusedViewId = view.getId(); + } + updateSelector(); + } + }); + } + }; - public ScheduleRowViewHolder(View view) { + public ScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) { super(view); + mPresenter = presenter; mLtr = view.getContext().getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; mInfoContainer = (LinearLayout) view.findViewById(R.id.info_container); - mScheduleActionContainer = (RelativeLayout) view.findViewById( - R.id.action_schedule_container); - mScheduleActionView = (ImageView) view.findViewById(R.id.action_schedule); - mDeleteActionContainer = (RelativeLayout) view.findViewById( - R.id.action_delete_container); - mDeleteActionView = (ImageView) view.findViewById(R.id.action_delete); + mSecondActionContainer = (RelativeLayout) view.findViewById( + R.id.action_second_container); + mSecondActionView = (ImageView) view.findViewById(R.id.action_second); + mFirstActionContainer = (RelativeLayout) view.findViewById( + R.id.action_first_container); + mFirstActionView = (ImageView) view.findViewById(R.id.action_first); mSelectorView = view.findViewById(R.id.selector); mTimeView = (TextView) view.findViewById(R.id.time); mProgramTitleView = (TextView) view.findViewById(R.id.program_title); mInfoSeparatorView = (TextView) view.findViewById(R.id.info_separator); mChannelNameView = (TextView) view.findViewById(R.id.channel_name); mConflictInfoView = (TextView) view.findViewById(R.id.conflict_info); - - mInfoContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean focused) { - view.post(new Runnable() { - @Override - public void run() { - updateSelector(); - } - }); - } - }); - - mDeleteActionContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean focused) { - view.post(new Runnable() { - @Override - public void run() { - updateSelector(); - } - }); - } - }); - - mScheduleActionContainer.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean focused) { - view.post(new Runnable() { - @Override - public void run() { - updateSelector(); - } - }); - } - }); - } - - /** - * Sets scheduled recording. - */ - public void setRecording(ScheduledRecording recording) { - mRecording = recording; - } - - /** - * Returns Info container. - */ - public LinearLayout getInfoContainer() { - return mInfoContainer; - } - - /** - * Returns schedule action container. - */ - public RelativeLayout getScheduleActionContainer() { - return mScheduleActionContainer; - } - - /** - * Returns delete action container. - */ - public RelativeLayout getDeleteActionContainer() { - return mDeleteActionContainer; + Resources res = view.getResources(); + mSelectorTranslationDelta = + res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_focus_translation_delta); + mSelectorWidthDelta = res.getDimensionPixelSize( + R.dimen.dvr_schedules_item_focus_width_delta); + mRoundRectRadius = res.getDimensionPixelSize(R.dimen.dvr_schedules_selector_radius); + int fullWidth = res.getDimensionPixelSize( + R.dimen.dvr_schedules_item_width) + - 2 * res.getDimensionPixelSize(R.dimen.dvr_schedules_layout_padding); + mInfoContainerTargetWidthWithNoAction = fullWidth + 2 * mRoundRectRadius; + mInfoContainerTargetWidthWithOneAction = fullWidth + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_delete_width) + + mRoundRectRadius + mSelectorWidthDelta; + mInfoContainerTargetWidthWithTwoAction = mInfoContainerTargetWidthWithOneAction + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) + - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_icon_size); + + mInfoContainer.setOnFocusChangeListener(mOnFocusChangeListener); + mFirstActionContainer.setOnFocusChangeListener(mOnFocusChangeListener); + mSecondActionContainer.setOnFocusChangeListener(mOnFocusChangeListener); } /** @@ -187,92 +187,55 @@ public class ScheduleRowPresenter extends RowPresenter { return mProgramTitleView; } - /** - * Returns subtitle view. - */ - public TextView getChannelNameView() { - return mChannelNameView; - } - - /** - * Returns conflict information view. - */ - public TextView getConflictInfoView() { - return mConflictInfoView; - } - - /** - * Returns schedule action view. - */ - public ImageView getScheduleActionView() { - return mScheduleActionView; - } - - /** - * Returns delete action view. - */ - public ImageView getDeleteActionView() { - return mDeleteActionView; - } - - /** - * Returns scheduled recording. - */ - public ScheduledRecording getRecording() { - return mRecording; - } - private void updateSelector() { - // TODO: Support RTL language int animationDuration = mSelectorView.getResources().getInteger( android.R.integer.config_shortAnimTime); DecelerateInterpolator interpolator = new DecelerateInterpolator(); - int roundRectRadius = view.getResources().getDimensionPixelSize( - R.dimen.dvr_schedules_selector_radius); - if (mInfoContainer.isFocused() || mScheduleActionContainer.isFocused() - || mDeleteActionContainer.isFocused()) { + if (mInfoContainer.isFocused() || mSecondActionContainer.isFocused() + || mFirstActionContainer.isFocused()) { final ViewGroup.LayoutParams lp = mSelectorView.getLayoutParams(); final int targetWidth; if (mInfoContainer.isFocused()) { - if (mScheduleActionContainer.getVisibility() == View.GONE - && mDeleteActionContainer.getVisibility() == View.GONE) { - targetWidth = mInfoContainer.getWidth() + 2 * roundRectRadius; + // Use actions to check the visibility of the actions instead of calling + // View.getVisibility() because the view could be on the hiding animation. + if (mActions == null || mActions.length == 0) { + targetWidth = mInfoContainerTargetWidthWithNoAction; + } else if (mActions.length == 1) { + targetWidth = mInfoContainerTargetWidthWithOneAction; } else { - targetWidth = mInfoContainer.getWidth() + roundRectRadius; - } - } else if (mScheduleActionContainer.isFocused()) { - if (mScheduleActionContainer.getWidth() > 2 * roundRectRadius) { - targetWidth = mScheduleActionContainer.getWidth(); - } else { - targetWidth = 2 * roundRectRadius; + targetWidth = mInfoContainerTargetWidthWithTwoAction; } + } else if (mSecondActionContainer.isFocused()) { + targetWidth = Math.max(mSecondActionContainer.getWidth(), 2 * mRoundRectRadius); } else { - targetWidth = mDeleteActionContainer.getWidth() + roundRectRadius; + targetWidth = mFirstActionContainer.getWidth() + mRoundRectRadius + + mSelectorTranslationDelta; } float targetTranslationX; if (mInfoContainer.isFocused()) { - targetTranslationX = mLtr ? mInfoContainer.getLeft() - roundRectRadius + targetTranslationX = mLtr ? mInfoContainer.getLeft() - mRoundRectRadius - mSelectorView.getLeft() : - mInfoContainer.getRight() + roundRectRadius - mInfoContainer.getRight(); - } else if (mScheduleActionContainer.isFocused()) { - if (mScheduleActionContainer.getWidth() > 2 * roundRectRadius) { - targetTranslationX = mLtr ? mScheduleActionContainer.getLeft() - + mInfoContainer.getRight() + mRoundRectRadius - mSelectorView.getRight(); + } else if (mSecondActionContainer.isFocused()) { + if (mSecondActionContainer.getWidth() > 2 * mRoundRectRadius) { + targetTranslationX = mLtr ? mSecondActionContainer.getLeft() - mSelectorView.getLeft() - : mScheduleActionContainer.getRight() - mSelectorView.getRight(); + : mSecondActionContainer.getRight() - mSelectorView.getRight(); } else { - targetTranslationX = mLtr ? mScheduleActionContainer.getLeft() - - (roundRectRadius - mScheduleActionContainer.getWidth() / 2) - + targetTranslationX = mLtr ? mSecondActionContainer.getLeft() - + (mRoundRectRadius - mSecondActionContainer.getWidth() / 2) - mSelectorView.getLeft() - : mScheduleActionContainer.getRight() + - (roundRectRadius - mScheduleActionContainer.getWidth() / 2) - + : mSecondActionContainer.getRight() + + (mRoundRectRadius - mSecondActionContainer.getWidth() / 2) - mSelectorView.getRight(); } } else { - targetTranslationX = mLtr ? mDeleteActionContainer.getLeft() - - mSelectorView.getLeft() - : mDeleteActionContainer.getRight() - mSelectorView.getRight(); + targetTranslationX = mLtr ? mFirstActionContainer.getLeft() + - mSelectorTranslationDelta - mSelectorView.getLeft() + : mFirstActionContainer.getRight() + mSelectorTranslationDelta + - mSelectorView.getRight(); } if (mSelectorView.getAlpha() == 0) { @@ -294,10 +257,14 @@ public class ScheduleRowPresenter extends RowPresenter { mSelectorView.requestLayout(); } }).setDuration(animationDuration).setInterpolator(interpolator).start(); + if (mPendingAnimationRunnable != null) { + mPendingAnimationRunnable.run(); + mPendingAnimationRunnable = null; + } } else { mSelectorView.animate().cancel(); mSelectorView.animate().alpha(0f).setDuration(animationDuration) - .setInterpolator(interpolator).start(); + .setInterpolator(interpolator).setUpdateListener(null).start(); } } @@ -338,23 +305,20 @@ public class ScheduleRowPresenter extends RowPresenter { setHeaderPresenter(null); setSelectEffectEnabled(false); mContext = context; - mBeingRecordedDrawable = mContext.getDrawable(R.drawable.ic_record_stop); - mOnAirDrawable = mContext.getDrawable(R.drawable.ic_record_start); - mCancelDrawable = mContext.getDrawable(R.drawable.ic_dvr_cancel); - mScheduleDrawable = mContext.getDrawable(R.drawable.ic_scheduled_recording); + mDvrManager = TvApplication.getSingletons(context).getDvrManager(); + mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager(); mTunerConflictWillNotBeRecordedInfo = mContext.getString( R.string.dvr_schedules_tuner_conflict_will_not_be_recorded_info); mTunerConflictWillBePartiallyRecordedInfo = mContext.getString( R.string.dvr_schedules_tuner_conflict_will_be_partially_recorded); - mInfoSeparator = mContext.getString(R.string.dvr_schedules_information_separator); - updateInputScheduleMap(); + mAnimationDuration = mContext.getResources().getInteger( + android.R.integer.config_shortAnimTime); } @Override public ViewHolder createRowViewHolder(ViewGroup parent) { - View view = LayoutInflater.from(mContext).inflate(R.layout.dvr_schedules_item, - parent, false); - return onGetScheduleRowViewHolder(view); + return onGetScheduleRowViewHolder(LayoutInflater.from(mContext) + .inflate(R.layout.dvr_schedules_item, parent, false)); } /** @@ -365,396 +329,467 @@ public class ScheduleRowPresenter extends RowPresenter { } /** - * Returns be recorded drawable which is for being recorded scheduled recordings. - */ - protected Drawable getBeingRecordedDrawable() { - return mBeingRecordedDrawable; - } - - /** - * Returns on air drawable which is for on air but not being recorded scheduled recordings. + * Returns DVR manager. */ - protected Drawable getOnAirDrawable() { - return mOnAirDrawable; - } - - /** - * Returns cancel drawable which is for cancelling scheduled recording. - */ - protected Drawable getCancelDrawable() { - return mCancelDrawable; - } - - /** - * Returns schedule drawable which is for scheduling. - */ - protected Drawable getScheduleDrawable() { - return mScheduleDrawable; - } - - /** - * Returns conflicting scheduled recordings. - */ - protected List<ScheduledRecording> getConflicts() { - return mConflicts; + protected DvrManager getDvrManager() { + return mDvrManager; } @Override protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { super.onBindRowViewHolder(vh, item); ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; - ScheduleRow scheduleRow = (ScheduleRow) item; - ScheduledRecording recording = scheduleRow.getRecording(); - // TODO: Do not show separator in the first row. + ScheduleRow row = (ScheduleRow) item; + @ScheduleRowAction int[] actions = getAvailableActions(row); + viewHolder.mActions = actions; viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onInfoClicked(scheduleRow); + onInfoClicked(row); } }); - viewHolder.mDeleteActionContainer.setOnClickListener(new View.OnClickListener() { + viewHolder.mFirstActionContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onDeleteClicked(scheduleRow, viewHolder); + onActionClicked(actions[0], row); } }); - viewHolder.mScheduleActionContainer.setOnClickListener(new View.OnClickListener() { + viewHolder.mSecondActionContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onScheduleClicked(scheduleRow); + onActionClicked(actions[1], row); } }); - viewHolder.mTimeView.setText(onGetRecordingTimeText(recording)); - Channel channel = TvApplication.getSingletons(mContext).getChannelDataManager() - .getChannel(recording.getChannelId()); - String programInfoText = onGetProgramInfoText(recording); + viewHolder.mTimeView.setText(onGetRecordingTimeText(row)); + String programInfoText = onGetProgramInfoText(row); if (TextUtils.isEmpty(programInfoText)) { int durationMins = - Math.max((int) TimeUnit.MILLISECONDS.toMinutes(recording.getDuration()), 1); + Math.max((int) TimeUnit.MILLISECONDS.toMinutes(row.getDuration()), 1); programInfoText = mContext.getResources().getQuantityString( R.plurals.dvr_schedules_recording_duration, durationMins, durationMins); } - String channelName = channel != null ? channel.getDisplayName() : null; + String channelName = getChannelNameText(row); viewHolder.mProgramTitleView.setText(programInfoText); viewHolder.mInfoSeparatorView.setVisibility((!TextUtils.isEmpty(programInfoText) && !TextUtils.isEmpty(channelName)) ? View.VISIBLE : View.GONE); viewHolder.mChannelNameView.setText(channelName); - if (!scheduleRow.isRemoveScheduleChecked()) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewHolder.mDeleteActionView.setImageDrawable(mBeingRecordedDrawable); - } else { - viewHolder.mDeleteActionView.setImageDrawable(mCancelDrawable); + if (actions != null) { + switch (actions.length) { + case 2: + viewHolder.mSecondActionView.setImageResource(getImageForAction(actions[1])); + // pass through + case 1: + viewHolder.mFirstActionView.setImageResource(getImageForAction(actions[0])); + break; } - } else { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewHolder.mDeleteActionView.setImageDrawable(mOnAirDrawable); + } + if (mDvrManager.isConflicting(row.getSchedule())) { + String conflictInfo; + if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) { + conflictInfo = mTunerConflictWillBePartiallyRecordedInfo; } else { - viewHolder.mDeleteActionView.setImageDrawable(mScheduleDrawable); + conflictInfo = mTunerConflictWillNotBeRecordedInfo; } - viewHolder.mProgramTitleView.setTextColor( - mContext.getResources().getColor(R.color.dvr_schedules_item_info, null)); + viewHolder.mConflictInfoView.setText(conflictInfo); + viewHolder.mConflictInfoView.setVisibility(View.VISIBLE); + } else { + viewHolder.mConflictInfoView.setVisibility(View.GONE); + } + if (shouldBeGrayedOut(row)) { + viewHolder.greyOutInfo(); + } else { + viewHolder.whiteBackInfo(); + } + updateActionContainer(viewHolder, viewHolder.isSelected()); + } + + private int getImageForAction(@ScheduleRowAction int action) { + switch (action) { + case ACTION_START_RECORDING: + return R.drawable.ic_record_start; + case ACTION_STOP_RECORDING: + return R.drawable.ic_record_stop; + case ACTION_CREATE_SCHEDULE: + return R.drawable.ic_scheduled_recording; + case ACTION_REMOVE_SCHEDULE: + return R.drawable.ic_dvr_cancel; + default: + return 0; } - viewHolder.mRecording = recording; - onBindRowViewHolderInternal(viewHolder, scheduleRow); } /** * Returns view holder for schedule row. */ protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) { - return new ScheduleRowViewHolder(view); + return new ScheduleRowViewHolder(view, this); } /** * Returns time text for time view from scheduled recording. */ - protected String onGetRecordingTimeText(ScheduledRecording recording) { - return Utils.getDurationString(mContext, recording.getStartTimeMs(), - recording.getEndTimeMs(), true, false, true, 0); + protected String onGetRecordingTimeText(ScheduleRow row) { + return Utils.getDurationString(mContext, row.getStartTimeMs(), row.getEndTimeMs(), true, + false, true, 0); } /** * Returns program info text for program title view. */ - protected String onGetProgramInfoText(ScheduledRecording recording) { - if (recording != null) { - return recording.getProgramTitle(); - } - return null; + protected String onGetProgramInfoText(ScheduleRow row) { + return row.getProgramTitleWithEpisodeNumber(mContext); } - /** - * Internal method for onBindRowViewHolder, can be customized by subclass. - */ - protected void onBindRowViewHolderInternal(ScheduleRowViewHolder viewHolder, ScheduleRow - scheduleRow) { - if (mConflicts.contains(scheduleRow.getRecording())) { - viewHolder.mScheduleActionView.setImageDrawable(mScheduleDrawable); - String conflictInfo = mTunerConflictWillNotBeRecordedInfo; - // TODO: It's also possible for the NonStarted schedules to be partially recorded. - if (viewHolder.mRecording.getState() - == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - conflictInfo = mTunerConflictWillBePartiallyRecordedInfo; - } - viewHolder.mConflictInfoView.setText(conflictInfo); - // TODO: Add 12dp warning icon to conflict info. - viewHolder.mConflictInfoView.setVisibility(View.VISIBLE); - viewHolder.greyOutInfo(); - } else { - viewHolder.mScheduleActionContainer.setVisibility(View.GONE); - viewHolder.mConflictInfoView.setVisibility(View.GONE); - if (!scheduleRow.isRemoveScheduleChecked()) { - viewHolder.whiteBackInfo(); - } - } + private String getChannelNameText(ScheduleRow row) { + Channel channel = TvApplication.getSingletons(mContext).getChannelDataManager() + .getChannel(row.getChannelId()); + return channel == null ? null : + TextUtils.isEmpty(channel.getDisplayName()) ? channel.getDisplayNumber() : + channel.getDisplayName().trim() + " " + channel.getDisplayNumber(); } /** - * Updates input schedule map. + * Called when user click Info in {@link ScheduleRow}. */ - private void updateInputScheduleMap() { - mInputScheduleMap.clear(); - List<ScheduledRecording> allRecordings = TvApplication.getSingletons(getContext()) - .getDvrDataManager().getAvailableScheduledRecordings(); - for(ScheduledRecording recording : allRecordings) { - addScheduledRecordingToMap(recording); + protected void onInfoClicked(ScheduleRow scheduleRow) { + ScheduledRecording schedule = scheduleRow.getSchedule(); + if (schedule != null) { + DvrUiHelper.startDetailsActivity((Activity) mContext, schedule, null, true); } - updateConflicts(); } /** - * Updates conflicting scheduled recordings. + * Called when the button in a row is clicked. */ - private void updateConflicts() { - mConflicts.clear(); - for (String inputId : mInputScheduleMap.keySet()) { - TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId); - if (input == null) { - continue; - } - mConflicts.addAll(DvrScheduleManager.getConflictingSchedules( - new ArrayList<>(mInputScheduleMap.get(inputId).values()), - input.getTunerCount())); + protected void onActionClicked(@ScheduleRowAction final int action, ScheduleRow row) { + switch (action) { + case ACTION_START_RECORDING: + onStartRecording(row); + break; + case ACTION_STOP_RECORDING: + onStopRecording(row); + break; + case ACTION_CREATE_SCHEDULE: + onCreateSchedule(row); + break; + case ACTION_REMOVE_SCHEDULE: + onRemoveSchedule(row); + break; } } /** - * Adds a scheduled recording to the map, it happens when user undo cancel. + * Action handler for {@link #ACTION_START_RECORDING}. */ - private void addScheduledRecordingToMap(ScheduledRecording recording) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, - recording.getChannelId()); - if (input == null) { + protected void onStartRecording(ScheduleRow row) { + ScheduledRecording schedule = row.getSchedule(); + if (schedule == null) { + // This row has been deleted. return; } - String inputId = input.getId(); - HashMap<Long, ScheduledRecording> schedulesMap = mInputScheduleMap.get(inputId); - if (schedulesMap == null) { - schedulesMap = new HashMap<>(); - mInputScheduleMap.put(inputId, schedulesMap); + // Checks if there are current recordings that will be stopped by schedule this program. + // If so, shows confirmation dialog to users. + List<ScheduledRecording> conflictSchedules = mDvrScheduleManager.getConflictingSchedules( + schedule.getChannelId(), System.currentTimeMillis(), schedule.getEndTimeMs()); + for (int i = conflictSchedules.size() - 1; i >= 0; i--) { + ScheduledRecording conflictSchedule = conflictSchedules.get(i); + if (conflictSchedule.isInProgress()) { + DvrUiHelper.showStopRecordingDialog((Activity) mContext, + conflictSchedule.getChannelId(), + DvrStopRecordingFragment.REASON_ON_CONFLICT, + new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrStopRecordingFragment.ACTION_STOP) { + onStartRecordingInternal(row); + } + } + }); + return; + } } - schedulesMap.put(recording.getId(), recording); + onStartRecordingInternal(row); } - /** - * Called when a scheduled recording is added into dvr date manager. - */ - public void onScheduledRecordingAdded(ScheduledRecording recording) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED || recording - .getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - addScheduledRecordingToMap(recording); - updateConflicts(); + private void onStartRecordingInternal(ScheduleRow row) { + if (row.isOnAir() && !row.isRecordingInProgress() && !row.isStartRecordingRequested()) { + row.setStartRecordingRequested(true); + if (row.isRecordingNotStarted()) { + mDvrManager.setHighestPriority(row.getSchedule()); + } else if (row.isRecordingFinished()) { + mDvrManager.addSchedule(ScheduledRecording.buildFrom(row.getSchedule()) + .setId(ScheduledRecording.ID_NOT_SET) + .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) + .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) + .build()); + } else { + SoftPreconditions.checkState(false, TAG, "Invalid row state to start recording: " + + row); + return; + } + String msg = mContext.getString(R.string.dvr_msg_current_program_scheduled, + row.getSchedule().getProgramTitle(), + Utils.toTimeString(row.getEndTimeMs(), false)); + ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT); } } /** - * Adds a scheduled recording to the map, it happens when user undo cancel. + * Action handler for {@link #ACTION_STOP_RECORDING}. */ - private void updateScheduledRecordingToMap(ScheduledRecording recording) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED || - recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, - recording.getChannelId()); - if (input == null) { - return; - } - String inputId = input.getId(); - HashMap<Long, ScheduledRecording> schedulesMap = mInputScheduleMap.get(inputId); - if (schedulesMap == null) { - addScheduledRecordingToMap(recording); - return; + protected void onStopRecording(ScheduleRow row) { + if (row.getSchedule() == null) { + // This row has been deleted. + return; + } + if (row.isOnAir() && row.isRecordingInProgress() && !row.isStopRecordingRequested()) { + row.setStopRecordingRequested(true); + mDvrManager.stopRecording(row.getSchedule()); + CharSequence deletedInfo = onGetProgramInfoText(row); + if (TextUtils.isEmpty(deletedInfo)) { + deletedInfo = getChannelNameText(row); } - schedulesMap.put(recording.getId(), recording); - } else { - removeScheduledRecordingFromMap(recording); + ToastUtils.show(mContext, mContext.getResources() + .getString(R.string.dvr_schedules_deletion_info, deletedInfo), + Toast.LENGTH_SHORT); } } /** - * Called when a scheduled recording is updated in dvr date manager. + * Action handler for {@link #ACTION_CREATE_SCHEDULE}. */ - public void onScheduledRecordingUpdated(ScheduledRecording recording) { - updateScheduledRecordingToMap(recording); - updateConflicts(); + protected void onCreateSchedule(ScheduleRow row) { + if (row.getSchedule() == null) { + // This row has been deleted. + return; + } + if (!row.isOnAir()) { + if (row.isScheduleCanceled()) { + mDvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(row.getSchedule()) + .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) + .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) + .build()); + String msg = mContext.getString(R.string.dvr_msg_program_scheduled, + row.getSchedule().getProgramTitle()); + ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT); + } else if (mDvrManager.isConflicting(row.getSchedule())) { + mDvrManager.setHighestPriority(row.getSchedule()); + } + } } /** - * Removes a scheduled recording from the map, it happens when user cancel schedule. + * Action handler for {@link #ACTION_REMOVE_SCHEDULE}. */ - private void removeScheduledRecordingFromMap(ScheduledRecording recording) { - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, recording.getChannelId()); - if (input == null) { + protected void onRemoveSchedule(ScheduleRow row) { + if (row.getSchedule() == null) { + // This row has been deleted. return; } - String inputId = input.getId(); - HashMap<Long, ScheduledRecording> schedulesMap = mInputScheduleMap.get(inputId); - if (schedulesMap == null) { - return; + CharSequence deletedInfo = null; + if (row.isOnAir()) { + if (row.isRecordingNotStarted()) { + deletedInfo = getDeletedInfo(row); + mDvrManager.removeScheduledRecording(row.getSchedule()); + } + } else { + if (mDvrManager.isConflicting(row.getSchedule()) + && !shouldKeepScheduleAfterRemoving()) { + deletedInfo = getDeletedInfo(row); + mDvrManager.removeScheduledRecording(row.getSchedule()); + } else if (row.isRecordingNotStarted()) { + deletedInfo = getDeletedInfo(row); + mDvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(row.getSchedule()) + .setState(ScheduledRecording.STATE_RECORDING_CANCELED) + .build()); + } } - schedulesMap.remove(recording.getId()); - if (schedulesMap.isEmpty()) { - mInputScheduleMap.remove(inputId); + if (deletedInfo != null) { + ToastUtils.show(mContext, mContext.getResources() + .getString(R.string.dvr_schedules_deletion_info, deletedInfo), + Toast.LENGTH_SHORT); } } - /** - * Called when a scheduled recording is removed from dvr date manager. - */ - public void onScheduledRecordingRemoved(ScheduledRecording recording) { - removeScheduledRecordingFromMap(recording); - updateConflicts(); + private CharSequence getDeletedInfo(ScheduleRow row) { + CharSequence deletedInfo = onGetProgramInfoText(row); + if (TextUtils.isEmpty(deletedInfo)) { + return getChannelNameText(row); + } + return deletedInfo; } - /** - * Called when user click Info in {@link ScheduleRow}. - */ - protected void onInfoClicked(ScheduleRow scheduleRow) { - DvrUiHelper.startDetailsActivity((Activity) mContext, - scheduleRow.getRecording(), null, true); + @Override + protected void onRowViewSelected(ViewHolder vh, boolean selected) { + super.onRowViewSelected(vh, selected); + updateActionContainer(vh, selected); } /** - * Called when user click schedule in {@link ScheduleRow}. + * Internal method for onRowViewSelected, can be customized by subclass. */ - protected void onScheduleClicked(ScheduleRow scheduleRow) { - ScheduledRecording scheduledRecording = scheduleRow.getRecording(); - TvInputInfo input = Utils.getTvInputInfoForChannelId(mContext, - scheduledRecording.getChannelId()); - if (input == null) { - return; - } - List<ScheduledRecording> allScheduledRecordings = new ArrayList<ScheduledRecording>( - mInputScheduleMap.get(input.getId()).values()); - long maxPriority = scheduledRecording.getPriority(); - for (ScheduledRecording recording : allScheduledRecordings) { - if (scheduledRecording.isOverLapping( - new Range<>(recording.getStartTimeMs(), recording.getEndTimeMs()))) { - if (maxPriority < recording.getPriority()) { - maxPriority = recording.getPriority(); + private void updateActionContainer(ViewHolder vh, boolean selected) { + ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; + viewHolder.mSecondActionContainer.animate().setListener(null).cancel(); + viewHolder.mFirstActionContainer.animate().setListener(null).cancel(); + if (selected && viewHolder.mActions != null) { + switch (viewHolder.mActions.length) { + case 2: + prepareShowActionView(viewHolder.mSecondActionContainer); + prepareShowActionView(viewHolder.mFirstActionContainer); + viewHolder.mPendingAnimationRunnable = new Runnable() { + @Override + public void run() { + showActionView(viewHolder.mSecondActionContainer); + showActionView(viewHolder.mFirstActionContainer); + } + }; + break; + case 1: + prepareShowActionView(viewHolder.mFirstActionContainer); + viewHolder.mPendingAnimationRunnable = new Runnable() { + @Override + public void run() { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + showActionView(viewHolder.mFirstActionContainer); + } + }; + if (mLastFocusedViewId == R.id.action_second_container) { + mLastFocusedViewId = R.id.info_container; + } + break; + case 0: + default: + viewHolder.mPendingAnimationRunnable = new Runnable() { + @Override + public void run() { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + hideActionView(viewHolder.mFirstActionContainer, View.GONE); + } + }; + if (mLastFocusedViewId == R.id.action_first_container + || mLastFocusedViewId == R.id.action_second_container) { + mLastFocusedViewId = R.id.info_container; + } + break; + } + View view = viewHolder.view.findViewById(mLastFocusedViewId); + if (view != null && view.getVisibility() == View.VISIBLE) { + // When the row is selected, information container gets the initial focus. + // To give the focus to the same control as the previous row, we need to call + // requestFocus() explicitly. + if (view.hasFocus()) { + viewHolder.mPendingAnimationRunnable.run(); + } else { + view.requestFocus(); } } + } else { + viewHolder.mPendingAnimationRunnable = null; + hideActionView(viewHolder.mFirstActionContainer, View.GONE); + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + } + } + + private void prepareShowActionView(View view) { + if (view.getVisibility() != View.VISIBLE) { + view.setAlpha(0.0f); } - TvApplication.getSingletons(getContext()).getDvrManager() - .updateScheduledRecording(ScheduledRecording.buildFrom(scheduledRecording) - .setPriority(maxPriority + 1).build()); - updateConflicts(); + view.setVisibility(View.VISIBLE); } /** - * Called when user click delete in {@link ScheduleRow}. + * Add animation when view is visible. */ - protected void onDeleteClicked(ScheduleRow scheduleRow, ViewHolder vh) { - ScheduledRecording recording = scheduleRow.getRecording(); - ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; - if (!scheduleRow.isRemoveScheduleChecked()) { - if (mConflicts.contains(recording)) { - TvApplication.getSingletons(mContext) - .getDvrManager().removeScheduledRecording(recording); - } + private void showActionView(View view) { + view.animate().alpha(1.0f).setInterpolator(new DecelerateInterpolator()) + .setDuration(mAnimationDuration).start(); + } - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewHolder.mDeleteActionView.setImageDrawable(mOnAirDrawable); - // TODO: Replace an icon whose size is the same as scheudle. - } else { - viewHolder.getDeleteActionView().setImageDrawable(mScheduleDrawable); - } - viewHolder.greyOutInfo(); - scheduleRow.setRemoveScheduleChecked(true); - CharSequence deletedInfo = viewHolder.getProgramTitleView().getText(); - if (TextUtils.isEmpty(deletedInfo)) { - deletedInfo = viewHolder.getChannelNameView().getText(); - } - Toast.makeText(mContext, mContext.getResources() - .getString(R.string.dvr_schedules_deletion_info, deletedInfo), - Toast.LENGTH_SHORT).show(); - removeScheduledRecordingFromMap(recording); - } else { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewHolder.mDeleteActionView.setImageDrawable(mBeingRecordedDrawable); - // TODO: Replace an icon whose size is the same as scheudle. - } else { - viewHolder.getDeleteActionView().setImageDrawable(mCancelDrawable); + /** + * Add animation when view change to invisible. + */ + private void hideActionView(View view, int visibility) { + if (view.getVisibility() != View.VISIBLE) { + if (view.getVisibility() != visibility) { + view.setVisibility(visibility); } - viewHolder.whiteBackInfo(); - scheduleRow.setRemoveScheduleChecked(false); - addScheduledRecordingToMap(recording); - } - updateConflicts(); - for (ScheduleRowClickListener l : mListeners) { - l.onDeleteClicked(scheduleRow); + return; } + view.animate().alpha(0.0f).setInterpolator(new DecelerateInterpolator()) + .setDuration(mAnimationDuration) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(visibility); + view.animate().setListener(null); + } + }).start(); } /** - * Adds {@link ScheduleRowClickListener}. + * Returns the available actions according to the row's state. It should be the reverse order + * with that in the screen. */ - public void addListener(ScheduleRowClickListener scheduleRowClickListener) { - mListeners.add(scheduleRowClickListener); + @ScheduleRowAction + protected int[] getAvailableActions(ScheduleRow row) { + if (row.getSchedule() != null) { + if (row.isOnAir()) { + if (row.isRecordingInProgress()) { + return new int[] {ACTION_STOP_RECORDING}; + } else if (row.isRecordingNotStarted()) { + if (canResolveConflict()) { + // The "START" action can change the conflict states. + return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING}; + } else { + return new int[] {ACTION_REMOVE_SCHEDULE}; + } + } else if (row.isRecordingFinished()) { + return new int[] {ACTION_START_RECORDING}; + } else { + SoftPreconditions.checkState(false, TAG, "Invalid row state in checking the" + + " available actions(on air): " + row); + } + } else { + if (row.isScheduleCanceled()) { + return new int[] {ACTION_CREATE_SCHEDULE}; + } else if (mDvrManager.isConflicting(row.getSchedule()) && canResolveConflict()) { + return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_CREATE_SCHEDULE}; + } else if (row.isRecordingNotStarted()) { + return new int[] {ACTION_REMOVE_SCHEDULE}; + } else { + SoftPreconditions.checkState(false, TAG, "Invalid row state in checking the" + + " available actions(future schedule): " + row); + } + } + } + return null; } /** - * Removes {@link ScheduleRowClickListener}. + * Check if the conflict can be resolved in this screen. */ - public void removeListener(ScheduleRowClickListener - scheduleRowClickListener) { - mListeners.remove(scheduleRowClickListener); - } - - @Override - protected void onRowViewSelected(ViewHolder vh, boolean selected) { - super.onRowViewSelected(vh, selected); - onRowViewSelectedInternal(vh, selected); + protected boolean canResolveConflict() { + return true; } /** - * Internal method for onRowViewSelected, can be customized by subclass. + * Check if the schedule should be kept after removing it. */ - protected void onRowViewSelectedInternal(ViewHolder vh, boolean selected) { - ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; - boolean isRecordingConflicting = mConflicts.contains(viewHolder.mRecording); - if (selected) { - viewHolder.mDeleteActionContainer.setVisibility(View.VISIBLE); - if (isRecordingConflicting) { - viewHolder.mScheduleActionContainer.setVisibility(View.VISIBLE); - } - } else { - viewHolder.mDeleteActionContainer.setVisibility(View.GONE); - if (isRecordingConflicting) { - viewHolder.mScheduleActionContainer.setVisibility(View.GONE); - } - } + protected boolean shouldKeepScheduleAfterRemoving() { + return false; } /** - * A listener for clicking {@link ScheduleRow}. + * Checks if the row should be grayed out. */ - public interface ScheduleRowClickListener{ - /** - * To notify other observers that delete button has been clicked. - */ - void onDeleteClicked(ScheduleRow scheduleRow); + protected boolean shouldBeGrayedOut(ScheduleRow row) { + return row.getSchedule() == null + || (row.isOnAir() && !row.isRecordingInProgress()) + || mDvrManager.isConflicting(row.getSchedule()) + || row.isScheduleCanceled(); } } diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java index d103a533..0fb0924d 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java @@ -16,8 +16,6 @@ package com.android.tv.dvr.ui.list; -import android.support.annotation.Nullable; - import com.android.tv.dvr.SeriesRecording; /** @@ -88,13 +86,6 @@ public abstract class SchedulesHeaderRow { } /** - * Sets the latest time of the list which belongs to the header row. - */ - public void setDeadLineMs(long deadLineMs) { - mDeadLineMs = deadLineMs; - } - - /** * Returns the latest time of the list which belongs to the header row. */ public long getDeadLineMs() { @@ -106,35 +97,26 @@ public abstract class SchedulesHeaderRow { * The header row which represent the series recording. */ public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow { - private SeriesRecording mSeries; - private boolean mCancelAllChecked; + private SeriesRecording mSeriesRecording; public SeriesRecordingHeaderRow(String title, String description, int itemCount, SeriesRecording series) { super(title, description, itemCount); - mSeries = series; - mCancelAllChecked = series.getState() == SeriesRecording.STATE_SERIES_CANCELED; - } - - /** - * Sets cancel all checked status. - */ - public void setCancelAllChecked(boolean checked) { - mCancelAllChecked = checked; + mSeriesRecording = series; } /** - * Returns cancel all checked status. + * Returns the series recording, it is for series schedules list. */ - public boolean isCancelAllChecked() { - return mCancelAllChecked; + public SeriesRecording getSeriesRecording() { + return mSeriesRecording; } /** - * Returns the series recording, it is for series schedules list. + * Sets the series recording. */ - public SeriesRecording getSeriesRecording() { - return mSeries; + public void setSeriesRecording(SeriesRecording seriesRecording) { + mSeriesRecording = seriesRecording; } } } diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java index 483962e7..69c33a96 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java @@ -20,7 +20,6 @@ import android.animation.ValueAnimator; import android.content.Context; import android.graphics.drawable.Drawable; import android.support.v17.leanback.widget.RowPresenter; -import android.util.ArraySet; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -36,14 +35,11 @@ import com.android.tv.dvr.SeriesRecording; import com.android.tv.dvr.ui.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; -import java.util.Set; - /** * A base class for RowPresenter for {@link SchedulesHeaderRow} */ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { private Context mContext; - private Set<SchedulesHeaderRowListener> mListeners = new ArraySet<>(); public SchedulesHeaderRowPresenter(Context context) { setHeaderPresenter(null); @@ -59,26 +55,6 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } /** - * Adds {@link SchedulesHeaderRowListener}. - */ - public void addListener(SchedulesHeaderRowListener listener) { - mListeners.add(listener); - } - - /** - * Removes {@link SchedulesHeaderRowListener}. - */ - public void removeListener(SchedulesHeaderRowListener listener) { - mListeners.remove(listener); - } - - void notifyUpdateAllScheduleRows() { - for (SchedulesHeaderRowListener listener : mListeners) { - listener.onUpdateAllScheduleRows(); - } - } - - /** * A ViewHolder for {@link SchedulesHeaderRow}. */ public static class SchedulesHeaderRowViewHolder extends RowPresenter.ViewHolder { @@ -147,54 +123,59 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { mCancelDrawable = context.getDrawable(R.drawable.ic_dvr_cancel_large); mResumeDrawable = context.getDrawable(R.drawable.ic_record_start); mSettingsInfo = context.getString(R.string.dvr_series_schedules_settings); - mCancelAllInfo = context.getString(R.string.dvr_series_schedules_cancel_all); - mResumeInfo = context.getString(R.string.dvr_series_schedules_resume); + mCancelAllInfo = context.getString(R.string.dvr_series_schedules_stop); + mResumeInfo = context.getString(R.string.dvr_series_schedules_start); } @Override protected ViewHolder createRowViewHolder(ViewGroup parent) { - return new SeriesRecordingRowViewHolder(getContext(), parent); + return new SeriesHeaderRowViewHolder(getContext(), parent); } @Override protected void onBindRowViewHolder(RowPresenter.ViewHolder viewHolder, Object item) { super.onBindRowViewHolder(viewHolder, item); - SeriesRecordingRowViewHolder headerViewHolder = - (SeriesRecordingRowViewHolder) viewHolder; + SeriesHeaderRowViewHolder headerViewHolder = + (SeriesHeaderRowViewHolder) viewHolder; SeriesRecordingHeaderRow header = (SeriesRecordingHeaderRow) item; headerViewHolder.mSeriesSettingsButton.setVisibility( - isSeriesScheduleCanceled(getContext(), header) ? View.INVISIBLE : View.VISIBLE); + header.getSeriesRecording().isStopped() ? View.INVISIBLE : View.VISIBLE); headerViewHolder.mSeriesSettingsButton.setText(mSettingsInfo); setTextDrawable(headerViewHolder.mSeriesSettingsButton, mSettingsDrawable); - if (header.isCancelAllChecked()) { - headerViewHolder.mTogglePauseButton.setText(mResumeInfo); - setTextDrawable(headerViewHolder.mTogglePauseButton, mResumeDrawable); + if (header.getSeriesRecording().isStopped()) { + headerViewHolder.mToggleStartStopButton.setText(mResumeInfo); + setTextDrawable(headerViewHolder.mToggleStartStopButton, mResumeDrawable); } else { - headerViewHolder.mTogglePauseButton.setText(mCancelAllInfo); - setTextDrawable(headerViewHolder.mTogglePauseButton, mCancelDrawable); + headerViewHolder.mToggleStartStopButton.setText(mCancelAllInfo); + setTextDrawable(headerViewHolder.mToggleStartStopButton, mCancelDrawable); } headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { + // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId()); + header.getSeriesRecording().getId(), null, false, false, false); } }); - headerViewHolder.mTogglePauseButton.setOnClickListener(new OnClickListener() { + headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { - if (!header.isCancelAllChecked()) { - DvrUiHelper.showCancelAllSeriesRecordingDialog((DvrSchedulesActivity) view - .getContext()); + if (header.getSeriesRecording().isStopped()) { + // Reset priority to the highest. + SeriesRecording seriesRecording = SeriesRecording + .buildFrom(header.getSeriesRecording()) + .setPriority(TvApplication.getSingletons(getContext()) + .getDvrScheduleManager().suggestNewSeriesPriority()) + .build(); + TvApplication.getSingletons(getContext()).getDvrManager() + .updateSeriesRecording(seriesRecording); + // TODO: pass channel list for settings. + DvrUiHelper.startSeriesSettingsActivity(getContext(), + header.getSeriesRecording().getId(), null, false, false, false); } else { - if (isSeriesScheduleCanceled(getContext(), header)) { - TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(SeriesRecording.buildFrom(header - .getSeriesRecording()).setState(SeriesRecording - .STATE_SERIES_NORMAL).build()); - } - header.setCancelAllChecked(false); - notifyUpdateAllScheduleRows(); + DvrUiHelper.showCancelAllSeriesRecordingDialog( + (DvrSchedulesActivity) view.getContext(), + header.getSeriesRecording()); } } }); @@ -208,97 +189,85 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } } - private static boolean isSeriesScheduleCanceled(Context context, - SeriesRecordingHeaderRow header) { - return TvApplication.getSingletons(context).getDvrDataManager() - .getSeriesRecording(header.getSeriesRecording().getId()).getState() - == SeriesRecording.STATE_SERIES_CANCELED; - } - /** * A ViewHolder for {@link SeriesRecordingHeaderRow}. */ - public static class SeriesRecordingRowViewHolder extends SchedulesHeaderRowViewHolder { + public static class SeriesHeaderRowViewHolder extends SchedulesHeaderRowViewHolder { private final TextView mSeriesSettingsButton; - private final TextView mTogglePauseButton; + private final TextView mToggleStartStopButton; private final boolean mLtr; private final View mSelector; private View mLastFocusedView; - public SeriesRecordingRowViewHolder(Context context, ViewGroup parent) { + public SeriesHeaderRowViewHolder(Context context, ViewGroup parent) { super(context, parent); mLtr = context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; view.findViewById(R.id.button_container).setVisibility(View.VISIBLE); mSeriesSettingsButton = (TextView) view.findViewById(R.id.series_settings); - mTogglePauseButton = (TextView) view.findViewById(R.id.series_toggle_pause); + mToggleStartStopButton = + (TextView) view.findViewById(R.id.series_toggle_start_stop); mSelector = view.findViewById(R.id.selector); OnFocusChangeListener onFocusChangeListener = new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean focused) { - onIconFouseChange(view); + view.post(new Runnable() { + @Override + public void run() { + updateSelector(view); + } + }); } }; mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener); - mTogglePauseButton.setOnFocusChangeListener(onFocusChangeListener); - } - - void onIconFouseChange(View focusedView) { - updateSelector(focusedView, mSelector); + mToggleStartStopButton.setOnFocusChangeListener(onFocusChangeListener); } - private void updateSelector(View focusedView, final View selectorView) { - int animationDuration = selectorView.getContext().getResources() + private void updateSelector(View focusedView) { + int animationDuration = mSelector.getContext().getResources() .getInteger(android.R.integer.config_shortAnimTime); DecelerateInterpolator interpolator = new DecelerateInterpolator(); if (focusedView.hasFocus()) { - final ViewGroup.LayoutParams lp = selectorView.getLayoutParams(); + ViewGroup.LayoutParams lp = mSelector.getLayoutParams(); final int targetWidth = focusedView.getWidth(); float targetTranslationX; if (mLtr) { - targetTranslationX = focusedView.getLeft() - selectorView.getLeft(); + targetTranslationX = focusedView.getLeft() - mSelector.getLeft(); } else { - targetTranslationX = focusedView.getRight() - selectorView.getRight(); + targetTranslationX = focusedView.getRight() - mSelector.getRight(); } // if the selector is invisible, set the width and translation X directly - // don't animate. - if (selectorView.getAlpha() == 0) { - selectorView.setTranslationX(targetTranslationX); + if (mSelector.getAlpha() == 0) { + mSelector.setTranslationX(targetTranslationX); lp.width = targetWidth; - selectorView.requestLayout(); + mSelector.requestLayout(); } // animate the selector in and to the proper width and translation X. final float deltaWidth = lp.width - targetWidth; - selectorView.animate().cancel(); - selectorView.animate().translationX(targetTranslationX).alpha(1f) + mSelector.animate().cancel(); + mSelector.animate().translationX(targetTranslationX).alpha(1f) .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { // Set width to the proper width for this animation step. lp.width = targetWidth + Math.round( deltaWidth * (1f - animation.getAnimatedFraction())); - selectorView.requestLayout(); + mSelector.requestLayout(); } }).setDuration(animationDuration).setInterpolator(interpolator).start(); mLastFocusedView = focusedView; } else if (mLastFocusedView == focusedView) { - selectorView.animate().cancel(); - selectorView.animate().alpha(0f).setDuration(animationDuration) + mSelector.animate().setUpdateListener(null).cancel(); + mSelector.animate().alpha(0f).setDuration(animationDuration) .setInterpolator(interpolator).start(); mLastFocusedView = null; } } } } - - public interface SchedulesHeaderRowListener { - /** - * Updates all schedule rows. - */ - void onUpdateAllScheduleRows(); - } } diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java index 8b162c54..3b493774 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java @@ -16,12 +16,20 @@ package com.android.tv.dvr.ui.list; +import android.annotation.TargetApi; import android.content.Context; +import android.media.tv.TvInputInfo; +import android.os.Build; import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.util.ArrayMap; +import android.util.Log; +import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.SeriesRecording; @@ -30,127 +38,232 @@ import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.Map; /** * An adapter for series schedule row. */ +@TargetApi(Build.VERSION_CODES.N) public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { - private static final String TAG = "SeriesScheduleRowAdapter"; + private static final String TAG = "SeriesRowAdapter"; + private static final boolean DEBUG = false; - private SeriesRecording mSeriesRecording; + private final SeriesRecording mSeriesRecording; + private final String mInputId; + private final DvrManager mDvrManager; + private final DvrDataManager mDataManager; + private final Map<Long, Program> mPrograms = new ArrayMap<>(); + private SeriesRecordingHeaderRow mHeaderRow; - public SeriesScheduleRowAdapter(Context context, - ClassPresenterSelector classPresenterSelector, SeriesRecording seriesRecording) { + public SeriesScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector, + SeriesRecording seriesRecording) { super(context, classPresenterSelector); mSeriesRecording = seriesRecording; + TvInputInfo input = Utils.getTvInputInfoForInputId(context, mSeriesRecording.getInputId()); + if (SoftPreconditions.checkNotNull(input) != null) { + mInputId = input.getId(); + } else { + mInputId = null; + } + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mDvrManager = singletons.getDvrManager(); + mDataManager = singletons.getDvrDataManager(); + setHasStableIds(true); } @Override public void start() { - List<ScheduledRecording> recordings = TvApplication.getSingletons(getContext()) - .getDvrDataManager().getAvailableAndCanceledScheduledRecordings(); - List<ScheduledRecording> seriesScheduledRecordings = new ArrayList<>(); - if (mSeriesRecording == null) { - return; + setPrograms(Collections.emptyList()); + } + + @Override + public void stop() { + super.stop(); + } + + /** + * Sets the programs to show. + */ + public void setPrograms(List<Program> programs) { + if (programs == null) { + programs = Collections.emptyList(); } - for (ScheduledRecording recording : recordings) { - if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) { - seriesScheduledRecordings.add(recording); + clear(); + mPrograms.clear(); + List<Program> sortedPrograms = new ArrayList<>(programs); + Collections.sort(sortedPrograms); + List<EpisodicProgramRow> rows = new ArrayList<>(); + mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), + null, sortedPrograms.size(), mSeriesRecording); + for (Program program : sortedPrograms) { + ScheduledRecording schedule = + mDataManager.getScheduledRecordingForProgramId(program.getId()); + if (schedule != null && !willBeKept(schedule)) { + schedule = null; } + rows.add(new EpisodicProgramRow(mInputId, program, schedule, mHeaderRow)); + mPrograms.put(program.getId(), program); } - Collections.sort(seriesScheduledRecordings, - ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR); - int dayCountToLastRecording = 0; - if (!seriesScheduledRecordings.isEmpty()) { - long lastRecordingStartTimeMs = seriesScheduledRecordings - .get(seriesScheduledRecordings.size() - 1).getStartTimeMs(); - dayCountToLastRecording = Utils.computeDateDifference(System.currentTimeMillis(), - lastRecordingStartTimeMs) + 1; + mHeaderRow.setDescription(getDescription()); + add(mHeaderRow); + for (EpisodicProgramRow row : rows) { + add(row); } - SchedulesHeaderRow headerRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), - getContext().getResources().getQuantityString( - R.plurals.dvr_series_schedules_header_description, dayCountToLastRecording, - dayCountToLastRecording), seriesScheduledRecordings.size(), mSeriesRecording); - add(headerRow); - for (ScheduledRecording recording : seriesScheduledRecordings) { - add(new ScheduleRow(recording, headerRow)); + sendNextUpdateMessage(System.currentTimeMillis()); + } + + private String getDescription() { + int conflicts = 0; + for (long programId : mPrograms.keySet()) { + if (mDvrManager.isConflicting( + mDataManager.getScheduledRecordingForProgramId(programId))) { + ++conflicts; + } } + return conflicts == 0 ? null : getContext().getResources().getQuantityString( + R.plurals.dvr_series_schedules_header_description, conflicts, conflicts); } @Override - public void stop() { - SoftPreconditions.checkState(get(0) instanceof SchedulesHeaderRow, TAG, - "First row is not SchedulesHeaderRow"); - boolean cancelAll = size() > 0 && ((SeriesRecordingHeaderRow) get(0)).isCancelAllChecked(); - if (!cancelAll) { - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - for (int i = 0; i < size(); i++) { - if (get(i) instanceof ScheduleRow) { - ScheduleRow scheduleRow = (ScheduleRow) get(i); - if (scheduleRow.isRemoveScheduleChecked()) { - dvrManager.removeScheduledRecording(scheduleRow.getRecording()); - } - } + public long getId(int position) { + Object obj = get(position); + if (obj instanceof EpisodicProgramRow) { + return ((EpisodicProgramRow) obj).getProgram().getId(); + } + if (obj instanceof SeriesRecordingHeaderRow) { + return 0; + } + return super.getId(position); + } + + @Override + public void onScheduledRecordingAdded(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule); + int index = findRowIndexByProgramId(schedule.getProgramId()); + if (index != -1) { + EpisodicProgramRow row = (EpisodicProgramRow) get(index); + if (!row.isStartRecordingRequested()) { + row.setSchedule(schedule); + notifyArrayItemRangeChanged(index, 1); } } } @Override - protected void addScheduleRow(ScheduledRecording recording) { - if (recording != null && recording.getSeriesRecordingId() == mSeriesRecording.getId()) { - int index = 0; - for (; index < size(); index++) { - if (get(index) instanceof ScheduleRow) { - ScheduleRow scheduleRow = (ScheduleRow) get(index); - if (ScheduledRecording.START_TIME_THEN_PRIORITY_COMPARATOR.compare( - scheduleRow.getRecording(), recording) > 0) { - break; + public void onScheduledRecordingRemoved(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); + int index = findRowIndexByProgramId(schedule.getProgramId()); + if (index != -1) { + EpisodicProgramRow row = (EpisodicProgramRow) get(index); + row.setSchedule(null); + notifyArrayItemRangeChanged(index, 1); + } + } + + @Override + public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); + int index = findRowIndexByProgramId(schedule.getProgramId()); + if (index != -1) { + EpisodicProgramRow row = (EpisodicProgramRow) get(index); + if (conflictChange && isStartOrStopRequested()) { + // Delay the conflict update until it gets the response of the start/stop request. + // The purpose is to avoid the intermediate conflict change. + addPendingUpdate(row); + return; + } + if (row.isStopRecordingRequested()) { + // Wait until the recording is finished + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + row.setStopRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); } + row.setSchedule(null); } + } else if (row.isStartRecordingRequested()) { + // When the start recording was requested, we give the highest priority. So it is + // guaranteed that the state will be changed from NOT_STARTED to the other state. + // Update the row with the next state not to show the intermediate state to avoid + // blinking. + if (schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + row.setStartRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); + } + row.setSchedule(schedule); + } + } else if (willBeKept(schedule)) { + row.setSchedule(schedule); + } else { + row.setSchedule(null); } - SoftPreconditions.checkState(get(0) instanceof SchedulesHeaderRow, TAG, - "First row is not SchedulesHeaderRow"); - if (index == 0) { - index++; - } - SchedulesHeaderRow headerRow = (SchedulesHeaderRow) get(0); - headerRow.setItemCount(headerRow.getItemCount() + 1); - ScheduleRow addedRow = new ScheduleRow(recording, headerRow); - add(index, addedRow); - updateHeaderRowDescription(headerRow); + notifyArrayItemRangeChanged(index, 1); } } - @Override - protected void removeScheduleRow(ScheduleRow scheduleRow) { - if (scheduleRow != null) { - remove(scheduleRow); - SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow(); - // Changes the count information of header which the removed row belongs to. - if (headerRow != null) { - headerRow.setItemCount(headerRow.getItemCount() - 1); - if (headerRow.getItemCount() == 0) { - // TODO: Add a emtpy view. - } else if (get(size() - 1) instanceof ScheduleRow) { - updateHeaderRowDescription(headerRow); + public void onSeriesRecordingUpdated(SeriesRecording seriesRecording) { + if (seriesRecording.getId() == mSeriesRecording.getId()) { + mHeaderRow.setSeriesRecording(seriesRecording); + notifyArrayItemRangeChanged(0, 1); + } + } + + private int findRowIndexByProgramId(long programId) { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof EpisodicProgramRow) { + if (((EpisodicProgramRow) item).getProgram().getId() == programId) { + return i; } } } + return -1; } @Override - protected boolean willBeKept(ScheduledRecording recording) { - return super.willBeKept(recording) - || recording.getState() == ScheduledRecording.STATE_RECORDING_CANCELED; + public void notifyArrayItemRangeChanged(int positionStart, int itemCount) { + mHeaderRow.setDescription(getDescription()); + super.notifyArrayItemRangeChanged(0, 1); + super.notifyArrayItemRangeChanged(positionStart, itemCount); } - private void updateHeaderRowDescription(SchedulesHeaderRow headerRow) { - int nextDays = Utils.computeDateDifference(System.currentTimeMillis(), - ((ScheduleRow) get(size() - 1)).getRecording().getStartTimeMs()) + 1; - headerRow.setDescription(getContext().getResources() - .getQuantityString(R.plurals.dvr_series_schedules_header_description, - nextDays, nextDays)); - replace(indexOf(headerRow), headerRow); + @Override + protected void handleUpdateRow(long currentTimeMs) { + for (Iterator<Program> iter = mPrograms.values().iterator(); iter.hasNext(); ) { + Program program = iter.next(); + if (program.getEndTimeUtcMillis() <= currentTimeMs) { + // Remove the old program. + removeItems(findRowIndexByProgramId(program.getId()), 1); + iter.remove(); + } else if (program.getStartTimeUtcMillis() < currentTimeMs) { + // Change the button "START RECORDING" + notifyItemRangeChanged(findRowIndexByProgramId(program.getId()), 1); + } + } + } + + /** + * Should take the current time argument which is the time when the programs are checked in + * handler. + */ + @Override + protected long getNextTimerMs(long currentTimeMs) { + long earliest = Long.MAX_VALUE; + for (Program program : mPrograms.values()) { + if (earliest > program.getStartTimeUtcMillis() + && program.getStartTimeUtcMillis() >= currentTimeMs) { + // Need the button from "CREATE SCHEDULE" to "START RECORDING" + earliest = program.getStartTimeUtcMillis(); + } else if (earliest > program.getEndTimeUtcMillis()) { + // Need to remove the row. + earliest = program.getEndTimeUtcMillis(); + } + } + return earliest; } } diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java index 4f31528c..5d88579a 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java @@ -21,16 +21,16 @@ import android.view.View; import android.view.ViewGroup; import com.android.tv.R; +import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; import com.android.tv.util.Utils; /** * A RowPresenter for series schedule row. */ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { - private boolean mIsCancelAll; + private static final String TAG = "SeriesRowPresenter"; + private boolean mLtr; public SeriesScheduleRowPresenter(Context context) { @@ -40,8 +40,8 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { } public static class SeriesScheduleRowViewHolder extends ScheduleRowViewHolder { - public SeriesScheduleRowViewHolder(View view) { - super(view); + public SeriesScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) { + super(view, presenter); ViewGroup.LayoutParams lp = getTimeView().getLayoutParams(); lp.width = view.getResources().getDimensionPixelSize( R.dimen.dvr_series_schedules_item_time_width); @@ -51,37 +51,29 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { @Override protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) { - return new SeriesScheduleRowViewHolder(view); + return new SeriesScheduleRowViewHolder(view, this); } @Override - protected String onGetRecordingTimeText(ScheduledRecording recording) { - return Utils.getDurationString(getContext(), - recording.getStartTimeMs(), recording.getEndTimeMs(), false, true, true, 0); + protected String onGetRecordingTimeText(ScheduleRow row) { + return Utils.getDurationString(getContext(), row.getStartTimeMs(), row.getEndTimeMs(), + false, true, true, 0); } @Override - protected String onGetProgramInfoText(ScheduledRecording recording) { - if (recording != null) { - return recording.getEpisodeDisplayTitle(getContext()); - } - return null; + protected String onGetProgramInfoText(ScheduleRow row) { + return row.getEpisodeDisplayTitle(getContext()); } @Override - protected void onBindRowViewHolderInternal(ScheduleRowViewHolder viewHolder, - ScheduleRow scheduleRow) { - mIsCancelAll = ((SeriesRecordingHeaderRow) scheduleRow.getHeaderRow()).isCancelAllChecked(); - boolean isConflicting = getConflicts().contains(scheduleRow.getRecording()); - if (mIsCancelAll || isConflicting || scheduleRow.isRemoveScheduleChecked()) { - viewHolder.greyOutInfo(); - } else { - viewHolder.whiteBackInfo(); - } - if (!mIsCancelAll && isConflicting) { + protected void onBindRowViewHolder(ViewHolder vh, Object item) { + super.onBindRowViewHolder(vh, item); + SeriesScheduleRowViewHolder viewHolder = (SeriesScheduleRowViewHolder) vh; + EpisodicProgramRow row = (EpisodicProgramRow) item; + if (getDvrManager().isConflicting(row.getSchedule())) { viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext() .getResources().getDimensionPixelOffset( - R.dimen.dvr_schedules_warning_icon_padding)); + R.dimen.dvr_schedules_warning_icon_padding)); if (mLtr) { viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( R.drawable.ic_warning_gray600_36dp, 0, 0, 0); @@ -92,26 +84,60 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { } else { viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } - if (mIsCancelAll) { - viewHolder.getInfoContainer().setClickable(false); - viewHolder.getDeleteActionContainer().setVisibility(View.GONE); + } + + @Override + protected void onInfoClicked(ScheduleRow row) { + if (row.getSchedule() != null) { + DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); } } @Override - protected void onRowViewSelectedInternal(ViewHolder vh, boolean selected) { - ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; - if (!mIsCancelAll) { - if (selected) { - viewHolder.getDeleteActionContainer().setVisibility(View.VISIBLE); + protected void onStartRecording(ScheduleRow row) { + SoftPreconditions.checkState(row.getSchedule() == null, TAG, + "Start request with the existing schedule: " + row); + row.setStartRecordingRequested(true); + getDvrManager().addScheduleWithHighestPriority(((EpisodicProgramRow) row).getProgram()); + } + + @Override + protected void onStopRecording(ScheduleRow row) { + SoftPreconditions.checkState(row.getSchedule() != null, TAG, + "Stop request with the null schedule: " + row); + row.setStopRecordingRequested(true); + getDvrManager().stopRecording(row.getSchedule()); + } + + @Override + protected void onCreateSchedule(ScheduleRow row) { + if (row.getSchedule() == null) { + getDvrManager().addScheduleWithHighestPriority(((EpisodicProgramRow) row).getProgram()); + } else { + super.onCreateSchedule(row); + } + } + + @Override + @ScheduleRowAction + protected int[] getAvailableActions(ScheduleRow row) { + if (row.getSchedule() == null) { + if (row.isOnAir()) { + return new int[] {ACTION_START_RECORDING}; } else { - viewHolder.getDeleteActionContainer().setVisibility(View.GONE); + return new int[] {ACTION_CREATE_SCHEDULE}; } } + return super.getAvailableActions(row); + } + + @Override + protected boolean canResolveConflict() { + return false; } @Override - protected void onInfoClicked(ScheduleRow scheduleRow) { - DvrUiHelper.startSchedulesActivity(getContext(), scheduleRow.getRecording()); + protected boolean shouldKeepScheduleAfterRemoving() { + return true; } } diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index a6ac4375..120b3dba 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -22,7 +22,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; -import android.animation.ValueAnimator; +import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Point; @@ -42,6 +42,7 @@ import android.view.View.MeasureSpec; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityManager; import com.android.tv.ChannelTuner; import com.android.tv.Features; @@ -56,6 +57,7 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; +import com.android.tv.ui.ViewUtils; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -90,6 +92,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { private final MainActivity mActivity; private final ProgramManager mProgramManager; + private final AccessibilityManager mAccessibilityManager; private final ChannelTuner mChannelTuner; private final Tracker mTracker; private final DurationTimer mVisibleDuration = new DurationTimer(); @@ -374,7 +377,10 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mProgramTableFadeInAnimator.setTarget(mTable); mProgramTableFadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable)); mSharedPreference = PreferenceManager.getDefaultSharedPreferences(mActivity); - mShowGuidePartial = mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true); + mAccessibilityManager = + (AccessibilityManager) mActivity.getSystemService(Context.ACCESSIBILITY_SERVICE); + mShowGuidePartial = mAccessibilityManager.isEnabled() + || mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true); } private void updateGuidePosition() { @@ -606,7 +612,9 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } private void startFull() { - if (isFull()) { + if (isFull() || mAccessibilityManager.isEnabled()) { + // If accessibility service is enabled, focus cannot be moved to side panel due to it's + // hidden. Therefore, we don't hide side panel when accessibility service is enabled. return; } mShowGuidePartial = false; @@ -743,7 +751,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { View detailView = row.findViewById(R.id.detail); detailView.findViewById(R.id.detail_content_full).setAlpha(1); detailView.findViewById(R.id.detail_content_full).setTranslationY(0); - setLayoutHeight(detailView, mDetailHeight); + ViewUtils.setLayoutHeight(detailView, mDetailHeight); detailView.setVisibility(View.VISIBLE); final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row); @@ -785,8 +793,8 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { fadeOutAnimator.setDuration(mAnimationDuration); fadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(outDetailContent)); - Animator collapseAnimator = - createHeightAnimator(outDetail, getLayoutHeight(outDetail), 0); + Animator collapseAnimator = ViewUtils + .createHeightAnimator(outDetail, ViewUtils.getLayoutHeight(outDetail), 0); collapseAnimator.setStartDelay(mAnimationDuration); collapseAnimator.setDuration(mTableFadeAnimDuration); collapseAnimator.addListener(new AnimatorListenerAdapter() { @@ -817,7 +825,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { if (inDetail != null) { final View inDetailContent = inDetail.findViewById(R.id.detail_content_full); - Animator expandAnimator = createHeightAnimator(inDetail, 0, mDetailHeight); + Animator expandAnimator = ViewUtils.createHeightAnimator(inDetail, 0, mDetailHeight); expandAnimator.setStartDelay(mAnimationDuration); expandAnimator.setDuration(mTableFadeAnimDuration); expandAnimator.addListener(new AnimatorListenerAdapter() { @@ -832,17 +840,15 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { inDetailContent.setAlpha(0); } }); - Animator fadeInAnimator = ObjectAnimator.ofPropertyValuesHolder(inDetailContent, PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, direction * -mDetailPadding, 0f)); - fadeInAnimator.setStartDelay(mAnimationDuration + mTableFadeAnimDuration); fadeInAnimator.setDuration(mAnimationDuration); fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent)); AnimatorSet inAnimator = new AnimatorSet(); - inAnimator.playTogether(expandAnimator, fadeInAnimator); + inAnimator.playSequentially(expandAnimator, fadeInAnimator); inAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { @@ -854,41 +860,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } } - private Animator createHeightAnimator( - final View target, int initialHeight, int targetHeight) { - ValueAnimator animator = ValueAnimator.ofInt(initialHeight, targetHeight); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int value = (Integer) animation.getAnimatedValue(); - if (value == 0) { - if (target.getVisibility() != View.GONE) { - target.setVisibility(View.GONE); - } - } else { - if (target.getVisibility() != View.VISIBLE) { - target.setVisibility(View.VISIBLE); - } - setLayoutHeight(target, value); - } - } - }); - return animator; - } - - private int getLayoutHeight(View view) { - LayoutParams layoutParams = view.getLayoutParams(); - return layoutParams.height; - } - - private void setLayoutHeight(View view, int height) { - LayoutParams layoutParams = view.getLayoutParams(); - if (height != layoutParams.height) { - layoutParams.height = height; - view.setLayoutParams(layoutParams); - } - } - private class GlobalFocusChangeListener implements ViewTreeObserver.OnGlobalFocusChangeListener { private static final int UNKNOWN = 0; diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index f638e830..4c7a4404 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -47,9 +47,9 @@ import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.guide.ProgramManager.TableEntry; +import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; -import java.lang.ref.WeakReference; import java.lang.reflect.InvocationTargetException; import java.util.concurrent.TimeUnit; @@ -73,8 +73,6 @@ public class ProgramItemView extends TextView { private static TextAppearanceSpan sEpisodeTitleStyle; private static TextAppearanceSpan sGrayedOutEpisodeTitleStyle; - private static WeakReference<Toast> sToast; - private DvrManager mDvrManager; private TableEntry mTableEntry; private int mMaxWidthForRipple; @@ -96,10 +94,9 @@ public class ProgramItemView extends TextView { ApplicationSingletons singletons = TvApplication.getSingletons(view.getContext()); Tracker tracker = singletons.getTracker(); tracker.sendEpgItemClicked(); + final MainActivity tvActivity = (MainActivity) view.getContext(); + final Channel channel = tvActivity.getChannelDataManager().getChannel(entry.channelId); if (entry.isCurrentProgram()) { - final MainActivity tvActivity = (MainActivity) view.getContext(); - final Channel channel = tvActivity.getChannelDataManager() - .getChannel(entry.channelId); view.postDelayed(new Runnable() { @Override public void run() { @@ -114,42 +111,25 @@ public class ProgramItemView extends TextView { if (entry.entryStartUtcMillis > System.currentTimeMillis() && dvrManager.isProgramRecordable(entry.program)) { if (entry.scheduledRecording == null) { - if (DvrUiHelper.handleCreateSchedule((MainActivity) view.getContext(), - entry.program)) { + if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity, + channel.getInputId()) + && DvrUiHelper.handleCreateSchedule(tvActivity, entry.program)) { String msg = view.getContext().getString( R.string.dvr_msg_program_scheduled, entry.program.getTitle()); - showToast(view.getContext(), msg); + ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT); } - } else if (entry.scheduledRecording.getState() - == ScheduledRecording.STATE_RECORDING_CANCELED) { - // TODO: replace the toast with a dialog. - String msg = view.getResources().getString( - R.string.dvr_msg_program_scheduled, entry.program.getTitle()); - showToast(view.getContext(), msg); - dvrManager.updateScheduledRecording(ScheduledRecording.buildFrom(entry - .scheduledRecording).setState(ScheduledRecording - .STATE_RECORDING_NOT_STARTED).build()); } else { dvrManager.removeScheduledRecording(entry.scheduledRecording); String msg = view.getResources().getString( R.string.dvr_schedules_deletion_info, entry.program.getTitle()); - showToast(view.getContext(), msg); + ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT); } } else { - showToast(view.getContext(), view.getResources() - .getString(R.string.dvr_msg_cannot_record_program)); + ToastUtils.show(view.getContext(), view.getResources() + .getString(R.string.dvr_msg_cannot_record_program), Toast.LENGTH_SHORT); } } } - - private void showToast(Context context, String msg) { - if (sToast != null && sToast.get() != null) { - sToast.get().cancel(); - } - Toast toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT); - toast.show(); - sToast = new WeakReference<>(toast); - } }; private static final View.OnFocusChangeListener ON_FOCUS_CHANGED = @@ -322,7 +302,7 @@ public class ProgramItemView extends TextView { int iconResId = 0; if (mTableEntry.scheduledRecording != null) { if (mDvrManager.isConflicting(mTableEntry.scheduledRecording)) { - iconResId = R.drawable.ic_warning_white_36dp; + iconResId = R.drawable.ic_warning_white_18dp; } else { switch (mTableEntry.scheduledRecording.getState()) { case ScheduledRecording.STATE_RECORDING_NOT_STARTED: diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index 489cc5ef..e3d919df 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -204,17 +204,17 @@ public class ProgramManager { @Override public void onLoadFinished() { mChannelDataLoaded = true; - updateChannels(true, false); + updateChannels(false); } @Override public void onChannelListUpdated() { - updateChannels(true, false); + updateChannels(false); } @Override public void onChannelBrowsableChanged() { - updateChannels(true, false); + updateChannels(false); } }; @@ -222,7 +222,7 @@ public class ProgramManager { new ProgramDataManager.Listener() { @Override public void onProgramUpdated() { - updateTableEntries(true, true); + updateTableEntries(true); } }; @@ -434,18 +434,16 @@ public class ProgramManager { // Note that This can be happens only if program guide isn't shown // because an user has to select channels as browsable through UI. - private void updateChannels(boolean notify, boolean clearPreviousTableEntries) { + private void updateChannels(boolean clearPreviousTableEntries) { if (DEBUG) Log.d(TAG, "updateChannels"); mChannels = mChannelDataManager.getBrowsableChannelList(); mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; mFilteredChannels = mChannels; - if (notify) { - notifyChannelsUpdated(); - } - updateTableEntries(notify, clearPreviousTableEntries); + notifyChannelsUpdated(); + updateTableEntries(clearPreviousTableEntries); } - private void updateTableEntries(boolean notify, boolean clear) { + private void updateTableEntries(boolean clear) { if (clear) { mChannelIdEntriesMap.clear(); } @@ -494,9 +492,7 @@ public class ProgramManager { } } - if (notify) { - notifyTableEntriesUpdated(); - } + notifyTableEntriesUpdated(); buildGenreFilters(); } @@ -579,7 +575,7 @@ public class ProgramManager { } mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis); - updateChannels(true, true); + updateChannels(true); setTimeRange(startUtcMillis, endUtcMillis); } diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java index 5c4236a6..2c98ab2d 100644 --- a/src/com/android/tv/guide/ProgramRow.java +++ b/src/com/android/tv/guide/ProgramRow.java @@ -22,8 +22,7 @@ import android.support.v7.widget.LinearLayoutManager; import android.util.AttributeSet; import android.util.Log; import android.view.View; -import android.view.ViewParent; -import android.view.ViewTreeObserver; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; import com.android.tv.data.Channel; import com.android.tv.guide.ProgramManager.TableEntry; @@ -45,25 +44,26 @@ public class ProgramRow extends TimelineGridView { interface ChildFocusListener { /** - * Is called after focus is moved. Only children to {@code ProgramRow} will be passed. + * Is called after focus is moved. It used {@link ChildFocusListener#isChild} to decide if + * old and new focuses are listener's children. * See {@code ProgramRow#setChildFocusListener(ChildFocusListener)}. */ void onChildFocus(View oldFocus, View newFocus); } - private final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener = - new ViewTreeObserver.OnGlobalFocusChangeListener() { - @Override - public void onGlobalFocusChanged(View oldFocus, View newFocus) { - updateCurrentFocus(oldFocus, newFocus); - } - }; - /** * Used only for debugging. */ private Channel mChannel; + private final OnGlobalLayoutListener mLayoutListener = new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + updateChildVisibleArea(); + } + }; + public ProgramRow(Context context) { this(context, null); } @@ -94,19 +94,15 @@ public class ProgramRow extends TimelineGridView { @Override public void onScrolled(int dx, int dy) { + // Remove callback to prevent updateChildVisibleArea being called twice. + getViewTreeObserver().removeOnGlobalLayoutListener(mLayoutListener); super.onScrolled(dx, dy); - int childCount = getChildCount(); if (DEBUG) { Log.d(TAG, "onScrolled by " + dx); - Log.d(TAG, "channelId=" + mChannel.getId() + ", childCount=" + childCount); + Log.d(TAG, "channelId=" + mChannel.getId() + ", childCount=" + getChildCount()); Log.d(TAG, "ProgramRow {" + Utils.toRectString(this) + "}"); } - for (int i = 0; i < childCount; ++i) { - ProgramItemView child = (ProgramItemView) getChildAt(i); - if (getLeft() <= child.getRight() && child.getLeft() <= getRight()) { - child.updateVisibleArea(); - } - } + updateChildVisibleArea(); } /** @@ -117,29 +113,9 @@ public class ProgramRow extends TimelineGridView { if (currentProgram == null) { currentProgram = getChildAt(0); } - updateCurrentFocus(null, currentProgram); - } - - private void updateCurrentFocus(View oldFocus, View newFocus) { - if (mChildFocusListener == null) { - return; - } - - mChildFocusListener.onChildFocus(isChild(oldFocus) ? oldFocus : null, - isChild(newFocus) ? newFocus : null); - } - - private boolean isChild(View view) { - if (view == null) { - return false; - } - - for (ViewParent p = view.getParent(); p != null; p = p.getParent()) { - if (p == this) { - return true; - } + if (mChildFocusListener != null) { + mChildFocusListener.onChildFocus(null, currentProgram); } - return false; } // Call this API after RTL is resolved. (i.e. View is measured.) @@ -216,23 +192,21 @@ public class ProgramRow extends TimelineGridView { } @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - getViewTreeObserver().addOnGlobalFocusChangeListener(mGlobalFocusChangeListener); - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - getViewTreeObserver().removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener); - } - - @Override public void onChildDetachedFromWindow(View child) { if (child.hasFocus()) { // Focused view can be detached only if it's updated. TableEntry entry = ((ProgramItemView) child).getTableEntry(); - if (entry.isCurrentProgram()) { + if (entry.program == null) { + // The focus is lost due to information loaded. Requests focus immediately. + // (Because this entry is detached after real entries attached, we can't take + // the below approach to resume focus on entry being attached.) + post(new Runnable() { + @Override + public void run() { + requestFocus(); + } + }); + } else if (entry.isCurrentProgram()) { if (DEBUG) Log.d(TAG, "Keep focus to the current program"); // Current program is visible in the guide. // Updated entries including current program's will be attached again soon @@ -250,13 +224,13 @@ public class ProgramRow extends TimelineGridView { if (mKeepFocusToCurrentProgram) { TableEntry entry = ((ProgramItemView) child).getTableEntry(); if (entry.isCurrentProgram()) { + mKeepFocusToCurrentProgram = false; post(new Runnable() { @Override public void run() { requestFocus(); } }); - mKeepFocusToCurrentProgram = false; } } } @@ -324,6 +298,22 @@ public class ProgramRow extends TimelineGridView { mProgramManager.getStartTime(), entry.entryStartUtcMillis) - scrollOffset; ((LinearLayoutManager) getLayoutManager()) .scrollToPositionWithOffset(position, offset); + // Workaround to b/31598505. When a program's duration is too long, + // RecyclerView.onScrolled() will not be called after scrollToPositionWithOffset(). + // Therefore we have to update children's visible areas by ourselves in theis case. + // Since scrollToPositionWithOffset() will call requestLayout(), we can listen to this + // behavior to ensure program items' visible areas are correctly updated after layouts + // are adjusted, i.e., scrolling is over. + getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener); + } + } + + private void updateChildVisibleArea() { + for (int i = 0; i < getChildCount(); ++i) { + ProgramItemView child = (ProgramItemView) getChildAt(i); + if (getLeft() < child.getRight() && child.getLeft() < getRight()) { + child.updateVisibleArea(); + } } } } diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java index fd561992..e4a67972 100644 --- a/src/com/android/tv/guide/ProgramTableAdapter.java +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -17,6 +17,7 @@ package com.android.tv.guide; import static com.android.tv.util.ImageLoader.ImageLoaderCallback; + import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; @@ -42,6 +43,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityManager; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -50,10 +53,10 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; -import com.android.tv.data.Program.CriticScore; import com.android.tv.data.Program; -import com.android.tv.dvr.DvrManager; +import com.android.tv.data.Program.CriticScore; import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ScheduledRecording; import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; import com.android.tv.parental.ParentalControlSettings; @@ -80,6 +83,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte private final DvrManager mDvrManager; private final DvrDataManager mDvrDataManager; private final ProgramManager mProgramManager; + private final AccessibilityManager mAccessibilityManager; private final ProgramGuide mProgramGuide; private final Handler mHandler = new Handler(); private final List<ProgramListAdapter> mProgramListAdapters = new ArrayList<>(); @@ -103,6 +107,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte private final String mProgramRecordableText; private final String mRecordingScheduledText; private final String mRecordingConflictText; + private final String mRecordingFailedText; private final String mRecordingInProgressText; private final int mDvrPaddingStartWithTrack; private final int mDvrPaddingStartWithOutTrack; @@ -110,6 +115,8 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte public ProgramTableAdapter(Context context, ProgramManager programManager, ProgramGuide programGuide) { mContext = context; + mAccessibilityManager = + (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper(); if (CommonFeatures.DVR.isEnabled(context)) { mDvrManager = TvApplication.getSingletons(context).getDvrManager(); @@ -149,6 +156,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mProgramRecordableText = res.getString(R.string.dvr_epg_program_recordable); mRecordingScheduledText = res.getString(R.string.dvr_epg_program_recording_scheduled); mRecordingConflictText = res.getString(R.string.dvr_epg_program_recording_conflict); + mRecordingFailedText = res.getString(R.string.dvr_epg_program_recording_failed); mRecordingInProgressText = res.getString(R.string.dvr_epg_program_recording_in_progress); mDvrPaddingStartWithTrack = res.getDimensionPixelOffset( R.dimen.program_guide_table_detail_dvr_margin_start); @@ -162,7 +170,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mEpisodeTitleStyle = new TextAppearanceSpan(null, 0, episodeTitleSize, episodeTitleColor, null); - mCriticScoreViews = new ArrayList<LinearLayout>(); + mCriticScoreViews = new ArrayList<>(); mRecycledViewPool = new RecycledViewPool(); mRecycledViewPool.setMaxRecycledViews(R.layout.program_guide_table_item, context.getResources().getInteger( @@ -170,12 +178,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mProgramManager.addListener(new ProgramManager.ListenerAdapter() { @Override public void onChannelsUpdated() { - mHandler.post(new Runnable() { - @Override - public void run() { - update(); - } - }); + update(); } }); update(); @@ -238,6 +241,16 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte notifyItemChanged(channelIndex, true); } + @Override + public void onViewAttachedToWindow(ProgramRowHolder holder) { + holder.onAttachedToWindow(); + } + + @Override + public void onViewDetachedFromWindow(ProgramRowHolder holder) { + holder.onDetachedFromWindow(); + } + // TODO: make it static public class ProgramRowHolder extends RecyclerView.ViewHolder implements ProgramRow.ChildFocusListener { @@ -265,6 +278,15 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte } }; + private final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener = + new ViewTreeObserver.OnGlobalFocusChangeListener() { + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + onChildFocus(isChild(oldFocus) ? oldFocus : null, + isChild(newFocus) ? newFocus : null); + } + }; + // Members of Program Details private final ViewGroup mDetailView; private final ImageView mImageView; @@ -317,6 +339,16 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo); mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block); mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo); + mDetailView.setFocusable(mAccessibilityManager.isEnabled()); + mChannelHeaderView.setFocusable(mAccessibilityManager.isEnabled()); + mAccessibilityManager.addAccessibilityStateChangeListener( + new AccessibilityManager.AccessibilityStateChangeListener() { + @Override + public void onAccessibilityStateChanged(boolean enable) { + mDetailView.setFocusable(enable); + mChannelHeaderView.setFocusable(enable); + } + }); } public void onBind(int position) { @@ -384,12 +416,32 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte } } + public boolean isChild(View view) { + if (view == null) { + return false; + } + for (ViewParent p = view.getParent(); p != null; p = p.getParent()) { + if (p == mContainer) { + return true; + } + } + return false; + } + @Override public void onChildFocus(View oldFocus, View newFocus) { if (newFocus == null) { return; } - mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry(); + // When the accessibility service is enabled, focus might be put on channel's header or + // detail view, besides program items. + if (newFocus == mChannelHeaderView) { + mSelectedEntry = ((ProgramItemView) mProgramRow.getChildAt(0)).getTableEntry(); + } else if (newFocus == mDetailView) { + return; + } else { + mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry(); + } if (oldFocus == null) { updateDetailView(); return; @@ -456,7 +508,21 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte }); } + private void onAttachedToWindow() { + mContainer.getViewTreeObserver() + .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener); + } + + private void onDetachedFromWindow() { + mContainer.getViewTreeObserver() + .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener); + } + private void updateDetailView() { + if (mSelectedEntry == null) { + // The view holder is never on focus before. + return; + } if (DEBUG) Log.d(TAG, "updateDetailView"); mCriticScoresLayout.removeAllViews(); if (Program.isValid(mSelectedEntry.program)) { @@ -508,7 +574,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte int iconResId = 0; if (scheduledRecording != null) { if (mDvrManager.isConflicting(scheduledRecording)) { - iconResId = R.drawable.ic_warning_white_24dp; + iconResId = R.drawable.ic_warning_white_12dp; statusText = mRecordingConflictText; } else { switch (scheduledRecording.getState()) { @@ -521,8 +587,8 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte statusText = mRecordingScheduledText; break; case ScheduledRecording.STATE_RECORDING_FAILED: - iconResId = R.drawable.ic_warning_white_24dp; - statusText = mRecordingConflictText; + iconResId = R.drawable.ic_warning_white_12dp; + statusText = mRecordingFailedText; break; default: iconResId = 0; diff --git a/src/com/android/tv/menu/ActionCardView.java b/src/com/android/tv/menu/ActionCardView.java index 2d72b06f..54892cac 100644 --- a/src/com/android/tv/menu/ActionCardView.java +++ b/src/com/android/tv/menu/ActionCardView.java @@ -69,11 +69,13 @@ public class ActionCardView extends FrameLayout implements ItemListRowView.CardV mStateView.setText(action.getActionDescription(getContext())); if (action.isEnabled()) { setEnabled(true); + setFocusable(true); mIconView.setAlpha(OPACITY_ENABLED); mLabelView.setAlpha(OPACITY_ENABLED); mStateView.setAlpha(OPACITY_ENABLED); } else { setEnabled(false); + setFocusable(false); mIconView.setAlpha(OPACITY_DISABLED); mLabelView.setAlpha(OPACITY_DISABLED); mStateView.setAlpha(OPACITY_DISABLED); diff --git a/src/com/android/tv/menu/AppLinkCardView.java b/src/com/android/tv/menu/AppLinkCardView.java index a71b1a61..bfb5e3f1 100644 --- a/src/com/android/tv/menu/AppLinkCardView.java +++ b/src/com/android/tv/menu/AppLinkCardView.java @@ -30,7 +30,6 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.View; -import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -48,10 +47,6 @@ public class AppLinkCardView extends BaseCardView<Channel> { private static final String TAG = MenuView.TAG; private static final boolean DEBUG = MenuView.DEBUG; - private final float mCardHeight; - private final float mExtendedCardHeight; - private final float mTextViewHeight; - private final float mExtendedTextViewCardHeight; private final int mCardImageWidth; private final int mCardImageHeight; private final int mIconWidth; @@ -62,12 +57,9 @@ public class AppLinkCardView extends BaseCardView<Channel> { private ImageView mImageView; private View mGradientView; private TextView mAppInfoView; - private TextView mMetaViewFocused; - private TextView mMetaViewUnfocused; private View mMetaViewHolder; private Channel mChannel; private Intent mIntent; - private boolean mExtendViewOnFocus; private final PackageManager mPackageManager; private final TvInputManagerHelper mTvInputManagerHelper; @@ -84,18 +76,11 @@ public class AppLinkCardView extends BaseCardView<Channel> { mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width); mCardImageHeight = getResources().getDimensionPixelSize(R.dimen.card_image_layout_height); - mCardHeight = getResources().getDimensionPixelSize(R.dimen.card_layout_height); - mExtendedCardHeight = getResources().getDimensionPixelOffset( - R.dimen.card_layout_height_extended); mIconWidth = getResources().getDimensionPixelSize(R.dimen.app_link_card_icon_width); mIconHeight = getResources().getDimensionPixelSize(R.dimen.app_link_card_icon_height); mIconPadding = getResources().getDimensionPixelOffset(R.dimen.app_link_card_icon_padding); mPackageManager = context.getPackageManager(); mTvInputManagerHelper = ((MainActivity) context).getTvInputManagerHelper(); - mTextViewHeight = getResources().getDimensionPixelSize( - R.dimen.card_meta_layout_height); - mExtendedTextViewCardHeight = getResources().getDimensionPixelOffset( - R.dimen.card_meta_layout_height_extended); mIconColorFilter = getResources().getColor(R.color.app_link_card_icon_color_filter, null); } @@ -119,7 +104,7 @@ public class AppLinkCardView extends BaseCardView<Channel> { switch (linkType) { case Channel.APP_LINK_TYPE_CHANNEL: - setMetaViewText(mChannel.getAppLinkText()); + setText(mChannel.getAppLinkText()); mAppInfoView.setVisibility(VISIBLE); mGradientView.setVisibility(VISIBLE); mAppInfoView.setCompoundDrawablePadding(mIconPadding); @@ -137,7 +122,7 @@ public class AppLinkCardView extends BaseCardView<Channel> { } break; case Channel.APP_LINK_TYPE_APP: - setMetaViewText(getContext().getString( + setText(getContext().getString( R.string.channels_item_app_link_app_launcher, mPackageManager.getApplicationLabel(appInfo))); mAppInfoView.setVisibility(GONE); @@ -163,17 +148,8 @@ public class AppLinkCardView extends BaseCardView<Channel> { } else { setCardImageWithBanner(appInfo); } - - mMetaViewFocused.measure(MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mExtendViewOnFocus = mMetaViewFocused.getLineCount() > 1; - if (mExtendViewOnFocus) { - setMetaViewFocusedAlpha(selected ? 1f : 0f); - } else { - setMetaViewFocusedAlpha(1f); - } - - // Call super.onBind() at the end in order to make getCardHeight() return a proper value. + // Call super.onBind() at the end intentionally. In order to correctly handle extension of + // text view, text should be set before calling super.onBind. super.onBind(channel, selected); } @@ -227,32 +203,6 @@ public class AppLinkCardView extends BaseCardView<Channel> { mGradientView = findViewById(R.id.image_gradient); mAppInfoView = (TextView) findViewById(R.id.app_info); mMetaViewHolder = findViewById(R.id.app_link_text_holder); - mMetaViewFocused = (TextView) findViewById(R.id.app_link_text_focused); - mMetaViewUnfocused = (TextView) findViewById(R.id.app_link_text_unfocused); - } - - @Override - protected void onFocusAnimationStart(boolean selected) { - if (mExtendViewOnFocus) { - setMetaViewFocusedAlpha(selected ? 1f : 0f); - } - } - - @Override - protected void onSetFocusAnimatedValue(float animatedValue) { - super.onSetFocusAnimatedValue(animatedValue); - if (mExtendViewOnFocus) { - ViewGroup.LayoutParams params = mMetaViewUnfocused.getLayoutParams(); - params.height = Math.round(mTextViewHeight - + (mExtendedTextViewCardHeight - mTextViewHeight) * animatedValue); - setMetaViewLayoutParams(params); - setMetaViewFocusedAlpha(animatedValue); - } - } - - @Override - protected float getCardHeight() { - return (mExtendViewOnFocus && isFocused()) ? mExtendedCardHeight : mCardHeight; } // Try to set the card image with following order: @@ -305,19 +255,4 @@ public class AppLinkCardView extends BaseCardView<Channel> { } }); } - - private void setMetaViewLayoutParams(ViewGroup.LayoutParams params) { - mMetaViewFocused.setLayoutParams(params); - mMetaViewUnfocused.setLayoutParams(params); - } - - private void setMetaViewText(String text) { - mMetaViewFocused.setText(text); - mMetaViewUnfocused.setText(text); - } - - private void setMetaViewFocusedAlpha(float focusedAlpha) { - mMetaViewFocused.setAlpha(focusedAlpha); - mMetaViewUnfocused.setAlpha(1f - focusedAlpha); - } } diff --git a/src/com/android/tv/menu/BaseCardView.java b/src/com/android/tv/menu/BaseCardView.java index b4500dd1..c6a34a5d 100644 --- a/src/com/android/tv/menu/BaseCardView.java +++ b/src/com/android/tv/menu/BaseCardView.java @@ -21,10 +21,13 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Outline; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.widget.LinearLayout; +import android.widget.TextView; import com.android.tv.R; @@ -44,6 +47,16 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo private final float mVerticalCardMargin; private final float mCardCornerRadius; private float mFocusAnimatedValue; + private boolean mExtendViewOnFocus; + private final float mExtendedCardHeight; + private final float mTextViewHeight; + private final float mExtendedTextViewHeight; + @Nullable + private TextView mTextView; + @Nullable + private TextView mTextViewFocused; + private final int mCardImageWidth; + private final float mCardHeight; public BaseCardView(Context context) { this(context, null); @@ -72,16 +85,42 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCardCornerRadius); } }); + mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width); + mCardHeight = getResources().getDimensionPixelSize(R.dimen.card_layout_height); + mExtendedCardHeight = getResources().getDimensionPixelSize( + R.dimen.card_layout_height_extended); + mTextViewHeight = getResources().getDimensionPixelSize(R.dimen.card_meta_layout_height); + mExtendedTextViewHeight = getResources().getDimensionPixelOffset( + R.dimen.card_meta_layout_height_extended); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mTextView = (TextView) findViewById(R.id.card_text); + mTextViewFocused = (TextView) findViewById(R.id.card_text_focused); } /** * Called when the view is displayed. + * + * Before onBind is called, this view's text should be set to determine if it'll be extended + * or not in focus state. */ @Override public void onBind(T item, boolean selected) { - // Note that getCardHeight() will be called by setFocusAnimatedValue(). - // Therefore, be sure that getCardHeight() has a proper value before this method is called. - setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); + if (mTextView != null && mTextViewFocused != null) { + mTextViewFocused.measure( + MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + mExtendViewOnFocus = mTextViewFocused.getLineCount() > 1; + if (mExtendViewOnFocus) { + setTextViewFocusedAlpha(selected ? 1f : 0f); + } else { + setTextViewFocusedAlpha(1f); + } + } + setFocusAnimatedValue(selected ? SCALE_FACTOR_1F : SCALE_FACTOR_0F); } @Override @@ -108,10 +147,48 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo } /** + * Sets text of this card view. + */ + public void setText(int resId) { + if (mTextViewFocused != null) { + mTextViewFocused.setText(resId); + } + if (mTextView != null) { + mTextView.setText(resId); + } + } + + /** + * Sets text of this card view. + */ + public void setText(String text) { + if (mTextViewFocused != null) { + mTextViewFocused.setText(text); + } + if (mTextView != null) { + mTextView.setText(text); + } + } + + /** + * Enables or disables text view of this card view. + */ + public void setTextViewEnabled(boolean enabled) { + if (mTextViewFocused != null) { + mTextViewFocused.setEnabled(enabled); + } + if (mTextView != null) { + mTextView.setEnabled(enabled); + } + } + + /** * Called when the focus animation started. */ protected void onFocusAnimationStart(boolean selected) { - // do nothing. + if (mExtendViewOnFocus) { + setTextViewFocusedAlpha(selected ? 1f : 0f); + } } /** @@ -126,10 +203,19 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo * between {@code SCALE_FACTOR_0F} and {@code SCALE_FACTOR_1F}. */ protected void onSetFocusAnimatedValue(float animatedValue) { - float scale = 1f + (mVerticalCardMargin / getCardHeight()) * animatedValue; + float cardViewHeight = (mExtendViewOnFocus && isFocused()) + ? mExtendedCardHeight : mCardHeight; + float scale = 1f + (mVerticalCardMargin / cardViewHeight) * animatedValue; setScaleX(scale); setScaleY(scale); setTranslationZ(mFocusTranslationZ * animatedValue); + if (mExtendViewOnFocus) { + ViewGroup.LayoutParams params = mTextView.getLayoutParams(); + params.height = Math.round(mTextViewHeight + + (mExtendedTextViewHeight - mTextViewHeight) * animatedValue); + setTextViewLayoutParams(params); + setTextViewFocusedAlpha(animatedValue); + } } private void setFocusAnimatedValue(float animatedValue) { @@ -171,8 +257,13 @@ public abstract class BaseCardView<T> extends LinearLayout implements ItemListRo } } - /** - * The implementation should return the height of the card. - */ - protected abstract float getCardHeight(); + private void setTextViewLayoutParams(ViewGroup.LayoutParams params) { + mTextViewFocused.setLayoutParams(params); + mTextView.setLayoutParams(params); + } + + private void setTextViewFocusedAlpha(float focusedAlpha) { + mTextViewFocused.setAlpha(focusedAlpha); + mTextView.setAlpha(1f - focusedAlpha); + } } diff --git a/src/com/android/tv/menu/ChannelCardView.java b/src/com/android/tv/menu/ChannelCardView.java index 4e29a5a9..1c8015a6 100644 --- a/src/com/android/tv/menu/ChannelCardView.java +++ b/src/com/android/tv/menu/ChannelCardView.java @@ -23,7 +23,6 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.View; -import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; @@ -42,10 +41,6 @@ public class ChannelCardView extends BaseCardView<Channel> { private static final String TAG = MenuView.TAG; private static final boolean DEBUG = MenuView.DEBUG; - private final float mCardHeight; - private final float mExtendedCardHeight; - private final float mProgramNameViewHeight; - private final float mExtendedTextViewCardHeight; private final int mCardImageWidth; private final int mCardImageHeight; @@ -53,11 +48,8 @@ public class ChannelCardView extends BaseCardView<Channel> { private View mGradientView; private TextView mChannelNumberNameView; private ProgressBar mProgressBar; - private TextView mMetaViewFocused; - private TextView mMetaViewUnfocused; private Channel mChannel; private Program mProgram; - private boolean mExtendViewOnFocus; private final MainActivity mMainActivity; public ChannelCardView(Context context) { @@ -70,17 +62,8 @@ public class ChannelCardView extends BaseCardView<Channel> { public ChannelCardView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mCardImageWidth = getResources().getDimensionPixelSize(R.dimen.card_image_layout_width); mCardImageHeight = getResources().getDimensionPixelSize(R.dimen.card_image_layout_height); - mCardHeight = getResources().getDimensionPixelSize(R.dimen.card_layout_height); - mExtendedCardHeight = getResources().getDimensionPixelSize( - R.dimen.card_layout_height_extended); - mProgramNameViewHeight = getResources().getDimensionPixelSize( - R.dimen.card_meta_layout_height); - mExtendedTextViewCardHeight = getResources().getDimensionPixelOffset( - R.dimen.card_meta_layout_height_extended); - mMainActivity = (MainActivity) context; } @@ -90,8 +73,6 @@ public class ChannelCardView extends BaseCardView<Channel> { mImageView = (ImageView) findViewById(R.id.image); mGradientView = findViewById(R.id.image_gradient); mChannelNumberNameView = (TextView) findViewById(R.id.channel_number_and_name); - mMetaViewFocused = (TextView) findViewById(R.id.channel_title_focused); - mMetaViewUnfocused = (TextView) findViewById(R.id.channel_title_unfocused); mProgressBar = (ProgressBar) findViewById(R.id.progress); } @@ -110,26 +91,18 @@ public class ChannelCardView extends BaseCardView<Channel> { mGradientView.setVisibility(View.GONE); mProgressBar.setVisibility(GONE); - setMetaViewEnabled(true); + setTextViewEnabled(true); if (mMainActivity.getParentalControlSettings().isParentalControlsEnabled() && mChannel.isLocked()) { - setMetaViewText(R.string.program_title_for_blocked_channel); + setText(R.string.program_title_for_blocked_channel); return; } else { - setMetaViewText(""); + setText(""); } updateProgramInformation(); - mMetaViewFocused.measure( - MeasureSpec.makeMeasureSpec(mCardImageWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - if (mExtendViewOnFocus = mMetaViewFocused.getLineCount() > 1) { - setMetaViewFocusedAlpha(selected ? 1f : 0f); - } else { - setMetaViewFocusedAlpha(1f); - } - - // Call super.onBind() at the end in order to make getCardHeight() return a proper value. + // Call super.onBind() at the end intentionally. In order to correctly handle extension of + // text view, text should be set before calling super.onBind. super.onBind(channel, selected); } @@ -153,40 +126,16 @@ public class ChannelCardView extends BaseCardView<Channel> { mGradientView.setVisibility(View.VISIBLE); } - @Override - protected void onFocusAnimationStart(boolean selected) { - if (mExtendViewOnFocus) { - setMetaViewFocusedAlpha(selected ? 1f : 0f); - } - } - - @Override - protected void onSetFocusAnimatedValue(float animatedValue) { - super.onSetFocusAnimatedValue(animatedValue); - if (mExtendViewOnFocus) { - ViewGroup.LayoutParams params = mMetaViewUnfocused.getLayoutParams(); - params.height = Math.round(mProgramNameViewHeight - + (mExtendedTextViewCardHeight - mProgramNameViewHeight) * animatedValue); - setMetaViewLayoutParams(params); - setMetaViewFocusedAlpha(animatedValue); - } - } - - @Override - protected float getCardHeight() { - return (mExtendViewOnFocus && isFocused()) ? mExtendedCardHeight : mCardHeight; - } - private void updateProgramInformation() { if (mChannel == null) { return; } mProgram = mMainActivity.getProgramDataManager().getCurrentProgram(mChannel.getId()); if (mProgram == null || TextUtils.isEmpty(mProgram.getTitle())) { - setMetaViewEnabled(false); - setMetaViewText(R.string.program_title_for_no_information); + setTextViewEnabled(false); + setText(R.string.program_title_for_no_information); } else { - setMetaViewText(mProgram.getTitle()); + setText(mProgram.getTitle()); } if (mProgram == null) { @@ -217,29 +166,4 @@ public class ChannelCardView extends BaseCardView<Channel> { createProgramPosterArtCallback(this, mProgram)); } } - - private void setMetaViewLayoutParams(ViewGroup.LayoutParams params) { - mMetaViewFocused.setLayoutParams(params); - mMetaViewUnfocused.setLayoutParams(params); - } - - private void setMetaViewText(String text) { - mMetaViewFocused.setText(text); - mMetaViewUnfocused.setText(text); - } - - private void setMetaViewText(int resId) { - mMetaViewFocused.setText(resId); - mMetaViewUnfocused.setText(resId); - } - - private void setMetaViewEnabled(boolean enabled) { - mMetaViewFocused.setEnabled(enabled); - mMetaViewUnfocused.setEnabled(enabled); - } - - private void setMetaViewFocusedAlpha(float focusedAlpha) { - mMetaViewFocused.setAlpha(focusedAlpha); - mMetaViewUnfocused.setAlpha(1f - focusedAlpha); - } } diff --git a/src/com/android/tv/menu/ChannelsRowAdapter.java b/src/com/android/tv/menu/ChannelsRowAdapter.java index 47c413a3..c8e1bd05 100644 --- a/src/com/android/tv/menu/ChannelsRowAdapter.java +++ b/src/com/android/tv/menu/ChannelsRowAdapter.java @@ -141,6 +141,8 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> viewHolder.itemView.setOnClickListener(mAppLinkOnClickListener); } else if (viewType == R.layout.menu_card_dvr) { viewHolder.itemView.setOnClickListener(mDvrOnClickListener); + SimpleCardView view = (SimpleCardView) viewHolder.itemView; + view.setText(R.string.channels_item_dvr); } else { viewHolder.itemView.setTag(getItemList().get(position)); viewHolder.itemView.setOnClickListener(mChannelOnClickListener); @@ -163,10 +165,7 @@ public class ChannelsRowAdapter extends ItemListRowView.ItemListAdapter<Channel> // Sometimes applicationInfo can be null. b/28932537 && inputManager.getTvInputAppInfo(currentChannel.getInputId()) != null; boolean showDvrCard = false; - if (mDvrDataManager != null && !(mDvrDataManager.getRecordedPrograms().isEmpty() - && mDvrDataManager.getStartedRecordings().isEmpty() - && mDvrDataManager.getNonStartedScheduledRecordings().isEmpty() - && mDvrDataManager.getSeriesRecordings().isEmpty())) { + if (mDvrDataManager != null) { for (TvInputInfo info : inputManager.getTvInputInfos(true, true)) { if (info.canRecord()) { showDvrCard = true; diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java index 0da10ff8..1160a5b5 100644 --- a/src/com/android/tv/menu/Menu.java +++ b/src/com/android/tv/menu/Menu.java @@ -179,7 +179,9 @@ public class Menu { mMenuView.onShow(reason, rowIdToSelect, mAnimationDisabledForTest ? null : new Runnable() { @Override public void run() { - mShowAnimator.start(); + if (isActive()) { + mShowAnimator.start(); + } } }); scheduleHide(); @@ -189,6 +191,9 @@ public class Menu { * Closes the menu. */ public void hide(boolean withAnimation) { + if (mShowAnimator.isStarted()) { + mShowAnimator.cancel(); + } if (!isActive()) { return; } diff --git a/src/com/android/tv/menu/MenuRowView.java b/src/com/android/tv/menu/MenuRowView.java index 7cdbfe9e..97dea29a 100644 --- a/src/com/android/tv/menu/MenuRowView.java +++ b/src/com/android/tv/menu/MenuRowView.java @@ -35,25 +35,6 @@ public abstract class MenuRowView extends LinearLayout { private static final String TAG = "MenuRowView"; private static final boolean DEBUG = false; - /** - * For setting ListView visible, and TitleView visible with the selected text size and color - * without animation. - */ - public static final int ANIM_NONE_SELECTED = 1; - /** - * For setting ListView gone, and TitleView visible with the deselected text size and color - * without animation. - */ - public static final int ANIM_NONE_DESELECTED = 2; - /** - * An animation for the selected item list view. - */ - public static final int ANIM_SELECTED = 3; - /** - * An animation for the deselected item list view. - */ - public static final int ANIM_DESELECTED = 4; - private TextView mTitleView; private View mContentsView; diff --git a/src/com/android/tv/menu/MenuUpdater.java b/src/com/android/tv/menu/MenuUpdater.java index 23f0373f..075b299e 100644 --- a/src/com/android/tv/menu/MenuUpdater.java +++ b/src/com/android/tv/menu/MenuUpdater.java @@ -20,16 +20,7 @@ import android.content.Context; import android.support.annotation.Nullable; import com.android.tv.ChannelTuner; -import com.android.tv.TvApplication; -import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.Channel; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; -import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; -import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; -import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.ScheduledRecording; import com.android.tv.ui.TunableTvView; import com.android.tv.ui.TunableTvView.OnScreenBlockingChangedListener; @@ -43,72 +34,8 @@ public class MenuUpdater { @Nullable private final TunableTvView mTvView; private final Menu mMenu; - @Nullable - private final DvrDataManager mDvrDataManager; private ChannelTuner mChannelTuner; - private final OnScreenBlockingChangedListener mOnScreenBlockingChangeListener = - new OnScreenBlockingChangedListener() { - @Override - public void onScreenBlockingChanged(boolean blocked) { - mMenu.update(PlayControlsRow.ID); - } - }; - private final OnRecordedProgramLoadFinishedListener mRecordedProgramLoadedListener = - new OnRecordedProgramLoadFinishedListener() { - @Override - public void onRecordedProgramLoadFinished() { - mMenu.update(ChannelsRow.ID); - } - }; - private final RecordedProgramListener mRecordedProgramListener = - new RecordedProgramListener() { - @Override - public void onRecordedProgramAdded(RecordedProgram recordedProgram) { - mMenu.update(ChannelsRow.ID); - } - - @Override - public void onRecordedProgramChanged(RecordedProgram recordedProgram) { } - - @Override - public void onRecordedProgramRemoved(RecordedProgram recordedProgram) { - if (mDvrDataManager != null && mDvrDataManager.getRecordedPrograms().isEmpty() - && mDvrDataManager.getStartedRecordings().isEmpty() - && mDvrDataManager.getNonStartedScheduledRecordings().isEmpty() - && mDvrDataManager.getSeriesRecordings().isEmpty()) { - mMenu.update(ChannelsRow.ID); - } - } - }; - private final OnDvrScheduleLoadFinishedListener mDvrScheduleLoadedListener = - new OnDvrScheduleLoadFinishedListener() { - @Override - public void onDvrScheduleLoadFinished() { - mMenu.update(ChannelsRow.ID); - } - }; - private final ScheduledRecordingListener mScheduledRecordingListener = - new ScheduledRecordingListener() { - @Override - public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { - mMenu.update(ChannelsRow.ID); - } - - @Override - public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { - if (mDvrDataManager != null && mDvrDataManager.getRecordedPrograms().isEmpty() - && mDvrDataManager.getStartedRecordings().isEmpty() - && mDvrDataManager.getNonStartedScheduledRecordings().isEmpty() - && mDvrDataManager.getSeriesRecordings().isEmpty()) { - mMenu.update(ChannelsRow.ID); - } - } - - @Override - public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { } - }; - private final ChannelTuner.Listener mChannelTunerListener = new ChannelTuner.Listener() { @Override public void onLoadFinished() {} @@ -131,16 +58,12 @@ public class MenuUpdater { mTvView = tvView; mMenu = menu; if (mTvView != null) { - mTvView.setOnScreenBlockedListener(mOnScreenBlockingChangeListener); - } - if (CommonFeatures.DVR.isEnabled(context)) { - mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); - mDvrDataManager.addDvrScheduleLoadFinishedListener(mDvrScheduleLoadedListener); - mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener); - mDvrDataManager.addRecordedProgramLoadFinishedListener(mRecordedProgramLoadedListener); - mDvrDataManager.addRecordedProgramListener(mRecordedProgramListener); - } else { - mDvrDataManager = null; + mTvView.setOnScreenBlockedListener(new OnScreenBlockingChangedListener() { + @Override + public void onScreenBlockingChanged(boolean blocked) { + mMenu.update(PlayControlsRow.ID); + } + }); } } @@ -163,13 +86,6 @@ public class MenuUpdater { * Called at the end of the menu's lifetime. */ public void release() { - if (mDvrDataManager != null) { - mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); - mDvrDataManager.removeRecordedProgramListener(mRecordedProgramListener); - mDvrDataManager.removeDvrScheduleLoadFinishedListener(mDvrScheduleLoadedListener); - mDvrDataManager - .removeRecordedProgramLoadFinishedListener(mRecordedProgramLoadedListener); - } if (mChannelTuner != null) { mChannelTuner.removeListener(mChannelTunerListener); } diff --git a/src/com/android/tv/menu/MenuView.java b/src/com/android/tv/menu/MenuView.java index 4c612520..ee0b036e 100644 --- a/src/com/android/tv/menu/MenuView.java +++ b/src/com/android/tv/menu/MenuView.java @@ -24,6 +24,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewParent; import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; +import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.widget.FrameLayout; import com.android.tv.menu.Menu.MenuShowReason; @@ -57,6 +58,8 @@ public class MenuView extends FrameLayout implements IMenuView { public MenuView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mLayoutInflater = LayoutInflater.from(context); + // Set hardware layer type for smooth animation of lots of views. + setLayerType(LAYER_TYPE_HARDWARE, null); getViewTreeObserver().addOnGlobalFocusChangeListener(new OnGlobalFocusChangeListener() { @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { @@ -129,7 +132,15 @@ public class MenuView extends FrameLayout implements IMenuView { // Make the selected row have the focus. requestFocus(); if (runnableAfterShow != null) { - runnableAfterShow.run(); + getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + getViewTreeObserver().removeOnGlobalLayoutListener(this); + // Start show animation after layout finishes for smooth animation because the + // layout can take long time. + runnableAfterShow.run(); + } + }); } mLayoutManager.onMenuShow(); } @@ -183,6 +194,16 @@ public class MenuView extends FrameLayout implements IMenuView { return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); } + @Override + public void focusableViewAvailable(View v) { + // Workaround of b/30788222 and b/32074688. + // The re-layout of RecyclerView gives the focus to the card view even when the menu is not + // visible. Don't report focusable view when the menu is not visible. + if (getVisibility() == VISIBLE) { + super.focusableViewAvailable(v); + } + } + private void setSelectedPosition(int position) { mLayoutManager.setSelectedPosition(position); } diff --git a/src/com/android/tv/menu/PlayControlsButton.java b/src/com/android/tv/menu/PlayControlsButton.java index 95c07b74..aff39db3 100644 --- a/src/com/android/tv/menu/PlayControlsButton.java +++ b/src/com/android/tv/menu/PlayControlsButton.java @@ -129,6 +129,7 @@ public class PlayControlsButton extends FrameLayout { public void setEnabled(boolean enabled) { super.setEnabled(enabled); mButton.setEnabled(enabled); + mButton.setFocusable(enabled); mIcon.setEnabled(enabled); mIcon.setAlpha(enabled ? ALPHA_ENABLED : ALPHA_DISABLED); mLabel.setEnabled(enabled); diff --git a/src/com/android/tv/menu/PlayControlsRowView.java b/src/com/android/tv/menu/PlayControlsRowView.java index a19cbf89..a620d4dd 100644 --- a/src/com/android/tv/menu/PlayControlsRowView.java +++ b/src/com/android/tv/menu/PlayControlsRowView.java @@ -40,6 +40,8 @@ import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.ui.DvrStopRecordingFragment; +import com.android.tv.dvr.ui.HalfSizedDialogFragment; import com.android.tv.menu.Menu.MenuShowReason; import com.android.tv.ui.TunableTvView; import com.android.tv.util.Utils; @@ -130,10 +132,11 @@ public class PlayControlsRowView extends MenuRowView { res.getDimensionPixelSize(R.dimen.play_controls_button_compact_margin); if (CommonFeatures.DVR.isEnabled(context)) { mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrManager = TvApplication.getSingletons(context).getDvrManager(); } else { mDvrDataManager = null; + mDvrManager = null; } - mDvrManager = TvApplication.getSingletons(context).getDvrManager(); mMainActivity = (MainActivity) context; } @@ -249,7 +252,7 @@ public class PlayControlsRowView extends MenuRowView { private boolean isCurrentChannelRecording() { Channel currentChannel = mMainActivity.getCurrentChannel(); - return currentChannel != null + return currentChannel != null && mDvrManager != null && mDvrManager.getCurrentRecording(currentChannel.getId()) != null; } @@ -259,10 +262,11 @@ public class PlayControlsRowView extends MenuRowView { TvApplication.getSingletons(getContext()).getTracker().sendMenuClicked(isRecording ? R.string.channels_item_record_start : R.string.channels_item_record_stop); if (!isRecording) { - if (!mDvrManager.isChannelRecordable(currentChannel)) { + if (!(mDvrManager != null && mDvrManager.isChannelRecordable(currentChannel))) { Toast.makeText(mMainActivity, R.string.dvr_msg_cannot_record_channel, Toast.LENGTH_SHORT).show(); - } else { + } else if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(mMainActivity, + currentChannel.getInputId())) { Program program = TvApplication.getSingletons(mMainActivity).getProgramDataManager() .getCurrentProgram(currentChannel.getId()); if (program == null) { @@ -274,16 +278,25 @@ public class PlayControlsRowView extends MenuRowView { Toast.makeText(mMainActivity, msg, Toast.LENGTH_SHORT).show(); } } - } else { - DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel); + } else if (currentChannel != null) { + DvrUiHelper.showStopRecordingDialog(mMainActivity, currentChannel.getId(), + DvrStopRecordingFragment.REASON_USER_STOP, + new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrStopRecordingFragment.ACTION_STOP) { + ScheduledRecording currentRecording = + mDvrManager.getCurrentRecording( + currentChannel.getId()); + if (currentRecording != null) { + mDvrManager.stopRecording(currentRecording); + } + } + } + }); } } - private boolean needToShowRecordButton() { - return CommonFeatures.DVR.isEnabled(getContext()) - && mDvrManager.isChannelRecordable(mMainActivity.getCurrentChannel()); - } - private void initializeButton(PlayControlsButton button, int imageResId, int descriptionId, Integer focusedIconColor, Runnable clickAction) { button.setImageResId(imageResId); @@ -609,7 +622,8 @@ public class PlayControlsRowView extends MenuRowView { } private void updateRecordButton() { - if (!needToShowRecordButton()) { + if (!(mDvrManager != null + && mDvrManager.isChannelRecordable(mMainActivity.getCurrentChannel()))) { mRecordButton.setVisibility(View.GONE); updateButtonMargin(); return; diff --git a/src/com/android/tv/menu/SetupCardView.java b/src/com/android/tv/menu/SetupCardView.java deleted file mode 100644 index 7ad5e9d0..00000000 --- a/src/com/android/tv/menu/SetupCardView.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.menu; - -import android.content.Context; -import android.util.AttributeSet; - -import com.android.tv.R; -import com.android.tv.data.Channel; - -/** - * A view to render a guide card. - */ -public class SetupCardView extends BaseCardView<Channel> { - private static final String TAG = "GuideCardView"; - private static final boolean DEBUG = false; - - private static final int INVALID_COUNT = -1; - - private final float mCardHeight; - - public SetupCardView(Context context) { - this(context, null, 0); - } - - public SetupCardView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SetupCardView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - mCardHeight = getResources().getDimension(R.dimen.card_layout_height); - } - - @Override - protected float getCardHeight() { - return mCardHeight; - } -} diff --git a/src/com/android/tv/menu/SimpleCardView.java b/src/com/android/tv/menu/SimpleCardView.java index 24a44244..c99834be 100644 --- a/src/com/android/tv/menu/SimpleCardView.java +++ b/src/com/android/tv/menu/SimpleCardView.java @@ -19,16 +19,12 @@ package com.android.tv.menu; import android.content.Context; import android.util.AttributeSet; -import com.android.tv.R; import com.android.tv.data.Channel; /** * A view to render a guide card. */ public class SimpleCardView extends BaseCardView<Channel> { - private static final String TAG = "GuideCardView"; - private static final boolean DEBUG = false; - private final float mCardHeight; public SimpleCardView(Context context) { this(context, null, 0); @@ -40,11 +36,5 @@ public class SimpleCardView extends BaseCardView<Channel> { public SimpleCardView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - mCardHeight = getResources().getDimension(R.dimen.card_layout_height); - } - - @Override - protected float getCardHeight() { - return mCardHeight; } } diff --git a/src/com/android/tv/onboarding/NewSourcesFragment.java b/src/com/android/tv/onboarding/NewSourcesFragment.java index 1b14c114..8509b50c 100644 --- a/src/com/android/tv/onboarding/NewSourcesFragment.java +++ b/src/com/android/tv/onboarding/NewSourcesFragment.java @@ -26,7 +26,6 @@ import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.ui.setup.OnActionClickListener; import com.android.tv.common.ui.setup.SetupActionHelper; import com.android.tv.util.SetupUtils; @@ -48,8 +47,6 @@ public class NewSourcesFragment extends Fragment { */ public static final int ACTION_SKIP = 2; - private OnActionClickListener mOnActionClickListener; - public NewSourcesFragment() { setAllowEnterTransitionOverlap(false); setAllowReturnTransitionOverlap(false); diff --git a/src/com/android/tv/onboarding/OnboardingActivity.java b/src/com/android/tv/onboarding/OnboardingActivity.java index 41467376..45205c4c 100644 --- a/src/com/android/tv/onboarding/OnboardingActivity.java +++ b/src/com/android/tv/onboarding/OnboardingActivity.java @@ -44,7 +44,6 @@ public class OnboardingActivity extends SetupActivity { private static final String KEY_INTENT_AFTER_COMPLETION = "key_intent_after_completion"; private static final int PERMISSIONS_REQUEST_READ_TV_LISTINGS = 1; - private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS"; private static final int SHOW_RIPPLE_DURATION_MS = 266; @@ -82,10 +81,19 @@ public class OnboardingActivity extends SetupActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (!PermissionUtils.hasAccessAllEpg(this) - && checkSelfPermission(PERMISSION_READ_TV_LISTINGS) - != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{PERMISSION_READ_TV_LISTINGS}, + ApplicationSingletons singletons = TvApplication.getSingletons(this); + mInputManager = singletons.getTvInputManagerHelper(); + if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) { + mChannelDataManager = singletons.getChannelDataManager(); + // Make the channels of the new inputs which have been setup outside Live TV + // browsable. + if (mChannelDataManager.isDbLoadFinished()) { + SetupUtils.getInstance(this).markNewChannelsBrowsable(); + } else { + mChannelDataManager.addListener(mChannelListener); + } + } else { + requestPermissions(new String[] {PermissionUtils.PERMISSION_READ_TV_LISTINGS}, PERMISSIONS_REQUEST_READ_TV_LISTINGS); } } @@ -101,16 +109,6 @@ public class OnboardingActivity extends SetupActivity { @Override protected Fragment onCreateInitialFragment() { if (PermissionUtils.hasAccessAllEpg(this) || PermissionUtils.hasReadTvListings(this)) { - ApplicationSingletons singletons = TvApplication.getSingletons(this); - mChannelDataManager = singletons.getChannelDataManager(); - mInputManager = singletons.getTvInputManagerHelper(); - // Make the channels of the new inputs which have been setup outside Live TV - // browsable. - if (mChannelDataManager.isDbLoadFinished()) { - SetupUtils.getInstance(this).markNewChannelsBrowsable(); - } else { - mChannelDataManager.addListener(mChannelListener); - } return OnboardingUtils.isFirstRunWithCurrentVersion(this) ? new WelcomeFragment() : new SetupSourcesFragment(); } diff --git a/src/com/android/tv/setup/SystemSetupActivity.java b/src/com/android/tv/setup/SystemSetupActivity.java new file mode 100644 index 00000000..7e627410 --- /dev/null +++ b/src/com/android/tv/setup/SystemSetupActivity.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.setup; + +import android.app.Activity; +import android.app.Fragment; +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.Intent; +import android.media.tv.TvInputInfo; +import android.os.Bundle; +import android.widget.Toast; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.R; +import com.android.tv.SetupPassthroughActivity; +import com.android.tv.common.TvCommonUtils; +import com.android.tv.common.ui.setup.SetupActivity; +import com.android.tv.common.ui.setup.SetupFragment; +import com.android.tv.common.ui.setup.SetupMultiPaneFragment; +import com.android.tv.TvApplication; +import com.android.tv.data.ChannelDataManager; +import com.android.tv.onboarding.SetupSourcesFragment; +import com.android.tv.util.OnboardingUtils; +import com.android.tv.util.SetupUtils; +import com.android.tv.util.TvInputManagerHelper; + +/** + * A activity to start input sources setup fragment for initial setup flow. + */ +public class SystemSetupActivity extends SetupActivity { + private static final String SYSTEM_SETUP = + "com.android.tv.action.LAUNCH_SYSTEM_SETUP"; + private static final int SHOW_RIPPLE_DURATION_MS = 266; + private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1; + + private TvInputManagerHelper mInputManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + if (!SYSTEM_SETUP.equals(intent.getAction())) { + finish(); + return; + } + ApplicationSingletons singletons = TvApplication.getSingletons(this); + mInputManager = singletons.getTvInputManagerHelper(); + } + + @Override + protected Fragment onCreateInitialFragment() { + return new SetupSourcesFragment(); + } + + private void showMerchantCollection() { + executeActionWithDelay(new Runnable() { + @Override + public void run() { + startActivity(OnboardingUtils.ONLINE_STORE_INTENT); + } + }, SHOW_RIPPLE_DURATION_MS); + } + + @Override + public boolean executeAction(String category, int actionId, Bundle params) { + switch (category) { + case SetupSourcesFragment.ACTION_CATEGORY: + switch (actionId) { + case SetupSourcesFragment.ACTION_ONLINE_STORE: + showMerchantCollection(); + return true; + case SetupSourcesFragment.ACTION_SETUP_INPUT: { + String inputId = params.getString( + SetupSourcesFragment.ACTION_PARAM_KEY_INPUT_ID); + TvInputInfo input = mInputManager.getTvInputInfo(inputId); + Intent intent = TvCommonUtils.createSetupIntent(input); + if (intent == null) { + Toast.makeText(this, R.string.msg_no_setup_activity, Toast.LENGTH_SHORT) + .show(); + return true; + } + // Even though other app can handle the intent, the setup launched by Live + // channels should go through Live channels SetupPassthroughActivity. + intent.setComponent(new ComponentName(this, + SetupPassthroughActivity.class)); + try { + // Now we know that the user intends to set up this input. Grant + // permission for writing EPG data. + SetupUtils.grantEpgPermission(this, input.getServiceInfo().packageName); + startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, + getString(R.string.msg_unable_to_start_setup_activity, + input.loadLabel(this)), Toast.LENGTH_SHORT).show(); + } + return true; + } + case SetupMultiPaneFragment.ACTION_DONE: { + // To make sure user can finish setup flow, set result as RESULT_OK. + setResult(Activity.RESULT_OK); + finish(); + return true; + } + } + break; + } + return false; + } +} diff --git a/src/com/android/tv/tuner/TunerPreferenceProvider.java b/src/com/android/tv/tuner/TunerPreferenceProvider.java index 3d289bb2..3a3561b6 100644 --- a/src/com/android/tv/tuner/TunerPreferenceProvider.java +++ b/src/com/android/tv/tuner/TunerPreferenceProvider.java @@ -38,8 +38,6 @@ public class TunerPreferenceProvider extends ContentProvider { private static final int DATABASE_VERSION = 1; private static final String DATABASE_NAME = "usbtuner_preferences.db"; private static final String PREFERENCES_TABLE = "preferences"; - private static final String PREFERENCES_TABLE_ID_INDEX = "preferences_id_index"; - private static final String PREFERENCES_TABLE_KEY_INDEX = "preferences_key_index"; private static final int MATCH_PREFERENCE = 1; private static final int MATCH_PREFERENCE_KEY = 2; diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java index 7a5518c8..1547e3ae 100644 --- a/src/com/android/tv/tuner/TunerPreferences.java +++ b/src/com/android/tv/tuner/TunerPreferences.java @@ -41,6 +41,7 @@ public class TunerPreferences { private static final String PREFS_KEY_SCANNED_CHANNEL_COUNT = "scanned_channel_count"; private static final String PREFS_KEY_SCAN_DONE = "scan_done"; private static final String PREFS_KEY_LAUNCH_SETUP = "launch_setup"; + private static final String PREFS_KEY_STORE_TS_STREAM = "store_ts_stream"; private static final String SHARED_PREFS_NAME = "com.android.tv.tuner.preferences"; @@ -202,6 +203,28 @@ public class TunerPreferences { } } + @MainThread + public static boolean getStoreTsStream(Context context) { + SoftPreconditions.checkState(sInitialized); + if (useContentProvider(context)) { + return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false); + } else { + return getSharedPreferences(context) + .getBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, false); + } + } + + @MainThread + public static void setStoreTsStream(Context context, boolean shouldStore) { + if (useContentProvider(context)) { + setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore); + } else { + getSharedPreferences(context).edit() + .putBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, shouldStore) + .apply(); + } + } + private static SharedPreferences getSharedPreferences(Context context) { return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); } @@ -266,6 +289,7 @@ public class TunerPreferences { break; case PREFS_KEY_SCAN_DONE: case PREFS_KEY_LAUNCH_SETUP: + case PREFS_KEY_STORE_TS_STREAM: bundle.putBoolean(key, Boolean.parseBoolean(value)); break; } diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java index 91c1f5b0..22cf2aa6 100644 --- a/src/com/android/tv/tuner/data/TunerChannel.java +++ b/src/com/android/tv/tuner/data/TunerChannel.java @@ -54,8 +54,6 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks "Extended Parameterized Service" }; private static final String ATSC_SERVICE_TYPE_NAME_RESERVED = ATSC_SERVICE_TYPE_NAMES[Channel.SERVICE_TYPE_ATSC_RESERVED]; - private static final String ATSC_SERVICE_TYPE_NAME_DIGITAL_TELEVISION = - ATSC_SERVICE_TYPE_NAMES[Channel.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION]; public static final int INVALID_FREQUENCY = -1; diff --git a/src/com/android/tv/tuner/exoplayer/DataSourceAdapter.java b/src/com/android/tv/tuner/exoplayer/DataSourceAdapter.java deleted file mode 100644 index e7c11e38..00000000 --- a/src/com/android/tv/tuner/exoplayer/DataSourceAdapter.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.tv.tuner.exoplayer; - - -import android.media.MediaDataSource; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.upstream.DataSource; -import com.google.android.exoplayer.upstream.DataSpec; - -import java.io.IOException; - -/** - * A DataSource adapter implementation by using {@link MediaDataSource}. - */ -public class DataSourceAdapter implements DataSource { - private MediaDataSource mMediaDataSource; - private long mReadPosition; - - public DataSourceAdapter(MediaDataSource mediaDataSource) { - mMediaDataSource = mediaDataSource; - } - - @Override - public long open(DataSpec dataSpec) throws IOException { - return C.LENGTH_UNBOUNDED; - } - - @Override - public void close() throws IOException { - mMediaDataSource.close(); - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { - int ret = mMediaDataSource.readAt(mReadPosition, buffer, offset, readLength); - if (ret > 0) { - mReadPosition += ret; - return ret; - } - return C.RESULT_END_OF_INPUT; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java index 4eb0d32c..c105e222 100644 --- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java +++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java @@ -16,10 +16,12 @@ package com.android.tv.tuner.exoplayer; -import android.media.MediaDataSource; -import android.media.tv.TvContract; +import android.net.Uri; import android.os.ConditionVariable; import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; import android.os.SystemClock; import com.google.android.exoplayer.C; @@ -28,6 +30,7 @@ import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.extractor.ExtractorSampleSource; +import com.google.android.exoplayer.extractor.ExtractorSampleSource.EventListener; import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultAllocator; @@ -41,6 +44,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; /** @@ -48,36 +52,49 @@ import java.util.concurrent.atomic.AtomicLong; * For demux, this class relies on {@link com.google.android.exoplayer.extractor.ts.TsExtractor}. */ public class ExoPlayerSampleExtractor implements SampleExtractor { - private static final String TAG = "ExoPlayerSampleExtractor"; + private static final String TAG = "ExoPlayerSampleExtracto"; // Buffer segment size for memory allocator. Copied from demo implementation of ExoPlayer. private static final int BUFFER_SEGMENT_SIZE_IN_BYTES = 64 * 1024; // Buffer segment count for sample source. Copied from demo implementation of ExoPlayer. private static final int BUFFER_SEGMENT_COUNT = 256; - private static final AtomicLong ID_COUNTER = new AtomicLong(0); - - private final ExtractorThread mExtractorThread; - private final BufferManager.SampleBuffer mSampleBuffer; + private final HandlerThread mSourceReaderThread; private final long mId; - private final List<MediaFormat> mTrackFormats = new ArrayList<>(); - private final SampleSource.SampleSourceReader mSampleSourceReader; + private final Handler.Callback mSourceReaderWorker; - private boolean mReleased; - private boolean mOnCompletionCalled; + private BufferManager.SampleBuffer mSampleBuffer; + private Handler mSourceReaderHandler; + private volatile boolean mPrepared; + private AtomicBoolean mOnCompletionCalled = new AtomicBoolean(); + private IOException mExceptionOnPrepare; + private List<MediaFormat> mTrackFormats; private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>(); private OnCompletionListener mOnCompletionListener; private Handler mOnCompletionListenerHandler; + private IOException mError; - public ExoPlayerSampleExtractor(DataSource source, BufferManager bufferManager, + public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager, PlaybackBufferListener bufferListener, boolean isRecording) { - mId = ID_COUNTER.incrementAndGet(); + // It'll be used as a timeshift file chunk name's prefix. + mId = System.currentTimeMillis(); Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES); - mSampleSourceReader = new ExtractorSampleSource(TvContract.Programs.CONTENT_URI, source, - allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES, null, null, 0); - mExtractorThread = new ExtractorThread(); + EventListener eventListener = new EventListener() { + + @Override + public void onLoadError(int sourceId, IOException e) { + mError = e; + } + }; + + mSourceReaderThread = new HandlerThread("SourceReaderThread"); + mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source, + allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES, + // Do not create a handler if we not on a looper. e.g. test. + Looper.myLooper() != null ? new Handler() : null, + eventListener, 0)); if (isRecording) { mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false, RecordingSampleBuffer.BUFFER_REASON_RECORDING); @@ -97,35 +114,114 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { mOnCompletionListenerHandler = handler; } - private class ExtractorThread extends Thread { - private static final int FETCH_SAMPLE_INTERVAL_MS = 50; - private volatile boolean mQuitRequested = false; + private class SourceReaderWorker implements Handler.Callback { + public static final int MSG_PREPARE = 1; + public static final int MSG_FETCH_SAMPLES = 2; + public static final int MSG_RELEASE = 3; + private static final int RETRY_INTERVAL_MS = 50; + + private final SampleSource mSampleSource; + private SampleSource.SampleSourceReader mSampleSourceReader; + private boolean[] mTrackMetEos; + private boolean mMetEos = false; private long mCurrentPosition; - public ExtractorThread() { - super("ExtractorThread"); + public SourceReaderWorker(SampleSource sampleSource) { + mSampleSource = sampleSource; } @Override - public void run() { - SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - ConditionVariable conditionVariable = new ConditionVariable(); - int trackCount = mSampleSourceReader.getTrackCount(); - while (!mQuitRequested) { - boolean didSomething = false; - for (int i = 0; i < trackCount; ++i) { - if(SampleSource.NOTHING_READ != fetchSample(i, sample, conditionVariable)) { - didSomething = true; + public boolean handleMessage(Message message) { + switch (message.what) { + case MSG_PREPARE: + mPrepared = prepare(); + if (!mPrepared && mExceptionOnPrepare == null) { + mSourceReaderHandler + .sendEmptyMessageDelayed(MSG_PREPARE, RETRY_INTERVAL_MS); + } else{ + mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); } - } - if (!didSomething) { - try { - Thread.sleep(FETCH_SAMPLE_INTERVAL_MS); - } catch (InterruptedException e) { + return true; + case MSG_FETCH_SAMPLES: + boolean didSomething = false; + SampleHolder sample = new SampleHolder( + SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + ConditionVariable conditionVariable = new ConditionVariable(); + int trackCount = mSampleSourceReader.getTrackCount(); + for (int i = 0; i < trackCount; ++i) { + if (!mTrackMetEos[i] && SampleSource.NOTHING_READ + != fetchSample(i, sample, conditionVariable)) { + if (mMetEos) { + // If mMetEos was on during fetchSample() due to an error, + // fetching from other tracks is not necessary. + break; + } + didSomething = true; + } + } + if (!mMetEos) { + if (didSomething) { + mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); + } else { + mSourceReaderHandler.sendEmptyMessageDelayed(MSG_FETCH_SAMPLES, + RETRY_INTERVAL_MS); + } + } else { + notifyCompletionIfNeeded(false); + } + return true; + case MSG_RELEASE: + if (mSampleSourceReader != null) { + if (mPrepared) { + // ExtractorSampleSource expects all the tracks should be disabled + // before releasing. + int count = mSampleSourceReader.getTrackCount(); + for (int i = 0; i < count; ++i) { + mSampleSourceReader.disable(i); + } + } + mSampleSourceReader.release(); + mSampleSourceReader = null; } + cleanUp(); + mSourceReaderHandler.removeCallbacksAndMessages(null); + return true; + } + return false; + } + + private boolean prepare() { + if (mSampleSourceReader == null) { + mSampleSourceReader = mSampleSource.register(); + } + if(!mSampleSourceReader.prepare(0)) { + return false; + } + if (mTrackFormats == null) { + int trackCount = mSampleSourceReader.getTrackCount(); + mTrackMetEos = new boolean[trackCount]; + List<MediaFormat> trackFormats = new ArrayList<>(); + for (int i = 0; i < trackCount; i++) { + trackFormats.add(mSampleSourceReader.getFormat(i)); + mSampleSourceReader.enable(i, 0); + + } + mTrackFormats = trackFormats; + List<String> ids = new ArrayList<>(); + for (int i = 0; i < mTrackFormats.size(); i++) { + ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); + } + try { + mSampleBuffer.init(ids, mTrackFormats); + } catch (IOException e) { + // In this case, we will not schedule any further operation. + // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will + // call release() eventually. + mExceptionOnPrepare = e; + return false; } } - cleanUp(); + return true; } private int fetchSample(int track, SampleHolder sample, @@ -150,20 +246,24 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { queueSample(track, sample, conditionVariable); } catch (IOException e) { mLastExtractedPositionUsMap.clear(); - mQuitRequested = true; + mMetEos = true; mSampleBuffer.setEos(); } } else if (ret == SampleSource.END_OF_STREAM) { - mQuitRequested = true; - mSampleBuffer.setEos(); + mTrackMetEos[track] = true; + for (int i = 0; i < mTrackMetEos.length; ++i) { + if (!mTrackMetEos[i]) { + break; + } + if (i == mTrackMetEos.length -1) { + mMetEos = true; + mSampleBuffer.setEos(); + } + } } // TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263 return ret; } - - public void quit() { - mQuitRequested = true; - } } private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) @@ -179,29 +279,30 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { } @Override + public void maybeThrowError() throws IOException { + if (mError != null) { + IOException e = mError; + mError = null; + throw e; + } + } + + @Override public boolean prepare() throws IOException { - synchronized (this) { - if(!mSampleSourceReader.prepare(0)) { - return false; - } - int trackCount = mSampleSourceReader.getTrackCount(); - mTrackFormats.clear(); - for (int i = 0; i < trackCount; i++) { - mTrackFormats.add(mSampleSourceReader.getFormat(i)); - mSampleSourceReader.enable(i, 0); - } - List<String> ids = new ArrayList<>(); - for (int i = 0; i < trackCount; i++) { - ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); - } - mSampleBuffer.init(ids, mTrackFormats); + if (!mSourceReaderThread.isAlive()) { + mSourceReaderThread.start(); + mSourceReaderHandler = new Handler(mSourceReaderThread.getLooper(), + mSourceReaderWorker); + mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_PREPARE); + } + if (mExceptionOnPrepare != null) { + throw mExceptionOnPrepare; } - mExtractorThread.start(); - return true; + return mPrepared; } @Override - public synchronized List<MediaFormat> getTrackFormats() { + public List<MediaFormat> getTrackFormats() { return mTrackFormats; } @@ -243,27 +344,45 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { @Override public void release() { - synchronized (this) { - mReleased = true; - } - if (mExtractorThread.isAlive()) { - mExtractorThread.quit(); + if (mSourceReaderThread.isAlive()) { + mSourceReaderHandler.removeCallbacksAndMessages(null); + mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_RELEASE); + mSourceReaderThread.quitSafely(); + // Return early in this case so that session worker can start working on the next + // request as early as it can. The clean up will be done in the reader thread while + // handling MSG_RELEASE. } else { cleanUp(); } } - private void onCompletion(final boolean result, long lastExtractedPositionUs) { - final OnCompletionListener listener = mOnCompletionListener; - if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) { - mOnCompletionListenerHandler.post(new Runnable() { - @Override - public void run() { - listener.onCompletion(result, lastExtractedPositionUs); - } - }); + private void cleanUp() { + boolean result = true; + try { + if (mSampleBuffer != null) { + mSampleBuffer.release(); + mSampleBuffer = null; + } + } catch (IOException e) { + result = false; + } + notifyCompletionIfNeeded(result); + setOnCompletionListener(null, null); + } + + private void notifyCompletionIfNeeded(final boolean result) { + if (!mOnCompletionCalled.getAndSet(true)) { + final OnCompletionListener listener = mOnCompletionListener; + final long lastExtractedPositionUs = getLastExtractedPositionUs(); + if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) { + mOnCompletionListenerHandler.post(new Runnable() { + @Override + public void run() { + listener.onCompletion(result, lastExtractedPositionUs); + } + }); + } } - mOnCompletionCalled = true; } private long getLastExtractedPositionUs() { @@ -276,23 +395,4 @@ public class ExoPlayerSampleExtractor implements SampleExtractor { } return lastExtractedPositionUs; } - - private synchronized void cleanUp() { - if (!mReleased) { - if (!mOnCompletionCalled) { - onCompletion(false, getLastExtractedPositionUs()); - } - return; - } - boolean result = true; - try { - mSampleBuffer.release(); - } catch (IOException e) { - result = false; - } - if (!mOnCompletionCalled) { - onCompletion(result, getLastExtractedPositionUs()); - } - setOnCompletionListener(null, null); - } } diff --git a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java index a5059bc2..ec7b4b16 100644 --- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java +++ b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java @@ -55,6 +55,11 @@ public class FileSampleExtractor implements SampleExtractor{ } @Override + public void maybeThrowError() throws IOException { + // Do nothing. + } + + @Override public boolean prepare() throws IOException { ArrayList<Pair<String, android.media.MediaFormat>> trackInfos = mBufferManager.readTrackInfoFiles(); diff --git a/src/com/android/tv/tuner/exoplayer/FrameworkSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FrameworkSampleExtractor.java deleted file mode 100644 index 487844b4..00000000 --- a/src/com/android/tv/tuner/exoplayer/FrameworkSampleExtractor.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer; - -import android.media.MediaDataSource; -import android.media.MediaExtractor; -import android.os.Handler; -import android.os.ConditionVariable; -import android.os.SystemClock; -import android.util.Log; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.MediaFormatUtil; -import com.google.android.exoplayer.SampleHolder; -import com.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; -import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.atomic.AtomicLong; - -/** - * A class that plays and records a live stream from tuner using a {@link MediaExtractor} - * and the private ExtractorThread class. - */ -public class FrameworkSampleExtractor implements SampleExtractor { - private static final String TAG = "FrameworkSampleExtractor"; - - // Maximum bandwidth of 1080p channel is about 2.2MB/s. 2MB for a sample will suffice. - private static final int SAMPLE_BUFFER_SIZE = 1024 * 1024 * 2; - private static final AtomicLong ID_COUNTER = new AtomicLong(0); - - private final MediaDataSource mDataSource; - private final MediaExtractor mMediaExtractor; - private final ExtractorThread mExtractorThread; - private final BufferManager.SampleBuffer mSampleBuffer; - private final long mId; - private final List<MediaFormat> mTrackFormats = new ArrayList<>(); - - private boolean mReleased; - private boolean mOnCompletionCalled; - private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>(); - private OnCompletionListener mOnCompletionListener; - private Handler mOnCompletionListenerHandler; - - public FrameworkSampleExtractor(MediaDataSource source, BufferManager bufferManager, - PlaybackBufferListener bufferListener, boolean isRecording) { - mId = ID_COUNTER.incrementAndGet(); - mDataSource = source; - mMediaExtractor = new MediaExtractor(); - mExtractorThread = new ExtractorThread(); - if (isRecording) { - mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false, - RecordingSampleBuffer.BUFFER_REASON_RECORDING); - } else { - if (bufferManager == null || bufferManager.isDisabled()) { - mSampleBuffer = new SimpleSampleBuffer(bufferListener); - } else { - mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true, - RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK); - } - } - } - - @Override - public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { - mOnCompletionListener = listener; - mOnCompletionListenerHandler = handler; - } - - private class ExtractorThread extends Thread { - private volatile boolean mQuitRequested = false; - - public ExtractorThread() { - super("ExtractorThread"); - } - - @Override - public void run() { - SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - sample.ensureSpaceForWrite(SAMPLE_BUFFER_SIZE); - ConditionVariable conditionVariable = new ConditionVariable(); - while (!mQuitRequested) { - fetchSample(sample, conditionVariable); - } - cleanUp(); - } - - private void fetchSample(SampleHolder sample, ConditionVariable conditionVariable) { - int index = mMediaExtractor.getSampleTrackIndex(); - if (index < 0) { - Log.i(TAG, "EoS"); - mQuitRequested = true; - mSampleBuffer.setEos(); - return; - } - sample.data.clear(); - sample.size = mMediaExtractor.readSampleData(sample.data, 0); - if (sample.size < 0 || sample.size > SAMPLE_BUFFER_SIZE) { - // Should not happen - Log.e(TAG, "Invalid sample size: " + sample.size); - mMediaExtractor.advance(); - return; - } - sample.data.position(sample.size); - sample.timeUs = mMediaExtractor.getSampleTime(); - sample.flags = mMediaExtractor.getSampleFlags(); - - mMediaExtractor.advance(); - try { - Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(index); - if (lastExtractedPositionUs == null) { - mLastExtractedPositionUsMap.put(index, sample.timeUs); - } else { - mLastExtractedPositionUsMap.put(index, - Math.max(lastExtractedPositionUs, sample.timeUs)); - } - queueSample(index, sample, conditionVariable); - } catch (IOException e) { - mLastExtractedPositionUsMap.clear(); - mQuitRequested = true; - mSampleBuffer.setEos(); - } - } - - public void quit() { - mQuitRequested = true; - } - } - - private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) - throws IOException { - long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); - mSampleBuffer.writeSample(index, sample, conditionVariable); - - // Checks whether the storage has enough bandwidth for recording samples. - if (mSampleBuffer.isWriteSpeedSlow(sample.size, - SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { - mSampleBuffer.handleWriteSpeedSlow(); - } - } - - @Override - public boolean prepare() throws IOException { - synchronized (this) { - mMediaExtractor.setDataSource(mDataSource); - int trackCount = mMediaExtractor.getTrackCount(); - mTrackFormats.clear(); - for (int i = 0; i < trackCount; i++) { - mTrackFormats.add(MediaFormatUtil.createMediaFormat( - mMediaExtractor.getTrackFormat(i))); - mMediaExtractor.selectTrack(i); - } - List<String> ids = new ArrayList<>(); - for (int i = 0; i < trackCount; i++) { - ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); - } - mSampleBuffer.init(ids, mTrackFormats); - } - mExtractorThread.start(); - return true; - } - - @Override - public synchronized List<MediaFormat> getTrackFormats() { - return mTrackFormats; - } - - @Override - public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { - outMediaFormatHolder.format = mTrackFormats.get(track); - outMediaFormatHolder.drmInitData = null; - } - - @Override - public void selectTrack(int index) { - mSampleBuffer.selectTrack(index); - } - - @Override - public void deselectTrack(int index) { - mSampleBuffer.deselectTrack(index); - } - - @Override - public long getBufferedPositionUs() { - return mSampleBuffer.getBufferedPositionUs(); - } - - @Override - public boolean continueBuffering(long positionUs) { - return mSampleBuffer.continueBuffering(positionUs); - } - - @Override - public void seekTo(long positionUs) { - mSampleBuffer.seekTo(positionUs); - } - - @Override - public int readSample(int track, SampleHolder sampleHolder) { - return mSampleBuffer.readSample(track, sampleHolder); - } - - @Override - public void release() { - synchronized (this) { - mReleased = true; - } - if (mExtractorThread.isAlive()) { - mExtractorThread.quit(); - - // We don't join here to prevent hang --- MediaExtractor is released at the thread. - } else { - cleanUp(); - } - } - - private void onCompletion(final boolean result, long lastExtractedPositionUs) { - final OnCompletionListener listener = mOnCompletionListener; - if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) { - mOnCompletionListenerHandler.post(new Runnable() { - @Override - public void run() { - listener.onCompletion(result, lastExtractedPositionUs); - } - }); - } - mOnCompletionCalled = true; - } - - private long getLastExtractedPositionUs() { - long lastExtractedPositionUs = Long.MAX_VALUE; - for (long value : mLastExtractedPositionUsMap.values()) { - lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value); - } - if (lastExtractedPositionUs == Long.MAX_VALUE) { - lastExtractedPositionUs = C.UNKNOWN_TIME_US; - } - return lastExtractedPositionUs; - } - - private synchronized void cleanUp() { - if (!mReleased) { - if (!mOnCompletionCalled) { - onCompletion(false, getLastExtractedPositionUs()); - } - return; - } - boolean result = true; - try { - mSampleBuffer.release(); - } catch (IOException e) { - result = false; - } - if (!mOnCompletionCalled) { - onCompletion(result, getLastExtractedPositionUs()); - } - setOnCompletionListener(null, null); - mMediaExtractor.release(); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java index c1a20a55..381b22e9 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java @@ -19,7 +19,6 @@ package com.android.tv.tuner.exoplayer; import android.content.Context; import android.media.AudioFormat; import android.media.MediaCodec.CryptoException; -import android.media.MediaDataSource; import android.media.PlaybackParams; import android.os.Handler; import android.support.annotation.IntDef; @@ -35,14 +34,15 @@ import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioTrack; +import com.google.android.exoplayer.upstream.DataSource; import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.Cea708Data.CaptionEvent; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer; import com.android.tv.tuner.exoplayer.ac3.Ac3TrackRenderer; -import com.android.tv.tuner.source.TsMediaDataSource; -import com.android.tv.tuner.source.TsMediaDataSourceManager; +import com.android.tv.tuner.source.TsDataSource; +import com.android.tv.tuner.source.TsDataSourceManager; import com.android.tv.tuner.tvinput.EventDetector; import java.lang.annotation.Retention; @@ -59,7 +59,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen * Interface definition for building specific track renderers. */ public interface RendererBuilder { - void buildRenderers(MpegTsPlayer mpegTsPlayer, MediaDataSource dataSource, + void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource, RendererBuilderCallback callback); } @@ -125,14 +125,13 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen private final ExoPlayer mPlayer; private final Handler mMainHandler; private final AudioCapabilities mAudioCapabilities; - private final TsMediaDataSourceManager mSourceManager; + private final TsDataSourceManager mSourceManager; private Listener mListener; @RendererBuildingState private int mRendererBuildingState; - private boolean mLastReportedPlayWhenReady; private Surface mSurface; - private TsMediaDataSource mMediaDataSource; + private TsDataSource mDataSource; private InternalRendererBuilderCallback mBuilderCallback; private TrackRenderer mVideoRenderer; private TrackRenderer mAudioRenderer; @@ -147,12 +146,12 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen * * @param rendererBuilder the builder of track renderers * @param handler the handler for the playback events in track renderers - * @param sourceManager the manager for {@link MediaDataSource} + * @param sourceManager the manager for {@link DataSource} * @param capabilities the {@link AudioCapabilities} of the current device * @param listener the listener for playback state changes */ public MpegTsPlayer(RendererBuilder rendererBuilder, Handler handler, - TsMediaDataSourceManager sourceManager, AudioCapabilities capabilities, + TsDataSourceManager sourceManager, AudioCapabilities capabilities, Listener listener) { mRendererBuilder = rendererBuilder; mPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS); @@ -213,7 +212,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen } /** - * Creates renderers and {@link MediaDataSource} and initializes player. + * Creates renderers and {@link DataSource} and initializes player. * @param context a {@link Context} instance * @param channel to play * @param eventListener for program information which will be scanned from MPEG2-TS stream @@ -221,14 +220,14 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen */ public boolean prepare(Context context, TunerChannel channel, EventDetector.EventListener eventListener) { - TsMediaDataSource source = null; + TsDataSource source = null; if (channel != null) { source = mSourceManager.createDataSource(context, channel, eventListener); if (source == null) { return false; } } - mMediaDataSource = source; + mDataSource = source; if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { mPlayer.stop(); } @@ -242,10 +241,10 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen } /** - * Returns {@link TsMediaDataSource} which provides MPEG2-TS stream. + * Returns {@link TsDataSource} which provides MPEG2-TS stream. */ - public TsMediaDataSource getDataSource() { - return mMediaDataSource; + public TsDataSource getDataSource() { + return mDataSource; } private void onRenderers(TrackRenderer[] renderers) { @@ -347,9 +346,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen * Releases the player. */ public void release() { - if (mMediaDataSource != null) { - mSourceManager.releaseDataSource(mMediaDataSource); - mMediaDataSource = null; + if (mDataSource != null) { + mSourceManager.releaseDataSource(mDataSource); + mDataSource = null; } if (mBuilderCallback != null) { mBuilderCallback.cancel(); @@ -479,6 +478,23 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen } /** + * Returns the number of tracks exposed by the specified renderer. + */ + public int getTrackCount(int rendererIndex) { + return mPlayer.getTrackCount(rendererIndex); + } + + /** + * Selects a track for the specified renderer. + */ + public void setSelectedTrack(int rendererIndex, int trackIndex) { + if (trackIndex >= getTrackCount(rendererIndex)) { + return; + } + mPlayer.setSelectedTrack(rendererIndex, trackIndex); + } + + /** * Gets the main handler of the player. */ /* package */ Handler getMainHandler() { diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java index f6ea84d9..0e46c9cf 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java @@ -17,10 +17,10 @@ package com.android.tv.tuner.exoplayer; import android.content.Context; -import android.media.MediaDataSource; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.upstream.DataSource; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder; import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback; import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer; @@ -43,7 +43,7 @@ public class MpegTsRendererBuilder implements RendererBuilder { } @Override - public void buildRenderers(MpegTsPlayer mpegTsPlayer, MediaDataSource dataSource, + public void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource, RendererBuilderCallback callback) { // Build the video and audio renderers. SampleExtractor extractor = dataSource == null ? diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java index aaf4a391..7bf116c8 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java @@ -16,17 +16,15 @@ package com.android.tv.tuner.exoplayer; -import android.media.MediaDataSource; +import android.net.Uri; import android.os.Handler; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.MediaFormatUtil; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.MimeTypes; -import com.android.tv.tuner.TunerFlags; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.exoplayer.buffer.SamplePool; import com.android.tv.tuner.tvinput.PlaybackBufferListener; @@ -38,7 +36,7 @@ import java.util.LinkedList; import java.util.List; /** - * Extracts samples from {@link MediaDataSource} for MPEG-TS streams. + * Extracts samples from {@link DataSource} for MPEG-TS streams. */ public final class MpegTsSampleExtractor implements SampleExtractor { public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708"; @@ -64,22 +62,17 @@ public final class MpegTsSampleExtractor implements SampleExtractor { } /** - * Creates MpegTsSampleExtractor for {@link MediaDataSource}. + * Creates MpegTsSampleExtractor for {@link DataSource}. * - * @param source the {@link MediaDataSource} to extract from + * @param source the {@link DataSource} to extract from * @param bufferManager the manager for reading & writing samples backed by physical storage * @param bufferListener the {@link PlaybackBufferListener} * to notify buffer storage status change */ - public MpegTsSampleExtractor(MediaDataSource source, - BufferManager bufferManager, PlaybackBufferListener bufferListener) { - if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) { - mSampleExtractor = new ExoPlayerSampleExtractor(new DataSourceAdapter(source), - bufferManager, bufferListener, false); - } else { - mSampleExtractor = new FrameworkSampleExtractor(source, bufferManager, bufferListener, - false); - } + public MpegTsSampleExtractor(DataSource source, BufferManager bufferManager, + PlaybackBufferListener bufferListener) { + mSampleExtractor = new ExoPlayerSampleExtractor(Uri.EMPTY, source, bufferManager, + bufferListener, false); init(); } @@ -97,6 +90,13 @@ public final class MpegTsSampleExtractor implements SampleExtractor { } @Override + public void maybeThrowError() throws IOException { + if (mSampleExtractor != null) { + mSampleExtractor.maybeThrowError(); + } + } + + @Override public boolean prepare() throws IOException { if(!mSampleExtractor.prepare()) { return false; @@ -124,8 +124,8 @@ public final class MpegTsSampleExtractor implements SampleExtractor { mCea708TextTrackIndex = trackCount; } if (mCea708TextTrackIndex >= 0) { - mTrackFormats.add(MediaFormatUtil.createTextMediaFormat(MIMETYPE_TEXT_CEA_708, - mTrackFormats.get(0).durationUs)); + mTrackFormats.add(MediaFormat.createTextFormat(null, MIMETYPE_TEXT_CEA_708, 0, + mTrackFormats.get(0).durationUs, "")); } return true; } @@ -224,81 +224,112 @@ public final class MpegTsSampleExtractor implements SampleExtractor { public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { } private abstract class CcParser { + // Interim buffer for reduce direct access to ByteBuffer which is expensive. Using + // relatively small buffer size in order to minimize memory footprint increase. + protected final byte[] mBuffer = new byte[1024]; + abstract void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs); - protected void parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) { + protected int parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) { // For the details of user_data_type_structure, see ATSC A/53 Part 4 - Table 6.9. int pos = offset; if (pos + 2 >= buffer.position()) { - return; + return offset; } boolean processCcDataFlag = (buffer.get(pos) & 64) != 0; int ccCount = buffer.get(pos) & 0x1f; pos += 2; if (!processCcDataFlag || pos + 3 * ccCount >= buffer.position() || ccCount == 0) { - return; + return offset; } SampleHolder holder = mCcSamplePool.acquireSample(CC_BUFFER_SIZE_IN_BYTES); for (int i = 0; i < 3 * ccCount; i++) { - holder.data.put(buffer.get(pos + i)); + holder.data.put(buffer.get(pos++)); } holder.timeUs = presentationTimeUs; mPendingCcSamples.add(holder); + return pos; } } private class Mpeg2CcParser extends CcParser { + private static final int PATTERN_LENGTH = 9; + @Override public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { - int pos = 0; - while (pos + 9 < buffer.position()) { - // Find the start prefix code of private user data. - if (buffer.get(pos) == 0 - && buffer.get(pos + 1) == 0 - && buffer.get(pos + 2) == 1 - && (buffer.get(pos + 3) & 0xff) == 0xb2) { - // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user - // identifier and user data type code 3. - if (buffer.get(pos + 4) == 'G' - && buffer.get(pos + 5) == 'A' - && buffer.get(pos + 6) == '9' - && buffer.get(pos + 7) == '4' - && buffer.get(pos + 8) == 3) { - parseClosedCaption(buffer, pos + 9, presentationTimeUs); + int totalSize = buffer.position(); + // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with + // overlapping to handle the case that the pattern exists in the boundary. + for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) { + buffer.position(i); + int size = Math.min(totalSize - i, mBuffer.length); + buffer.get(mBuffer, 0, size); + int j = 0; + while (j < size - PATTERN_LENGTH) { + // Find the start prefix code of private user data. + if (mBuffer[j] == 0 + && mBuffer[j + 1] == 0 + && mBuffer[j + 2] == 1 + && (mBuffer[j + 3] & 0xff) == 0xb2) { + // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user + // identifier and user data type code 3. + if (mBuffer[j + 4] == 'G' + && mBuffer[j + 5] == 'A' + && mBuffer[j + 6] == '9' + && mBuffer[j + 7] == '4' + && mBuffer[j + 8] == 3) { + j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH, + presentationTimeUs) - i; + } else { + j += PATTERN_LENGTH; + } + } else { + ++j; } - pos += 9; - } else { - ++pos; } } + buffer.position(totalSize); } } private class H264CcParser extends CcParser { + private static final int PATTERN_LENGTH = 14; + @Override public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { - int pos = 0; - while (pos + 7 < buffer.position()) { - // Find the start prefix code of a NAL Unit. - if (buffer.get(pos) == 0 - && buffer.get(pos + 1) == 0 - && buffer.get(pos + 2) == 1) { - int nalType = buffer.get(pos + 3) & 0x1f; - int payloadType = buffer.get(pos + 4) & 0xff; - - // ATSC closed caption data embedded in H264 private user data has NAL type 6, - // payload type 4, and 'GA94' user identifier for ATSC. - if (nalType == 6 && payloadType == 4 && buffer.get(pos + 9) == 'G' - && buffer.get(pos + 10) == 'A' - && buffer.get(pos + 11) == '9' - && buffer.get(pos + 12) == '4') { - parseClosedCaption(buffer, pos + 14, presentationTimeUs); + int totalSize = buffer.position(); + // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with + // overlapping to handle the case that the pattern exists in the boundary. + for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) { + buffer.position(i); + int size = Math.min(totalSize - i, mBuffer.length); + buffer.get(mBuffer, 0, size); + int j = 0; + while (j < size - PATTERN_LENGTH) { + // Find the start prefix code of a NAL Unit. + if (mBuffer[j] == 0 + && mBuffer[j + 1] == 0 + && mBuffer[j + 2] == 1) { + int nalType = mBuffer[j + 3] & 0x1f; + int payloadType = mBuffer[j + 4] & 0xff; + + // ATSC closed caption data embedded in H264 private user data has NAL type + // 6, payload type 4, and 'GA94' user identifier for ATSC. + if (nalType == 6 && payloadType == 4 && mBuffer[j + 9] == 'G' + && mBuffer[j + 10] == 'A' + && mBuffer[j + 11] == '9' + && mBuffer[j + 12] == '4') { + j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH, + presentationTimeUs) - i; + } else { + j += 7; + } + } else { + ++j; } - pos += 7; - } else { - ++pos; } } + buffer.position(totalSize); } } } diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java index 45b78bd1..6007b0be 100644 --- a/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java +++ b/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java @@ -154,6 +154,9 @@ public final class MpegTsSampleSource implements SampleSource, SampleSourceReade if (mPreparationError != null) { throw mPreparationError; } + if (mSampleExtractor != null) { + mSampleExtractor.maybeThrowError(); + } } @Override diff --git a/src/com/android/tv/tuner/exoplayer/SampleExtractor.java b/src/com/android/tv/tuner/exoplayer/SampleExtractor.java index 47790ccb..543588c7 100644 --- a/src/com/android/tv/tuner/exoplayer/SampleExtractor.java +++ b/src/com/android/tv/tuner/exoplayer/SampleExtractor.java @@ -41,10 +41,17 @@ import java.util.List; public interface SampleExtractor { /** + * If the extractor is currently having difficulty preparing or loading samples, then this + * method throws the underlying error. Otherwise does nothing. + * + * @throws IOException The underlying error. + */ + void maybeThrowError() throws IOException; + + /** * Prepares the extractor for reading track metadata and samples. * - * @return whether the source is ready; if {@code false}, {@link #prepare()} must be called - * again + * @return whether the source is ready; if {@code false}, this method must be called again. * @throws IOException thrown if the source can't be read */ boolean prepare() throws IOException; @@ -109,12 +116,12 @@ public interface SampleExtractor { * @param listener the OnCompletionListener * @param handler the {@link Handler} for {@link Handler#post(Runnable)} of OnCompletionListener */ - public void setOnCompletionListener(OnCompletionListener listener, Handler handler); + void setOnCompletionListener(OnCompletionListener listener, Handler handler); /** * The listener for SampleExtractor being completed. */ - public interface OnCompletionListener { + interface OnCompletionListener { /** * Called when sample extraction is completed. diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java index 434b46af..9dae2e34 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java +++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java @@ -37,6 +37,7 @@ import com.android.tv.tuner.tvinput.TunerDebug; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; /** * Decodes and renders AC3 audio. @@ -105,6 +106,7 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC private long mInterpolatedTimeUs; private long mPreviousPositionUs; private boolean mIsStopped; + private ArrayList<Integer> mTracksIndex; public Ac3PassthroughTrackRenderer(SampleSource source, Handler eventHandler, EventListener listener) { @@ -120,6 +122,7 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC mCodecCounters = new CodecCounters(); mMonitor = new AudioTrackMonitor(); mAudioClock = new AudioClock(); + mTracksIndex = new ArrayList<>(); } @Override @@ -139,8 +142,10 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC } for (int i = 0; i < mSource.getTrackCount(); i++) { if (handlesMimeType(mSource.getFormat(i).mimeType)) { - mTrackIndex = i; - return true; + if (mTrackIndex < 0) { + mTrackIndex = i; + } + mTracksIndex.add(i); } } @@ -150,18 +155,19 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC @Override protected int getTrackCount() { - return mTrackIndex < 0 ? 0 : 1; + return mTracksIndex.size(); } @Override protected MediaFormat getFormat(int track) { - Assertions.checkArgument(mTrackIndex != -1 && track == 0); - return mSource.getFormat(mTrackIndex); + Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); + return mSource.getFormat(mTracksIndex.get(track)); } @Override protected void onEnabled(int track, long positionUs, boolean joining) { - Assertions.checkArgument(mTrackIndex != -1 && track == 0); + Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); + mTrackIndex = mTracksIndex.get(track); mSource.enable(mTrackIndex, positionUs); seekToInternal(positionUs); } diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java index 4c46021f..2bf86b5a 100644 --- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java +++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java @@ -16,17 +16,12 @@ package com.android.tv.tuner.exoplayer.ac3; -import android.media.MediaCodec; import android.os.Handler; -import android.util.Log; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecSelector; import com.google.android.exoplayer.SampleSource; -import com.android.tv.tuner.TunerFlags; - -import java.nio.ByteBuffer; /** * MPEG-2 TS audio track renderer. @@ -40,10 +35,6 @@ public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer { private final String TAG = "Ac3TrackRenderer"; private final boolean DEBUG = false; - private int mZeroPresentationTimeCount; - private Long mPresentationTimeOffset; - private boolean mPresentationTimeOffsetNeeded; - private final MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); private final Ac3EventListener mListener; public interface Ac3EventListener extends EventListener { @@ -63,72 +54,6 @@ public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer { } @Override - protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { - super.onDiscontinuity(positionUs); - if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) { - return; - } - if (DEBUG) Log.d(TAG, "onDiscontinuity(), positionUs = " + positionUs); - mZeroPresentationTimeCount = 0; - mPresentationTimeOffset = null; - mPresentationTimeOffsetNeeded = false; - } - - @Override - protected void onQueuedInputBuffer(long presentationTimeUs, ByteBuffer buffer, - int bufferSize, boolean sampleEncrypted) { - if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) { - return; - } - if (DEBUG) Log.d(TAG, "onQueuedInputBuffer(), presentationTimeUs = " + presentationTimeUs); - if (presentationTimeUs == 0) { - // A sequence of consecutive zero presentation times indicate - // the starting of a data stream. - // Count the number of leading zeros - mZeroPresentationTimeCount++; - // Start waiting for the first non-zero presentation time. - mPresentationTimeOffsetNeeded = true; - } else if (mPresentationTimeOffset == null && mPresentationTimeOffsetNeeded) { - // Sets time offset based on the first non-zero presentation timestamp, - // which is the first timestamp we can trust. - mPresentationTimeOffset = mZeroPresentationTimeCount - * Ac3PassthroughTrackRenderer.AC3_SAMPLE_DURATION_US - - presentationTimeUs; - } - - } - - @Override - protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, - ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, - boolean shouldSkip) throws ExoPlaybackException { - if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) { - return super.processOutputBuffer(positionUs, elapsedRealtimeUs, codec, buffer, - bufferInfo, bufferIndex, shouldSkip); - } - if (mPresentationTimeOffset != null) { - // Adjust the presentation time. We don't modify the given {@code bufferInfo} here since - // this method can be called multiple times with the same buffer. - long presentationTimeUs = - Math.max(bufferInfo.presentationTimeUs - mPresentationTimeOffset, 0); - mBufferInfo.set(bufferInfo.offset, bufferInfo.size, - presentationTimeUs, bufferInfo.flags); - } else { - mBufferInfo.set(bufferInfo.offset, bufferInfo.size, - bufferInfo.presentationTimeUs, bufferInfo.flags); - } - try { - return super.processOutputBuffer(positionUs, elapsedRealtimeUs, codec, buffer, - mBufferInfo, bufferIndex, shouldSkip); - } catch (IllegalArgumentException e) { - if (isAudioTrackSetPlaybackParamsError(e)) { - notifyAudioTrackSetPlaybackParamsError(e); - } - return false; - } - } - - @Override public void handleMessage(int messageType, Object message) throws ExoPlaybackException { if (messageType == MSG_SET_PLAYBACK_PARAMS) { try { diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java index 8cf80420..eb596e93 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java @@ -81,6 +81,7 @@ public class BufferManager { private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK; private long mTotalWriteSize; private long mTotalWriteTimeNs; + private float mWriteBandwidth = 0.0f; private volatile int mSpeedCheckCount; private boolean mDisabled = false; @@ -555,7 +556,8 @@ public class BufferManager { } } - private void resetWriteStat() { + private void resetWriteStat(float writeBandwidth) { + mWriteBandwidth = writeBandwidth; mTotalWriteSize = 0; mTotalWriteTimeNs = 0; } @@ -585,8 +587,8 @@ public class BufferManager { return false; } mSpeedCheckCount++; - float megabytePerSecond = getWriteBandwidth(); - resetWriteStat(); + float megabytePerSecond = calculateWriteBandwidth(); + resetWriteStat(megabytePerSecond); if (DEBUG) { Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps"); } @@ -594,9 +596,14 @@ public class BufferManager { } /** - * Returns the disk write speed in megabytes per second. + * Returns recent write bandwidth in MBps. If recent bandwidth is not available, + * returns {float -1.0f}. */ - private float getWriteBandwidth() { + public float getWriteBandwidth() { + return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth; + } + + private float calculateWriteBandwidth() { if (mTotalWriteTimeNs == 0) { return -1; } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java index f9efa0de..6a0502a7 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java @@ -61,8 +61,11 @@ public class DvrStorageManager implements BufferManager.StorageManager { @Override public void clearStorage() { if (mIsRecording) { - for (File file : mBufferDir.listFiles()) { - file.delete(); + File[] files = mBufferDir.listFiles(); + if (files != null && files.length > 0) { + for (File file : files) { + file.delete(); + } } } } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java index 9243c568..4869b49f 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java @@ -183,12 +183,16 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer, @Override public void handleWriteSpeedSlow() throws IOException{ - Log.w(TAG, "Disk is too slow for recording"); if (mBufferReason == BUFFER_REASON_RECORDING) { - // Stops the recording immediately. - throw new IOException("Write bandwidth is not enough"); + // Recording does not need to stop because I/O speed is slow temporarily. + // If fixed size buffer of TsStreamer overflows, TsDataSource will reach EoS. + // Reaching EoS will stop recording eventually. + Log.w(TAG, "Disk I/O speed is slow for recording temporarily: " + + mBufferManager.getWriteBandwidth() + "MBps"); + return; } // Disables buffering samples afterwards, and notifies the disk speed is slow. + Log.w(TAG, "Disk is too slow for trickplay"); mBufferManager.disable(); mBufferListener.onDiskTooSlow(); } diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java index d8276059..37ae4022 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java @@ -27,6 +27,7 @@ import android.util.Pair; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.util.MimeTypes; +import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason; import java.io.IOException; @@ -65,6 +66,7 @@ public class SampleChunkIoHelper implements Handler.Callback { private final SampleChunk.IoState[] mReadIoStates; private final SampleChunk.IoState[] mWriteIoStates; private long mBufferDurationUs = 0; + private boolean mWriteEnded; private boolean mErrorNotified; private boolean mFinished; @@ -150,6 +152,7 @@ public class SampleChunkIoHelper implements Handler.Callback { for (int i = 0; i < mTrackCount; ++i) { mBufferManager.loadTrackFromStorage(mIds.get(i), mSamplePool); } + mWriteEnded = true; } else { for (int i = 0; i < mTrackCount; ++i) { mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_WRITE, i)); @@ -289,6 +292,12 @@ public class SampleChunkIoHelper implements Handler.Callback { int index = params.index; mIoHandler.removeMessages(MSG_READ, index); SampleChunk chunk = mBufferManager.getReadFile(mIds.get(index), params.positionUs); + if (chunk == null) { + String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs + + "is not found"; + SoftPreconditions.checkNotNull(chunk, TAG, errorMessage); + throw new IOException(errorMessage); + } mReadIoStates[index].openRead(chunk); if (mHandlerReadSampleBuffers[index] != null) { SampleHolder sample; @@ -314,6 +323,11 @@ public class SampleChunkIoHelper implements Handler.Callback { mIoHandler.obtainMessage(MSG_READ, index), READ_RESCHEDULING_DELAY_MS); } else { if (mReadIoStates[index].isReadFinished()) { + for (int i = 0; i < mTrackCount; ++i) { + if (!mReadIoStates[i].isReadFinished()) { + return; + } + } mIoCallback.onIoReachedEos(); return; } @@ -332,6 +346,10 @@ public class SampleChunkIoHelper implements Handler.Callback { private void doWrite(IoParams params) throws IOException { try { + if (mWriteEnded) { + SoftPreconditions.checkState(false); + return; + } int index = params.index; SampleHolder sample = params.sample; SampleChunk nextChunk = null; @@ -355,6 +373,10 @@ public class SampleChunkIoHelper implements Handler.Callback { } private void doCloseWrite() throws IOException { + if (mWriteEnded) { + return; + } + mWriteEnded = true; boolean readFinished = true; for (int i = 0; i < mTrackCount; ++i) { readFinished = readFinished && mReadIoStates[i].isReadFinished(); diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java index 9051b73b..258a5cd0 100644 --- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java +++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java @@ -18,6 +18,8 @@ package com.android.tv.tuner.exoplayer.buffer; import android.content.Context; import android.media.MediaFormat; +import android.os.AsyncTask; +import android.os.Looper; import android.provider.Settings; import android.util.Pair; @@ -64,8 +66,24 @@ public class TrickplayStorageManager implements BufferManager.StorageManager { @Override public void clearStorage() { - for (File file : mBufferDir.listFiles()) { - file.delete(); + File files[] = mBufferDir.listFiles(); + if (files == null || files.length == 0) { + return; + } + if (Looper.myLooper() == Looper.getMainLooper()) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + for (File file : files) { + file.delete(); + } + return null; + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + for (File file : files) { + file.delete(); + } } } diff --git a/src/com/android/tv/tuner/setup/ScanFragment.java b/src/com/android/tv/tuner/setup/ScanFragment.java index b286cff9..4b3ffe40 100644 --- a/src/com/android/tv/tuner/setup/ScanFragment.java +++ b/src/com/android/tv/tuner/setup/ScanFragment.java @@ -46,7 +46,7 @@ import com.android.tv.tuner.data.Channel; import com.android.tv.tuner.data.PsipData; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.source.FileTsStreamer; -import com.android.tv.tuner.source.TsMediaDataSource; +import com.android.tv.tuner.source.TsDataSource; import com.android.tv.tuner.source.TsStreamer; import com.android.tv.tuner.source.TunerTsStreamer; import com.android.tv.tuner.tvinput.ChannelDataManager; @@ -120,6 +120,7 @@ public class ScanFragment extends SetupFragment { } }); Bundle args = getArguments(); + // TODO: Handle the case when the fragment is restored. startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0)); TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title); if (TunerInputInfoUtils.isBuiltInTuner(getActivity())){ @@ -147,8 +148,10 @@ public class ScanFragment extends SetupFragment { @Override public void onDetach() { - // Ensure scan task will stop. - mChannelScanTask.stopScan(); + if (mChannelScanTask != null) { + // Ensure scan task will stop. + mChannelScanTask.stopScan(); + } super.onDetach(); } @@ -231,7 +234,7 @@ public class ScanFragment extends SetupFragment { } private class ChannelScanTask extends AsyncTask<Void, Integer, Void> - implements EventDetector.EventListener { + implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener { private static final int MAX_PROGRESS = 100; private final Activity mActivity; @@ -260,6 +263,7 @@ public class ScanFragment extends SetupFragment { } mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this) : null; mConditionStopped = new ConditionVariable(); + mChannelDataManager.setChannelScanListener(this, new Handler()); } private void maybeSetChannelListVisible() { @@ -302,28 +306,10 @@ public class ScanFragment extends SetupFragment { mScanChannelList.addAll(ChannelScanFileParser.parseScanFile( getResources().openRawResource(mChannelMapId))); scanChannels(); - mChannelDataManager.setCurrentVersion(mActivity); - mChannelDataManager.release(); return null; } @Override - protected void onPostExecute(Void result) { - mIsFinished = true; - TunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(), - mChannelDataManager.getScannedChannelCount()); - // Cancel a previously shown recommendation card. - TunerSetupActivity.cancelRecommendationCard(mActivity.getApplicationContext()); - // Mark scan as done - TunerPreferences.setScanDone(mActivity.getApplicationContext()); - // finishing will be done manually. - if (mFinishingProgressDialog != null) { - mFinishingProgressDialog.dismiss(); - } - onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH); - } - - @Override protected void onCancelled() { SoftPreconditions.checkState(false, TAG, "call cancelScan instead of cancel"); } @@ -404,7 +390,7 @@ public class ScanFragment extends SetupFragment { tunerChannel.getProgramNumber())); tunerChannel.setVirtualMajor(scanChannel.radioFrequencyNumber); tunerChannel.setVirtualMinor(tunerChannel.getProgramNumber()); - mChannelDataManager.notifyChannelDetected(tunerChannel, true); + onChannelDetected(tunerChannel, true); } } } @@ -452,6 +438,25 @@ public class ScanFragment extends SetupFragment { getString(R.string.ut_setup_cancel), true, false); } } + + @Override + public void onChannelHandlingDone() { + mChannelDataManager.setCurrentVersion(mActivity); + mChannelDataManager.releaseSafely(); + mIsFinished = true; + TunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(), + mChannelDataManager.getScannedChannelCount()); + // Cancel a previously shown recommendation card. + TunerSetupActivity.cancelRecommendationCard(mActivity.getApplicationContext()); + // Mark scan as done + TunerPreferences.setScanDone(mActivity.getApplicationContext()); + // finishing will be done manually. + if (mFinishingProgressDialog != null) { + mFinishingProgressDialog.dismiss(); + } + onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH); + mChannelScanTask = null; + } } private static class FakeTsStreamer implements TsStreamer { @@ -493,7 +498,7 @@ public class ScanFragment extends SetupFragment { } @Override - public TsMediaDataSource createMediaDataSource() { + public TsDataSource createDataSource() { return null; } } diff --git a/src/com/android/tv/tuner/setup/ScanResultFragment.java b/src/com/android/tv/tuner/setup/ScanResultFragment.java index 757eeddd..068543cd 100644 --- a/src/com/android/tv/tuner/setup/ScanResultFragment.java +++ b/src/com/android/tv/tuner/setup/ScanResultFragment.java @@ -38,12 +38,6 @@ public class ScanResultFragment extends SetupMultiPaneFragment { public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanResultFragment"; - /** - * An action which moves to previous page when the user presses BACK button. - * In some cases, more than one page can be popped out. - */ - public static final int ACTION_BACK_TO_CONNECTION_TYPE = ACTION_DONE - 1; - @Override protected SetupGuidedStepFragment onCreateContentFragment() { return new ContentFragment(); diff --git a/src/com/android/tv/tuner/setup/TunerSetupActivity.java b/src/com/android/tv/tuner/setup/TunerSetupActivity.java index 8359ce73..78121bc5 100644 --- a/src/com/android/tv/tuner/setup/TunerSetupActivity.java +++ b/src/com/android/tv/tuner/setup/TunerSetupActivity.java @@ -31,7 +31,9 @@ import android.graphics.BitmapFactory; import android.media.tv.TvContract; import android.os.Bundle; import android.support.v4.app.NotificationCompat; +import android.util.Log; import android.view.KeyEvent; +import android.widget.Toast; import com.android.tv.TvApplication; import com.android.tv.common.TvCommonConstants; @@ -40,6 +42,7 @@ import com.android.tv.common.ui.setup.SetupActivity; import com.android.tv.common.ui.setup.SetupFragment; import com.android.tv.common.ui.setup.SetupMultiPaneFragment; import com.android.tv.tuner.R; +import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.tvinput.TunerTvInputService; import com.android.tv.tuner.util.TunerInputInfoUtils; @@ -48,6 +51,7 @@ import com.android.tv.tuner.util.TunerInputInfoUtils; * An activity that serves tuner setup process. */ public class TunerSetupActivity extends SetupActivity { + private final String TAG = "TunerSetupActivity"; // For the recommendation card private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity"; private static final String NOTIFY_TAG = "TunerSetup"; @@ -107,9 +111,23 @@ public class TunerSetupActivity extends SetupActivity { } return true; case ConnectionTypeFragment.ACTION_CATEGORY: + TunerHal hal = TunerHal.createInstance(getApplicationContext()); + if (hal == null) { + finish(); + Toast.makeText(getApplicationContext(), + R.string.ut_channel_scan_tuner_unavailable,Toast.LENGTH_LONG).show(); + return true; + } + try { + hal.close(); + } catch (Exception e) { + Log.e(TAG, "Tuner hal close failed", e); + return true; + } mLastScanFragment = new ScanFragment(); Bundle args = new Bundle(); - args.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, CHANNEL_MAP_SCAN_FILE[actionId]); + args.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, + CHANNEL_MAP_SCAN_FILE[actionId]); mLastScanFragment.setArguments(args); showFragment(mLastScanFragment, true); return true; diff --git a/src/com/android/tv/tuner/source/FileTsStreamer.java b/src/com/android/tv/tuner/source/FileTsStreamer.java index 617440bc..14997ee4 100644 --- a/src/com/android/tv/tuner/source/FileTsStreamer.java +++ b/src/com/android/tv/tuner/source/FileTsStreamer.java @@ -20,9 +20,10 @@ import android.os.Environment; import android.util.Log; import android.util.SparseBooleanArray; +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.upstream.DataSpec; import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.ChannelScanFileParser.ScanChannel; -import com.android.tv.tuner.TunerFlags; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.ts.TsParser; import com.android.tv.tuner.tvinput.EventDetector; @@ -67,31 +68,17 @@ public class FileTsStreamer implements TsStreamer { private Thread mStreamingThread; private StreamProvider mSource; - public static class FileMediaDataSource extends TsMediaDataSource { + public static class FileDataSource extends TsDataSource { private final FileTsStreamer mTsStreamer; private final AtomicLong mLastReadPosition = new AtomicLong(0); private long mStartBufferedPosition; - private FileMediaDataSource(FileTsStreamer tsStreamer) { + private FileDataSource(FileTsStreamer tsStreamer) { mTsStreamer = tsStreamer; mStartBufferedPosition = tsStreamer.getBufferedPosition(); } @Override - public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException { - int ret = mTsStreamer.readAt(mStartBufferedPosition + pos, buffer, offset, amount); - if (ret > 0) { - mLastReadPosition.set(pos + ret); - } - return ret; - } - - @Override - public long getSize() { - return -1L; - } - - @Override public long getBufferedPosition() { return mTsStreamer.getBufferedPosition() - mStartBufferedPosition; } @@ -108,10 +95,25 @@ public class FileTsStreamer implements TsStreamer { mStartBufferedPosition += offset; } - // This will be called by {@link MediaExtractor} + @Override + public long open(DataSpec dataSpec) throws IOException { + mLastReadPosition.set(0); + return C.LENGTH_UNBOUNDED; + } + @Override public void close() { } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + int ret = mTsStreamer.readAt(mStartBufferedPosition + mLastReadPosition.get(), buffer, + offset, readLength); + if (ret > 0) { + mLastReadPosition.addAndGet(ret); + } + return ret; + } } /** @@ -154,13 +156,8 @@ public class FileTsStreamer implements TsStreamer { } mEventDetector.start(mSource, channel.getProgramNumber()); mSource.addPidFilter(channel.getVideoPid()); - if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) { - // ExoPlayer's extractor expects all the tracks which belong to the channel. - for (Integer i : channel.getAudioPids()) { - mSource.addPidFilter(i); - } - } else { - mSource.addPidFilter(channel.getAudioPid()); + for (Integer i : channel.getAudioPids()) { + mSource.addPidFilter(i); } mSource.addPidFilter(channel.getPcrPid()); mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID); @@ -199,8 +196,8 @@ public class FileTsStreamer implements TsStreamer { } @Override - public TsMediaDataSource createMediaDataSource() { - return new FileMediaDataSource(this); + public TsDataSource createDataSource() { + return new FileDataSource(this); } /** diff --git a/src/com/android/tv/tuner/source/TsMediaDataSource.java b/src/com/android/tv/tuner/source/TsDataSource.java index 1bef78fe..2ce3e670 100644 --- a/src/com/android/tv/tuner/source/TsMediaDataSource.java +++ b/src/com/android/tv/tuner/source/TsDataSource.java @@ -16,12 +16,12 @@ package com.android.tv.tuner.source; -import android.media.MediaDataSource; +import com.google.android.exoplayer.upstream.DataSource; /** - * {@link MediaDatasource} for MPEG-TS stream, which will be used by {@link MediaExtractor}. + * {@link DataSource} for MPEG-TS stream, which will be used by {@link TsExtractor}. */ -public abstract class TsMediaDataSource extends MediaDataSource { +public abstract class TsDataSource implements DataSource { /** * Returns the number of bytes being buffered by {@link TsStreamer} so far. @@ -33,7 +33,7 @@ public abstract class TsMediaDataSource extends MediaDataSource { } /** - * Returns the offset position where the last {@link MediaDataSource#readAt} read. + * Returns the offset position where the last {@link DataSource#read} read. * * @return the last read position */ diff --git a/src/com/android/tv/tuner/source/TsMediaDataSourceManager.java b/src/com/android/tv/tuner/source/TsDataSourceManager.java index d6c3c3b1..7286cd8c 100644 --- a/src/com/android/tv/tuner/source/TsMediaDataSourceManager.java +++ b/src/com/android/tv/tuner/source/TsDataSourceManager.java @@ -17,7 +17,6 @@ package com.android.tv.tuner.source; import android.content.Context; -import android.media.MediaDataSource; import com.android.tv.tuner.data.Channel; import com.android.tv.tuner.data.TunerChannel; @@ -27,15 +26,15 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** - * Manages {@link TsMediaDataSource} for playback and recording. + * Manages {@link DataSource} for playback and recording. * The class hides handling of {@link TunerHal} and {@link TsStreamer} from other classes. - * One TsMediaDataSourceManager should be created for per session. + * One TsDataSourceManager should be created for per session. */ -public class TsMediaDataSourceManager { - private static String TAG = "TsMediaDataSourceManager"; +public class TsDataSourceManager { + private static String TAG = "TsDataSourceManager"; private static final Object sLock = new Object(); - private static final Map<TsMediaDataSource, TsStreamer> sTsStreamers = + private static final Map<TsDataSource, TsStreamer> sTsStreamers = new ConcurrentHashMap<>(); private static int sSequenceId; @@ -48,33 +47,33 @@ public class TsMediaDataSourceManager { private boolean mKeepTuneStatus; /** - * Creates TsMediaDataSourceManager to create and release {@link MediaDataSource} which will be + * Creates TsDataSourceManager to create and release {@link DataSource} which will be * used for playing and recording. * @param isRecording {@code true} when for recording, {@code false} otherwise - * @return {@link TsMediaDataSourceManager} + * @return {@link TsDataSourceManager} */ - public static TsMediaDataSourceManager createSourceManager(boolean isRecording) { + public static TsDataSourceManager createSourceManager(boolean isRecording) { int id; synchronized (sLock) { id = ++sSequenceId; } - return new TsMediaDataSourceManager(id, isRecording); + return new TsDataSourceManager(id, isRecording); } - private TsMediaDataSourceManager(int id, boolean isRecording) { + private TsDataSourceManager(int id, boolean isRecording) { mId = id; mIsRecording = isRecording; mKeepTuneStatus = true; } /** - * Creates or retrieves {@link TsMediaDataSource} for playing or recording + * Creates or retrieves {@link TsDataSource} for playing or recording * @param context a {@link Context} instance * @param channel to play or record * @param eventListener for program information which will be scanned from MPEG2-TS stream - * @return {@link TsMediaDataSource} which will provide the specified channel stream + * @return {@link TsDataSource} which will provide the specified channel stream */ - public TsMediaDataSource createDataSource(Context context, TunerChannel channel, + public TsDataSource createDataSource(Context context, TunerChannel channel, EventDetector.EventListener eventListener) { if (channel.getType() == Channel.TYPE_FILE) { // MPEG2 TS captured stream file recording is not supported. @@ -83,7 +82,7 @@ public class TsMediaDataSourceManager { } FileTsStreamer streamer = new FileTsStreamer(eventListener); if (streamer.startStream(channel)) { - TsMediaDataSource source = streamer.createMediaDataSource(); + TsDataSource source = streamer.createDataSource(); sTsStreamers.put(source, streamer); return source; } @@ -94,14 +93,14 @@ public class TsMediaDataSourceManager { } /** - * Releases the specified {@link MediaDataSource} and underlying {@link TunerHal}. + * Releases the specified {@link TsDataSource} and underlying {@link TunerHal}. * @param source to release */ - public void releaseDataSource(TsMediaDataSource source) { - if (source instanceof TunerTsStreamer.TunerMediaDataSource) { + public void releaseDataSource(TsDataSource source) { + if (source instanceof TunerTsStreamer.TunerDataSource) { mTunerStreamerManager.releaseDataSource( source, mId, !mIsRecording && mKeepTuneStatus); - } else if (source instanceof FileTsStreamer.FileMediaDataSource) { + } else if (source instanceof FileTsStreamer.FileDataSource) { FileTsStreamer streamer = (FileTsStreamer) sTsStreamers.get(source); if (streamer != null) { sTsStreamers.remove(source); diff --git a/src/com/android/tv/tuner/source/TsStreamWriter.java b/src/com/android/tv/tuner/source/TsStreamWriter.java new file mode 100644 index 00000000..30650555 --- /dev/null +++ b/src/com/android/tv/tuner/source/TsStreamWriter.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.source; + +import android.content.Context; +import android.util.Log; +import com.android.tv.tuner.data.TunerChannel; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +/** + * Stores TS files to the disk for debugging. + */ +public class TsStreamWriter { + private static final String TAG = "TsStreamWriter"; + private static final boolean DEBUG = false; + + private static final long TIME_LIMIT_MS = 10000; // 10s + private static final int NO_INSTANCE_ID = 0; + private static final int MAX_GET_ID_RETRY_COUNT = 5; + private static final int MAX_INSTANCE_ID = 10000; + private static final String SEPARATOR = "_"; + + private FileOutputStream mFileOutputStream; + private long mFileStartTimeMs; + private String mFileName = null; + private final String mDirectoryPath; + private final File mDirectory; + private final int mInstanceId; + private TunerChannel mChannel; + + public TsStreamWriter(Context context) { + File externalFilesDir = context.getExternalFilesDir(null); + if (externalFilesDir == null || !externalFilesDir.isDirectory()) { + mDirectoryPath = null; + mDirectory = null; + mInstanceId = NO_INSTANCE_ID; + if (DEBUG) { + Log.w(TAG, "Fail to get external files dir!"); + } + } else { + mDirectoryPath = externalFilesDir.getPath() + "/EngTsStream"; + mDirectory = new File(mDirectoryPath); + if (!mDirectory.exists()) { + boolean madeDir = mDirectory.mkdir(); + if (!madeDir) { + Log.w(TAG, "Error. Fail to create folder!"); + } + } + mInstanceId = generateInstanceId(); + } + } + + /** + * Sets the current channel. + * + * @param channel curren channel of the stream + */ + public void setChannel(TunerChannel channel) { + mChannel = channel; + } + + /** + * Opens a file to store TS data. + */ + public void openFile() { + if (mChannel == null || mDirectoryPath == null) { + return; + } + mFileStartTimeMs = System.currentTimeMillis(); + mFileName = mChannel.getDisplayNumber() + SEPARATOR + mFileStartTimeMs + SEPARATOR + + mInstanceId + ".ts"; + String filePath = mDirectoryPath + "/" + mFileName; + try { + mFileOutputStream = new FileOutputStream(filePath, false); + } catch (FileNotFoundException e) { + Log.w(TAG, "Cannot open file: " + filePath, e); + } + } + + /** + * Closes the file and stops storing TS data. + * + * @param calledWhenStopStream {@code true} if this method is called when the stream is stopped + * {@code false} otherwise + */ + public void closeFile(boolean calledWhenStopStream) { + if (mFileOutputStream == null) { + return; + } + try { + mFileOutputStream.close(); + deleteOutdatedFiles(calledWhenStopStream); + mFileName = null; + mFileOutputStream = null; + } catch (IOException e) { + Log.w(TAG, "Error on closing file.", e); + } + } + + /** + * Writes the data to the file. + * + * @param buffer the data to be written + * @param bytesWritten number of bytes written + */ + public void writeToFile(byte[] buffer, int bytesWritten) { + if (mFileOutputStream == null) { + return; + } + if (System.currentTimeMillis() - mFileStartTimeMs > TIME_LIMIT_MS) { + closeFile(false); + openFile(); + } + try { + mFileOutputStream.write(buffer, 0, bytesWritten); + } catch (IOException e) { + Log.w(TAG, "Error on writing TS stream.", e); + } + } + + /** + * Deletes outdated files to save storage. + * + * @param deleteAll {@code true} if all the files with the relative ID should be deleted + * {@code false} if the most recent file should not be deleted + */ + private void deleteOutdatedFiles(boolean deleteAll) { + if (mFileName == null) { + return; + } + if (mDirectory == null || !mDirectory.isDirectory()) { + Log.e(TAG, "Error. The folder doesn't exist!"); + return; + } + if (mFileName == null) { + Log.e(TAG, "Error. The current file name is null!"); + return; + } + for (File file : mDirectory.listFiles()) { + if (file.isFile() && getFileId(file) == mInstanceId + && (deleteAll || !mFileName.equals(file.getName()))) { + boolean deleted = file.delete(); + if (DEBUG && !deleted) { + Log.w(TAG, "Failed to delete " + file.getName()); + } + } + } + } + + /** + * Generates a unique instance ID. + * + * @return a unique instance ID + */ + private int generateInstanceId() { + if (mDirectory == null) { + return NO_INSTANCE_ID; + } + Set<Integer> idSet = getExistingIds(); + if (idSet == null) { + return NO_INSTANCE_ID; + } + for (int i = 0; i < MAX_GET_ID_RETRY_COUNT; i++) { + // Range [1, MAX_INSTANCE_ID] + int id = (int)Math.floor(Math.random() * MAX_INSTANCE_ID) + 1; + if (!idSet.contains(id)) { + return id; + } + } + return NO_INSTANCE_ID; + } + + /** + * Gets all existing instance IDs. + * + * @return a set of all existing instance IDs + */ + private Set<Integer> getExistingIds() { + if (mDirectory == null || !mDirectory.isDirectory()) { + return null; + } + + Set<Integer> idSet = new HashSet<>(); + for (File file : mDirectory.listFiles()) { + int id = getFileId(file); + if(id != NO_INSTANCE_ID) { + idSet.add(id); + } + } + return idSet; + } + + /** + * Gets the instance ID of a given file. + * + * @param file the file whose TsStreamWriter ID is returned + * @return the TsStreamWriter ID of the file or NO_INSTANCE_ID if not available + */ + private static int getFileId(File file) { + if (file == null || !file.isFile()) { + return NO_INSTANCE_ID; + } + String fileName = file.getName(); + int lastSeparator = fileName.lastIndexOf(SEPARATOR); + if (!fileName.endsWith(".ts") || lastSeparator == -1) { + return NO_INSTANCE_ID; + } + try { + return Integer.parseInt(fileName.substring(lastSeparator + 1, fileName.length() - 3)); + } catch (NumberFormatException e) { + if (DEBUG) { + Log.e(TAG, fileName + " is not a valid file name."); + } + } + return NO_INSTANCE_ID; + } +} diff --git a/src/com/android/tv/tuner/source/TsStreamer.java b/src/com/android/tv/tuner/source/TsStreamer.java index cc28fc44..1ac950bb 100644 --- a/src/com/android/tv/tuner/source/TsStreamer.java +++ b/src/com/android/tv/tuner/source/TsStreamer.java @@ -46,11 +46,11 @@ public interface TsStreamer { void stopStream(); /** - * Creates {@link TsMediaDataSource} which will provide MPEG-2 TS stream for + * Creates {@link TsDataSource} which will provide MPEG-2 TS stream for * {@link android.media.MediaExtractor}. The source will start from the position * where it is created. * - * @return {@link TsMediaDataSource} + * @return {@link TsDataSource} */ - TsMediaDataSource createMediaDataSource(); + TsDataSource createDataSource(); } diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java index c756c381..b24048e6 100644 --- a/src/com/android/tv/tuner/source/TunerTsStreamer.java +++ b/src/com/android/tv/tuner/source/TunerTsStreamer.java @@ -16,11 +16,15 @@ package com.android.tv.tuner.source; +import android.content.Context; import android.util.Log; +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.upstream.DataSpec; import com.android.tv.common.SoftPreconditions; import com.android.tv.tuner.ChannelScanFileParser; import com.android.tv.tuner.TunerHal; +import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.tvinput.EventDetector; import com.android.tv.tuner.tvinput.EventDetector.EventListener; @@ -42,11 +46,6 @@ public class TunerTsStreamer implements TsStreamer { private static final int READ_TIMEOUT_MS = 5000; // 5 secs. private static final int BUFFER_UNDERRUN_SLEEP_MS = 10; - private static final int BUFFER_KEY_VERSION = 1; - - // UTCK stands for USB Tuner Buffer Key. - private static final String BUFFER_KEY_PREFIX = "UTCK"; - private final Object mCircularBufferMonitor = new Object(); private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE]; private long mBytesFetched; @@ -59,31 +58,19 @@ public class TunerTsStreamer implements TsStreamer { private Thread mStreamingThread; private final EventDetector mEventDetector; - public static class TunerMediaDataSource extends TsMediaDataSource { + private final TsStreamWriter mTsStreamWriter; + + public static class TunerDataSource extends TsDataSource { private final TunerTsStreamer mTsStreamer; private final AtomicLong mLastReadPosition = new AtomicLong(0); private long mStartBufferedPosition; - private TunerMediaDataSource(TunerTsStreamer tsStreamer) { + private TunerDataSource(TunerTsStreamer tsStreamer) { mTsStreamer = tsStreamer; mStartBufferedPosition = tsStreamer.getBufferedPosition(); } @Override - public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException { - int ret = mTsStreamer.readAt(mStartBufferedPosition + pos, buffer, offset, amount); - if (ret > 0) { - mLastReadPosition.set(pos + ret); - } - return ret; - } - - @Override - public long getSize() { - return -1L; - } - - @Override public long getBufferedPosition() { return mTsStreamer.getBufferedPosition() - mStartBufferedPosition; } @@ -100,19 +87,40 @@ public class TunerTsStreamer implements TsStreamer { mStartBufferedPosition += offset; } - // This will be called by {@link MediaExtractor} + @Override + public long open(DataSpec dataSpec) throws IOException { + mLastReadPosition.set(0); + return C.LENGTH_UNBOUNDED; + } + @Override public void close() { } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + int ret = mTsStreamer.readAt(mStartBufferedPosition + mLastReadPosition.get(), buffer, + offset, readLength); + if (ret > 0) { + mLastReadPosition.addAndGet(ret); + } + return ret; + } } /** * Creates {@link TsStreamer} for playing or recording the specified channel. * @param tunerHal the HAL for tuner device * @param eventListener the listener for channel & program information */ - public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) { + public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) { mTunerHal = tunerHal; mEventDetector = new EventDetector(mTunerHal, eventListener); + mTsStreamWriter = context != null && TunerPreferences.getStoreTsStream(context) ? + new TsStreamWriter(context) : null; + } + + public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) { + this(tunerHal, eventListener, null); } @Override @@ -122,9 +130,16 @@ public class TunerTsStreamer implements TsStreamer { mTunerHal.addPidFilter(channel.getVideoPid(), TunerHal.FILTER_TYPE_VIDEO); } - if (channel.hasAudio()) { - mTunerHal.addPidFilter(channel.getAudioPid(), - TunerHal.FILTER_TYPE_AUDIO); + boolean audioFilterSet = false; + for (Integer audioPid : channel.getAudioPids()) { + if (!audioFilterSet) { + mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_AUDIO); + audioFilterSet = true; + } else { + // FILTER_TYPE_AUDIO overrides the previous filter for audio. We use + // FILTER_TYPE_OTHER from the secondary one to get the all audio tracks. + mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_OTHER); + } } mTunerHal.addPidFilter(channel.getPcrPid(), TunerHal.FILTER_TYPE_PCR); @@ -143,6 +158,10 @@ public class TunerTsStreamer implements TsStreamer { mLastReadPosition.set(0L); mEndOfStreamSent = false; } + if (mTsStreamWriter != null) { + mTsStreamWriter.setChannel(mChannel); + mTsStreamWriter.openFile(); + } mStreamingThread = new StreamingThread(); mStreamingThread.start(); Log.i(TAG, "Streaming started"); @@ -193,11 +212,15 @@ public class TunerTsStreamer implements TsStreamer { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + if (mTsStreamWriter != null) { + mTsStreamWriter.closeFile(true); + mTsStreamWriter.setChannel(null); + } } @Override - public TsMediaDataSource createMediaDataSource() { - return new TunerMediaDataSource(this); + public TsDataSource createDataSource() { + return new TunerDataSource(this); } /** @@ -260,6 +283,10 @@ public class TunerTsStreamer implements TsStreamer { continue; } + if (mTsStreamWriter != null) { + mTsStreamWriter.writeToFile(dataBuffer, bytesWritten); + } + if (mEventDetector != null) { mEventDetector.feedTSStream(dataBuffer, 0, bytesWritten); } diff --git a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java index bd3fa5d6..cf1f6dcf 100644 --- a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java +++ b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java @@ -33,7 +33,7 @@ import java.util.Set; /** * Manages {@link TunerTsStreamer} for playback and recording. * The class hides handling of {@link TunerHal} from other classes. - * This class is used by {@link TsMediaDataSourceManager}. Don't use this class directly. + * This class is used by {@link TsDataSourceManager}. Don't use this class directly. */ class TunerTsStreamerManager { // The lock will protect mStreamerFinder, mSourceToStreamerMap and some part of TsStreamCreator @@ -42,7 +42,7 @@ class TunerTsStreamerManager { private final Object mCancelLock = new Object(); private final StreamerFinder mStreamerFinder = new StreamerFinder(); private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>(); - private final Map<TsMediaDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>(); + private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>(); private final TunerHalManager mTunerHalManager = new TunerHalManager(); private static TunerTsStreamerManager sInstance; @@ -59,7 +59,7 @@ class TunerTsStreamerManager { private TunerTsStreamerManager() { } - synchronized TsMediaDataSource createDataSource( + synchronized TsDataSource createDataSource( Context context, TunerChannel channel, EventDetector.EventListener listener, int sessionId, boolean reuse) { TsStreamerCreator creator; @@ -67,7 +67,7 @@ class TunerTsStreamerManager { if (mStreamerFinder.containsLocked(channel)) { mStreamerFinder.appendSessionLocked(channel, sessionId); TunerTsStreamer streamer = mStreamerFinder.getStreamerLocked(channel); - TsMediaDataSource source = streamer.createMediaDataSource(); + TsDataSource source = streamer.createDataSource(); mSourceToStreamerMap.put(source, streamer); return source; } @@ -82,7 +82,7 @@ class TunerTsStreamerManager { } if (!creator.isCancelledLocked()) { mStreamerFinder.putLocked(channel, sessionId, streamer); - TsMediaDataSource source = streamer.createMediaDataSource(); + TsDataSource source = streamer.createDataSource(); mSourceToStreamerMap.put(source, streamer); return source; } @@ -95,7 +95,7 @@ class TunerTsStreamerManager { return null; } - synchronized void releaseDataSource(TsMediaDataSource source, int sessionId, + synchronized void releaseDataSource(TsDataSource source, int sessionId, boolean reuse) { TunerTsStreamer streamer; synchronized (mCancelLock) { @@ -203,7 +203,7 @@ class TunerTsStreamerManager { } } if (!canceled) { - TunerTsStreamer tsStreamer = new TunerTsStreamer(hal, mEventListener); + TunerTsStreamer tsStreamer = new TunerTsStreamer(hal, mEventListener, mContext); if (tsStreamer.startStream(mChannel)) { return tsStreamer; } diff --git a/src/com/android/tv/tuner/ts/SectionParser.java b/src/com/android/tv/tuner/ts/SectionParser.java index f47d48c9..5d3e728a 100644 --- a/src/com/android/tv/tuner/ts/SectionParser.java +++ b/src/com/android/tv/tuner/ts/SectionParser.java @@ -532,6 +532,13 @@ public class SectionParser { int numChannelsInSection = (data[9] & 0xff); int sectionNumber = (data[6] & 0xff); int lastSectionNumber = (data[7] & 0xff); + if (sectionNumber > lastSectionNumber) { + // According to section 6.3.1 of the spec ATSC A/65, + // last section number is the largest section number. + Log.w(TAG, "Invalid VCT. Section Number " + sectionNumber + " > Last Section Number " + + lastSectionNumber); + return false; + } int pos = 10; List<VctItem> results = new ArrayList<>(); for (int i = 0; i < numChannelsInSection; ++i) { diff --git a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java index f8573732..a16bc522 100644 --- a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java +++ b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java @@ -39,6 +39,8 @@ import com.android.tv.tuner.data.TunerChannel; import com.android.tv.tuner.util.ConvertUtils; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,7 +48,6 @@ import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -76,8 +77,7 @@ public class ChannelDataManager implements Handler.Callback { private static final int MSG_BUILD_CHANNEL_MAP = 3; private static final int MSG_REQUEST_PROGRAMS = 4; private static final int MSG_CLEAR_CHANNELS = 6; - private static final int MSG_SCAN_COMPLETED = 7; - private static final int MSG_CHECK_VERSION = 8; + private static final int MSG_CHECK_VERSION = 7; // Throttle the batch operations to avoid TransactionTooLargeException. private static final int BATCH_OPERATION_COUNT = 100; @@ -97,6 +97,8 @@ public class ChannelDataManager implements Handler.Callback { private final Context mContext; private final String mInputId; private ProgramInfoListener mListener; + private ChannelScanListener mChannelScanListener; + private Handler mChannelScanHandler; private final HandlerThread mHandlerThread; private final Handler mHandler; private final ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap; @@ -107,7 +109,7 @@ public class ChannelDataManager implements Handler.Callback { private final ConcurrentSkipListSet<TunerChannel> mScannedChannels; private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels; private final AtomicBoolean mIsScanning; - private CountDownLatch mScanLatch; + private final AtomicBoolean scanCompleted = new AtomicBoolean(); public interface ProgramInfoListener { @@ -135,6 +137,13 @@ public class ChannelDataManager implements Handler.Callback { void onRescanNeeded(); } + public interface ChannelScanListener { + /** + * Invoked when all pending channels have been handled. + */ + void onChannelHandlingDone(); + } + public ChannelDataManager(Context context) { mContext = context; mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(), @@ -176,11 +185,20 @@ public class ChannelDataManager implements Handler.Callback { mListener = listener; } + public void setChannelScanListener(ChannelScanListener listener, Handler handler) { + mChannelScanListener = listener; + mChannelScanHandler = handler; + } + public void release() { mHandler.removeCallbacksAndMessages(null); mHandlerThread.quitSafely(); } + public void releaseSafely() { + mHandlerThread.quitSafely(); + } + public TunerChannel getChannel(long channelId) { TunerChannel channel = mTunerChannelMap.get(channelId); if (channel != null) { @@ -215,7 +233,13 @@ public class ChannelDataManager implements Handler.Callback { } public void notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { - mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget(); + if (mIsScanning.get()) { + // During scanning, channels should be handle first to improve scan time. + // EIT items can be handled in background after channel scan. + mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel)); + } else { + mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget(); + } } // For scanning process @@ -249,26 +273,35 @@ public class ChannelDataManager implements Handler.Callback { * obsolete channels, which are previously scanned but are not in the current scanned result. */ public void notifyScanCompleted() { - mScanLatch = new CountDownLatch(1); - mHandler.sendEmptyMessage(MSG_SCAN_COMPLETED); - try { - mScanLatch.await(); - } catch (InterruptedException e) { - Log.e(TAG, "Scanning process could not finish", e); - } + // Send a dummy message to check whether there is any MSG_HANDLE_CHANNEL in queue + // and avoid race conditions. + scanCompleted.set(true); + mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, null)); + } + + public void scannedChannelHandlingCompleted() { mIsScanning.set(false); - if (mPreviousScannedChannels.isEmpty()) { - return; - } - ArrayList<ContentProviderOperation> ops = new ArrayList<>(); - for (TunerChannel channel : mPreviousScannedChannels) { - ops.add(ContentProviderOperation.newDelete( - TvContract.buildChannelUri(channel.getChannelId())).build()); + if (!mPreviousScannedChannels.isEmpty()) { + ArrayList<ContentProviderOperation> ops = new ArrayList<>(); + for (TunerChannel channel : mPreviousScannedChannels) { + ops.add(ContentProviderOperation.newDelete( + TvContract.buildChannelUri(channel.getChannelId())).build()); + } + try { + mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Error deleting obsolete channels", e); + } } - try { - mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); - } catch (RemoteException | OperationApplicationException e) { - Log.e(TAG, "Error deleting obsolete channels", e); + if (mChannelScanListener != null && mChannelScanHandler != null) { + mChannelScanHandler.post(new Runnable() { + @Override + public void run() { + mChannelScanListener.onChannelHandlingDone(); + } + }); + } else { + Log.e(TAG, "Error. mChannelScanListener is null."); } } @@ -296,7 +329,14 @@ public class ChannelDataManager implements Handler.Callback { } case MSG_HANDLE_CHANNEL: { TunerChannel channel = (TunerChannel) msg.obj; - handleChannel(channel); + if (channel != null) { + handleChannel(channel); + } + if (scanCompleted.get() && mIsScanning.get() + && !mHandler.hasMessages(MSG_HANDLE_CHANNEL)) { + // Complete the scan when all found channels have already been handled. + scannedChannelHandlingCompleted(); + } return true; } case MSG_BUILD_CHANNEL_MAP: { @@ -318,10 +358,6 @@ public class ChannelDataManager implements Handler.Callback { clearChannels(); return true; } - case MSG_SCAN_COMPLETED: { - mScanLatch.countDown(); - return true; - } case MSG_CHECK_VERSION: { checkVersion(); return true; @@ -347,15 +383,69 @@ public class ChannelDataManager implements Handler.Callback { long currentTime = System.currentTimeMillis(); List<EitItem> oldItems = getAllProgramsForChannel(channel, currentTime, currentTime + PROGRAM_QUERY_DURATION); + ArrayList<ContentProviderOperation> ops = new ArrayList<>(); // TODO: Find a right way to check if the programs are added outside. + boolean addedOutside = false; for (EitItem item : oldItems) { if (item.getEventId() == 0) { - // The event has been added outside TV tuner. Do not update programs. - return; + // The event has been added outside TV tuner. + addedOutside = true; + break; } } + + // Inserting programs only when there is no overlapping with existing data assuming that: + // 1. external EPG is more accurate and rich and + // 2. the data we add here will be updated when we apply external EPG. + if (addedOutside) { + // oldItemCount cannot be 0 if addedOutside is true. + int oldItemCount = oldItems.size(); + for (EitItem newItem : items) { + if (newItem.getEndTimeUtcMillis() < currentTime) { + continue; + } + long newItemStartTime = newItem.getStartTimeUtcMillis(); + long newItemEndTime = newItem.getEndTimeUtcMillis(); + if (newItemStartTime < oldItems.get(0).getStartTimeUtcMillis()) { + // Start time smaller than that of any old items. Insert if no overlap. + if (newItemEndTime > oldItems.get(0).getStartTimeUtcMillis()) continue; + } else if (newItemStartTime + > oldItems.get(oldItemCount - 1).getStartTimeUtcMillis()) { + // Start time larger than that of any old item. Insert if no overlap. + if (newItemStartTime + < oldItems.get(oldItemCount - 1).getEndTimeUtcMillis()) continue; + } else { + int pos = Collections.binarySearch(oldItems, newItem, + new Comparator<EitItem>() { + @Override + public int compare(EitItem lhs, EitItem rhs) { + return Long.compare(lhs.getStartTimeUtcMillis(), + rhs.getStartTimeUtcMillis()); + } + }); + if (pos >= 0) { + // Same start Time found. Overlapped. + continue; + } + int insertPoint = -1 - pos; + // Check the two adjacent items. + if (newItemStartTime < oldItems.get(insertPoint - 1).getEndTimeUtcMillis() + || newItemEndTime > oldItems.get(insertPoint).getStartTimeUtcMillis()) { + continue; + } + } + ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( + TvContract.Programs.CONTENT_URI), newItem, channel.getChannelId())); + if (ops.size() >= BATCH_OPERATION_COUNT) { + applyBatch(channel.getName(), ops); + ops.clear(); + } + } + applyBatch(channel.getName(), ops); + return; + } + List<EitItem> outdatedOldItems = new ArrayList<>(); - ArrayList<ContentProviderOperation> ops = new ArrayList<>(); Map<Integer, EitItem> newEitItemMap = new HashMap<>(); for (EitItem item : items) { newEitItemMap.put(item.getEventId(), item); @@ -379,22 +469,8 @@ public class ChannelDataManager implements Handler.Callback { item.setDescription(oldItem.getDescription()); } if (item.compareTo(oldItem) != 0) { - ops.add(ContentProviderOperation.newUpdate( - TvContract.buildProgramUri(oldItem.getProgramId())) - .withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) - .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - item.getStartTimeUtcMillis()) - .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, - item.getEndTimeUtcMillis()) - .withValue(TvContract.Programs.COLUMN_CONTENT_RATING, - item.getContentRating()) - .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE, - item.getAudioLanguage()) - .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, - item.getDescription()) - .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, - item.getEventId()) - .build()); + ops.add(buildContentProviderOperation(ContentProviderOperation.newUpdate( + TvContract.buildProgramUri(oldItem.getProgramId())), item, null)); if (ops.size() >= BATCH_OPERATION_COUNT) { applyBatch(channel.getName(), ops); ops.clear(); @@ -428,9 +504,24 @@ public class ChannelDataManager implements Handler.Callback { if (item.getEndTimeUtcMillis() < currentTime) { continue; } - ops.add(ContentProviderOperation.newInsert(TvContract.Programs.CONTENT_URI) - .withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId()) - .withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) + ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( + TvContract.Programs.CONTENT_URI), item, channel.getChannelId())); + if (ops.size() >= BATCH_OPERATION_COUNT) { + applyBatch(channel.getName(), ops); + ops.clear(); + } + } + + applyBatch(channel.getName(), ops); + } + + private ContentProviderOperation buildContentProviderOperation( + ContentProviderOperation.Builder builder, EitItem item, Long channelId) { + if (channelId != null) { + builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channelId); + } + if (item != null) { + builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, item.getStartTimeUtcMillis()) .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, @@ -442,15 +533,9 @@ public class ChannelDataManager implements Handler.Callback { .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, item.getDescription()) .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, - item.getEventId()) - .build()); - if (ops.size() >= BATCH_OPERATION_COUNT) { - applyBatch(channel.getName(), ops); - ops.clear(); - } + item.getEventId()); } - - applyBatch(channel.getName(), ops); + return builder.build(); } private void applyBatch(String channelName, ArrayList<ContentProviderOperation> operations) { diff --git a/src/com/android/tv/tuner/tvinput/EventDetector.java b/src/com/android/tv/tuner/tvinput/EventDetector.java index e62df62f..27bbb8c7 100644 --- a/src/com/android/tv/tuner/tvinput/EventDetector.java +++ b/src/com/android/tv/tuner/tvinput/EventDetector.java @@ -20,7 +20,6 @@ import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; -import com.android.tv.tuner.TunerFlags; import com.android.tv.tuner.TunerHal; import com.android.tv.tuner.data.Track.AtscAudioTrack; import com.android.tv.tuner.data.Track.AtscCaptionTrack; @@ -39,7 +38,6 @@ import java.util.Set; */ public class EventDetector { private static final String TAG = "EventDetector"; - private static final String CABLE_MODULATION = "QAM"; private static final boolean DEBUG = false; public static final int ALL_PROGRAM_NUMBERS = -1; @@ -62,12 +60,7 @@ public class EventDetector { @Override public void onPatDetected(List<PsiData.PatItem> items) { for (PsiData.PatItem i : items) { - // In case of tuning to a cable channel, we don't add filters for EPG update for - // other channels due to b/29490412. We do the same when extractor in ExoPlayer is - // used since it doesn't allow PMTs not related to the channel to play. - if (mProgramNumber == ALL_PROGRAM_NUMBERS || mProgramNumber == i.getProgramNo() - || (!mModulation.startsWith(CABLE_MODULATION) - && !TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER)) { + if (mProgramNumber == ALL_PROGRAM_NUMBERS || mProgramNumber == i.getProgramNo()) { mTunerHal.addPidFilter(i.getPmtPid(), TunerHal.FILTER_TYPE_OTHER); } } diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java index 352687c1..6ec55e4f 100644 --- a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java +++ b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java @@ -24,6 +24,7 @@ import android.database.Cursor; import android.media.tv.TvContract; import android.media.tv.TvInputManager; import android.net.Uri; +import android.os.AsyncTask; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; @@ -32,23 +33,20 @@ import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.util.Log; -import com.google.android.exoplayer.upstream.DataSource; +import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.recording.RecordingCapability; +import com.android.tv.dvr.DvrStorageStatusManager; import com.android.tv.dvr.RecordedProgram; import com.android.tv.tuner.DvbDeviceAccessor; -import com.android.tv.tuner.TunerFlags; import com.android.tv.tuner.data.PsipData; import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.exoplayer.DataSourceAdapter; import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor; -import com.android.tv.tuner.exoplayer.FrameworkSampleExtractor; import com.android.tv.tuner.exoplayer.SampleExtractor; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; -import com.android.tv.tuner.source.TsMediaDataSource; -import com.android.tv.tuner.source.TsMediaDataSourceManager; -import com.android.tv.util.DvrTunerStorageUtils; +import com.android.tv.tuner.source.TsDataSource; +import com.android.tv.tuner.source.TsDataSourceManager; import com.android.tv.util.Utils; import java.io.File; @@ -75,11 +73,13 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS; private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4); private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10); + private static final long PREPARE_RECORDER_POLL_MS = 50; private static final int MSG_TUNE = 1; private static final int MSG_START_RECORDING = 2; - private static final int MSG_STOP_RECORDING = 3; - private static final int MSG_MONITOR_STORAGE_STATUS = 4; - private static final int MSG_RELEASE = 5; + private static final int MSG_PREPARE_RECODER = 3; + private static final int MSG_STOP_RECORDING = 4; + private static final int MSG_MONITOR_STORAGE_STATUS = 5; + private static final int MSG_RELEASE = 6; private final RecordingCapability mCapabilities; public RecordingCapability getCapabilities() { @@ -97,12 +97,12 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, private final Context mContext; private final ChannelDataManager mChannelDataManager; + private final DvrStorageStatusManager mDvrStorageStatusManager; private final Handler mHandler; - private final TsMediaDataSourceManager mSourceManager; + private final TsDataSourceManager mSourceManager; private final Random mRandom = new Random(); - private final CountDownLatch mReleaseLatch = new CountDownLatch(1); - private TsMediaDataSource mTunerSource; + private TsDataSource mTunerSource; private TunerChannel mChannel; private File mStorageDir; private long mRecordStartTime; @@ -122,9 +122,11 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, HandlerThread handlerThread = new HandlerThread(TAG); handlerThread.start(); mHandler = new Handler(handlerThread.getLooper(), this); + mDvrStorageStatusManager = + TvApplication.getSingletons(context).getDvrStorageStatusManager(); mChannelDataManager = dataManager; mChannelDataManager.checkDataVersion(context); - mSourceManager = TsMediaDataSourceManager.createSourceManager(true); + mSourceManager = TsDataSourceManager.createSourceManager(true); mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId); mInputId = inputId; if (DEBUG) Log.d(TAG, mCapabilities.toString()); @@ -202,13 +204,6 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, public void release() { mHandler.removeCallbacksAndMessages(null); mHandler.sendEmptyMessage(MSG_RELEASE); - try { - mReleaseLatch.await(); - } catch (InterruptedException e) { - Log.e(TAG, "Couldn't wait for finish of MSG_RELEASE", e); - } finally { - mHandler.getLooper().quitSafely(); - } } @Override @@ -216,6 +211,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, switch (msg.what) { case MSG_TUNE: { Uri channelUri = (Uri) msg.obj; + if (DEBUG) Log.d(TAG, "Tune to " + channelUri); if (doTune(channelUri)) { mSession.onTuned(channelUri); } else { @@ -224,12 +220,31 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, return true; } case MSG_START_RECORDING: { + if (DEBUG) Log.d(TAG, "Start recording"); if (!doStartRecording((Uri) msg.obj)) { reset(); } return true; } + case MSG_PREPARE_RECODER: { + if (DEBUG) Log.d(TAG, "Preparing recorder"); + if (!mRecorderRunning) { + return true; + } + try { + if (!mRecorder.prepare()) { + mHandler.sendEmptyMessageDelayed(MSG_PREPARE_RECODER, + PREPARE_RECORDER_POLL_MS); + } + } catch (IOException e) { + Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor"); + mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); + reset(); + } + return true; + } case MSG_STOP_RECORDING: { + if (DEBUG) Log.d(TAG, "Stop recording"); if (mSessionState != STATE_RECORDING) { mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); reset(); @@ -244,12 +259,12 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, if (mSessionState != STATE_RECORDING) { return true; } - if (!DvrTunerStorageUtils.isStorageSufficient(mContext)) { + if (!mDvrStorageStatusManager.isStorageSufficient()) { if (mRecorderRunning) { stopRecorder(); } + new DeleteRecordingTask().execute(mStorageDir); mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - Utils.deleteDirOrFile(mStorageDir); reset(); } else { mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, @@ -262,7 +277,8 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, // without notification. reset(); mSourceManager.release(); - mReleaseLatch.countDown(); + mHandler.removeCallbacksAndMessages(null); + mHandler.getLooper().quitSafely(); return true; } } @@ -318,7 +334,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel); return false; } - if (!DvrTunerStorageUtils.isStorageSufficient(mContext)) { + if (!mDvrStorageStatusManager.isStorageSufficient()) { mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); Log.w(TAG, "Tuning failed due to insufficient storage."); return false; @@ -339,8 +355,9 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, Log.e(TAG, "Recording session status abnormal"); return false; } - mStorageDir = DvrTunerStorageUtils.isStorageSufficient(mContext) ? - DvrTunerStorageUtils.getRecordingDataDirectory(mContext, getStorageKey()) : null; + mStorageDir = mDvrStorageStatusManager.isStorageSufficient() ? + new File(mDvrStorageStatusManager.getRecordingRootDataDirectory(), + getStorageKey()) : null; if (mStorageDir == null) { mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); Log.w(TAG, "Failed to start recording due to insufficient storage."); @@ -350,23 +367,13 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition()); mBufferManager = new BufferManager(new DvrStorageManager(mStorageDir, true)); mRecordStartTime = System.currentTimeMillis(); - if (TunerFlags.USE_EXTRACTOR_IN_EXOPLAYER) { - mRecorder = new ExoPlayerSampleExtractor(new DataSourceAdapter(mTunerSource), - mBufferManager, this, true); - } else { - mRecorder = new FrameworkSampleExtractor(mTunerSource, mBufferManager, this, true); - } + mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, mBufferManager, this, + true); mRecorder.setOnCompletionListener(this, mHandler); mProgramUri = programUri; - try { - mRecorder.prepare(); - } catch (IOException e) { - Log.w(TAG, "Failed to start recording. Couldn't prepare a extractor"); - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - return false; - } mSessionState = STATE_RECORDING; mRecorderRunning = true; + mHandler.sendEmptyMessage(MSG_PREPARE_RECODER); mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, STORAGE_MONITOR_INTERVAL_MS); @@ -553,9 +560,9 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, } if (!success && lastExtractedPositionUs < TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) { + new DeleteRecordingTask().execute(mStorageDir); mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); Log.w(TAG, "Recording failed during recording"); - Utils.deleteDirOrFile(mStorageDir); return; } Log.i(TAG, "recording finished " + (success ? "completely" : "partially")); @@ -563,11 +570,25 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener, Uri.fromFile(mStorageDir).toString(), 1024 * 1024, mRecordStartTime, mRecordStartTime + TimeUnit.MICROSECONDS.toMillis(lastExtractedPositionUs)); if (uri == null) { + new DeleteRecordingTask().execute(mStorageDir); mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); Log.e(TAG, "Inserting a recording to DB failed"); - Utils.deleteDirOrFile(mStorageDir); return; } mSession.onRecordFinished(uri); } + + private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> { + + @Override + public Void doInBackground(File... files) { + if (files == null || files.length == 0) { + return null; + } + for(File file : files) { + Utils.deleteDirOrFile(file); + } + return null; + } + } } diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java index 1494e212..abfd2b30 100644 --- a/src/com/android/tv/tuner/tvinput/TunerSession.java +++ b/src/com/android/tv/tuner/tvinput/TunerSession.java @@ -44,6 +44,7 @@ import com.android.tv.tuner.data.Cea708Data.CaptionEvent; import com.android.tv.tuner.data.Track.AtscCaptionTrack; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.data.TunerChannel; +import com.android.tv.tuner.util.GlobalSettingsUtils; import com.android.tv.tuner.util.StatusTextUtils; import com.android.tv.tuner.util.SystemPropertiesProxy; @@ -97,7 +98,7 @@ public class TunerSession extends TvInputService.Session implements Handler.Call mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status); mAudioStatusView.setVisibility(View.INVISIBLE); mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML( - context.getString(R.string.ut_ac3_passthrough_unavailable)))); + context.getString(R.string.ut_surround_sound_disabled)))); CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption); mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout); mSessionWorker = new TunerSessionWorker(context, channelDataManager, @@ -267,7 +268,14 @@ public class TunerSession extends TvInputService.Session implements Handler.Call return true; } case MSG_UI_SHOW_AUDIO_UNPLAYABLE: { - mAudioStatusView.setVisibility(View.VISIBLE); + // Showing message of enabling surround sound only when global surround sound + // setting is "never". + final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext); + if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) { + mAudioStatusView.setVisibility(View.VISIBLE); + } else { + Log.e(TAG, "Audio is unavailable, surround sound setting is " + value); + } return true; } case MSG_UI_HIDE_AUDIO_UNPLAYABLE: { diff --git a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java index ab9dda50..c0a613a4 100644 --- a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java +++ b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java @@ -45,6 +45,7 @@ import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.ExoPlayer; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.TvContentRatingCache; +import com.android.tv.tuner.TunerPreferences; import com.android.tv.tuner.data.Cea708Data; import com.android.tv.tuner.data.Channel; import com.android.tv.tuner.data.PsipData.EitItem; @@ -56,8 +57,8 @@ import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; import com.android.tv.tuner.exoplayer.MpegTsPlayer; -import com.android.tv.tuner.source.TsMediaDataSource; -import com.android.tv.tuner.source.TsMediaDataSourceManager; +import com.android.tv.tuner.source.TsDataSource; +import com.android.tv.tuner.source.TsDataSourceManager; import com.android.tv.tuner.util.StatusTextUtils; import java.io.File; @@ -112,6 +113,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, private static final int MSG_BUFFER_STATE_CHANGED = 1021; private static final int MSG_PROGRAM_DATA_RESULT = 1022; private static final int MSG_STOP_TUNE = 1023; + private static final int MSG_SET_SURFACE = 1024; + private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025; private static final int TS_PACKET_SIZE = 188; private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000; @@ -121,9 +124,11 @@ public class TunerSessionWorker implements PlaybackBufferListener, private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000; private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000; private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000; + // The following 3s is defined empirically. This should be larger than 2s considering video + // key frame interval in the TS stream. private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000; private static final int PLAYBACK_RETRY_DELAY_MS = 5000; - private static final int MAX_RETRY_COUNT = 5; + private static final int MAX_IMMEDIATE_RETRY_COUNT = 5; private static final long INVALID_TIME = -1; // Some examples of the track ids of the audio tracks, "a0", "a1", "a2". @@ -145,7 +150,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, private final Context mContext; private final ChannelDataManager mChannelDataManager; - private final TsMediaDataSourceManager mSourceManager; + private final TsDataSourceManager mSourceManager; private volatile Surface mSurface; private volatile float mVolume = 1.0f; private volatile boolean mCaptionEnabled; @@ -179,6 +184,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance(); private final TunerSession mSession; private int mPlayerState = ExoPlayer.STATE_IDLE; + private long mPreparingStartTimeMs; private long mBufferingStartTimeMs; private long mReadyStartTimeMs; @@ -196,7 +202,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, mChannelDataManager = channelDataManager; mChannelDataManager.setListener(this); mChannelDataManager.checkDataVersion(mContext); - mSourceManager = TsMediaDataSourceManager.createSourceManager(false); + mSourceManager = TsDataSourceManager.createSourceManager(false); mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); mTvTracks = new ArrayList<>(); mAudioTrackMap = new SparseArray<>(); @@ -206,6 +212,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, mCaptionEnabled = captioningManager.isEnabled(); mPlaybackParams.setSpeed(1.0f); mBufferManager = bufferManager; + mPreparingStartTimeMs = INVALID_TIME; mBufferingStartTimeMs = INVALID_TIME; mReadyStartTimeMs = INVALID_TIME; } @@ -236,7 +243,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, // mSurface is kept even when tune is called right after. But, messages can be deleted by // tune or updateChannelBlockStatus. So mSurface should be stored here, not through message. mSurface = surface; - mHandler.sendEmptyMessage(MSG_RESET_PLAYBACK); + mHandler.sendEmptyMessage(MSG_SET_SURFACE); } /** @@ -355,6 +362,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, return; } mReadyStartTimeMs = INVALID_TIME; + mPreparingStartTimeMs = INVALID_TIME; mBufferingStartTimeMs = INVALID_TIME; if (playbackState == ExoPlayer.STATE_READY) { if (DEBUG) Log.d(TAG, "ExoPlayer ready"); @@ -362,6 +370,8 @@ public class TunerSessionWorker implements PlaybackBufferListener, sendMessage(MSG_START_PLAYBACK, mPlayer); } mReadyStartTimeMs = SystemClock.elapsedRealtime(); + } else if (playbackState == ExoPlayer.STATE_PREPARING) { + mPreparingStartTimeMs = SystemClock.elapsedRealtime(); } else if (playbackState == ExoPlayer.STATE_BUFFERING) { mBufferingStartTimeMs = SystemClock.elapsedRealtime(); } else if (playbackState == ExoPlayer.STATE_ENDED) { @@ -377,6 +387,12 @@ public class TunerSessionWorker implements PlaybackBufferListener, @Override public void onError(Exception e) { + if (TunerPreferences.getStoreTsStream(mContext)) { + // Crash intentionally to capture the error causing TS file. + Log.e(TAG, "Crash intentionally to capture the error causing TS file. " + + e.getMessage()); + SoftPreconditions.checkState(false); + } // There maybe some errors that finally raise ExoPlaybackException and will be handled here. // If we are playing live stream, retrying playback maybe helpful. But for recorded stream, // retrying playback is not helpful. @@ -401,7 +417,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE"); mBufferStartTimeMs = mRecordStartTimeMs = (mRecordingId != null) ? 0 : System.currentTimeMillis(); - mSession.notifyVideoAvailable(); + notifyVideoAvailable(); mReportedDrawnToSurface = true; // If surface is drawn successfully, it means that the playback was brought back @@ -415,6 +431,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, } else { stopCaptionTrack(); } + mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED); } } @@ -584,6 +601,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (mHandler.hasMessages(MSG_TUNE)) { return true; } + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); Uri channelUri = (Uri) msg.obj; String recording = null; long channelId = parseChannel(channelUri); @@ -595,8 +613,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (channel == null && recording == null) { Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri); stopTune(); - mSession.notifyVideoUnavailable( - TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); return true; } mHandler.removeCallbacksAndMessages(null); @@ -619,8 +636,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, stopPlayback(); stopCaptionTrack(); resetTvTracks(); - mSession.notifyVideoUnavailable( - TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); return true; } case MSG_RELEASE: { @@ -642,7 +658,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (DEBUG) { Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount); } - if (mRetryCount <= MAX_RETRY_COUNT) { + if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) { resetPlayback(); } else { // When it reaches this point, it may be due to an error that occurred in @@ -651,11 +667,10 @@ public class TunerSessionWorker implements PlaybackBufferListener, stopPlayback(); stopCaptionTrack(); - mSession.notifyVideoUnavailable( - TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); - // After MAX_RETRY_COUNT, give some delay of an empirically chosen value - // before recovering the playback. + // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically chosen + // value before recovering the playback. mHandler.sendEmptyMessageDelayed(MSG_RESET_PLAYBACK, RECOVER_STOPPED_PLAYBACK_PERIOD_MS); } @@ -742,10 +757,16 @@ public class TunerSessionWorker implements PlaybackBufferListener, return true; } case MSG_TRICKPLAY_BY_SEEK: { + if (mPlayer == null) { + return true; + } doTrickplayBySeek(msg.arg1); return true; } case MSG_SMOOTH_TRICKPLAY_MONITOR: { + if (mPlayer == null) { + return true; + } long systemCurrentTime = System.currentTimeMillis(); long position = getCurrentPosition(); if (mRecordingId == null) { @@ -811,21 +832,33 @@ public class TunerSessionWorker implements PlaybackBufferListener, } case MSG_TIMESHIFT_PAUSE: { if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE"); + if (mPlayer == null) { + return true; + } doTimeShiftPause(); return true; } case MSG_TIMESHIFT_RESUME: { if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME"); + if (mPlayer == null) { + return true; + } doTimeShiftResume(); return true; } case MSG_TIMESHIFT_SEEK_TO: { long position = (long) msg.obj; if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + position + ")"); + if (mPlayer == null) { + return true; + } doTimeShiftSeekTo(position); return true; } case MSG_TIMESHIFT_SET_PLAYBACKPARAMS: { + if (mPlayer == null) { + return true; + } doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj); return true; } @@ -874,7 +907,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (mChannel == null || mPlayer == null) { return true; } - TsMediaDataSource source = mPlayer.getDataSource(); + TsDataSource source = mPlayer.getDataSource(); long limitInBytes = source != null ? source.getBufferedPosition() : 0L; long positionInBytes = source != null ? source.getLastReadPosition() : 0L; if (TunerDebug.ENABLED) { @@ -905,16 +938,21 @@ public class TunerSessionWorker implements PlaybackBufferListener, boolean isBufferingTooLong = mBufferingStartTimeMs != INVALID_TIME && currentTime - mBufferingStartTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; + boolean isPreparingTooLong = mPreparingStartTimeMs != INVALID_TIME + && currentTime - mPreparingStartTimeMs + > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; boolean isWeakSignal = source != null && mChannel.getType() == Channel.TYPE_TUNER - && (noBufferRead || isBufferingTooLong); + && (noBufferRead || isBufferingTooLong || isPreparingTooLong); if (isWeakSignal && !mReportedWeakSignal) { - mSession.notifyVideoUnavailable( - TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) { + mHandler.sendMessageDelayed(mHandler.obtainMessage( + MSG_RETRY_PLAYBACK, mPlayer), PLAYBACK_RETRY_DELAY_MS); + } if (mPlayer != null) { mPlayer.setAudioTrack(false); } - mReportedWeakSignal = true; + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); } else if (!isWeakSignal && mReportedWeakSignal) { boolean isPlaybackStable = mReadyStartTimeMs != INVALID_TIME && currentTime - mReadyStartTimeMs @@ -922,9 +960,9 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (!isPlaybackStable) { // Wait until playback becomes stable. } else if (mReportedDrawnToSurface) { - mSession.notifyVideoAvailable(); + mHandler.removeMessages(MSG_RETRY_PLAYBACK); + notifyVideoAvailable(); mPlayer.setAudioTrack(true); - mReportedWeakSignal = false; } } mLastLimitInBytes = limitInBytes; @@ -932,6 +970,20 @@ public class TunerSessionWorker implements PlaybackBufferListener, mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS); return true; } + case MSG_SET_SURFACE: { + if (mPlayer != null) { + mPlayer.setSurface(mSurface); + } else { + // TODO: Since surface is dynamically set, we can remove the dependency of + // playback start on mSurface nullity. + resetPlayback(); + } + return true; + } + case MSG_NOTIFY_AUDIO_TRACK_UPDATED: { + notifyAudioTracksUpdated(); + return true; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); return false; @@ -955,8 +1007,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, mChannel.selectAudioTrack(audioTrack.index); int newAudioPid = mChannel.getAudioPid(); if (oldAudioPid != newAudioPid) { - // TODO: Implement a switching between tracks more smoothly. - resetPlayback(); + mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, audioTrack.index); } mSession.notifyTrackSelected(type, trackId); } else if (type == TvTrackInfo.TYPE_SUBTITLE) { @@ -1061,33 +1112,48 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (DEBUG) { Log.d(TAG, "Update AudioTracks " + audioTracks); } - removeTvTracks(TvTrackInfo.TYPE_AUDIO); mAudioTrackMap.clear(); if (audioTracks != null) { int index = 0; for (AtscAudioTrack audioTrack : audioTracks) { - String language = audioTrack.language; - if (language == null && mChannel.getAudioTracks() != null - && mChannel.getAudioTracks().size() == audioTracks.size()) { - // If a language is not present, use a language field in PMT section parsed. - language = mChannel.getAudioTracks().get(index).language; - } - - // Save the index to the audio track. - // Later, when a audio track is selected, Both an audio pid and its audio stream - // type reside in the selected index position of the tuner channel's audio data. audioTrack.index = index; - TvTrackInfo.Builder builder = new TvTrackInfo.Builder( - TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + index); - builder.setLanguage(language); - builder.setAudioChannelCount(audioTrack.channelCount); - builder.setAudioSampleRate(audioTrack.sampleRate); - TvTrackInfo track = builder.build(); - mTvTracks.add(track); mAudioTrackMap.put(index, audioTrack); ++index; } } + mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED); + } + + private void notifyAudioTracksUpdated() { + if (mPlayer == null) { + // Audio tracks will be updated later once player initialization is done. + return; + } + int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO); + removeTvTracks(TvTrackInfo.TYPE_AUDIO); + for (int i = 0; i < audioTrackCount; i++) { + AtscAudioTrack audioTrack = mAudioTrackMap.get(i); + if (audioTrack == null) { + continue; + } + String language = audioTrack.language; + if (language == null && mChannel.getAudioTracks() != null + && mChannel.getAudioTracks().size() == mAudioTrackMap.size()) { + // If a language is not present, use a language field in PMT section parsed. + language = mChannel.getAudioTracks().get(i).language; + } + // Save the index to the audio track. + // Later, when an audio track is selected, both the audio pid and its audio stream + // type reside in the selected index position of the tuner channel's audio data. + audioTrack.index = i; + TvTrackInfo.Builder builder = new TvTrackInfo.Builder( + TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i); + builder.setLanguage(language); + builder.setAudioChannelCount(audioTrack.channelCount); + builder.setAudioSampleRate(audioTrack.sampleRate); + TvTrackInfo track = builder.build(); + mTvTracks.add(track); + } mSession.notifyTracksChanged(mTvTracks); } @@ -1170,10 +1236,11 @@ public class TunerSessionWorker implements PlaybackBufferListener, mPlaybackParams.setSpeed(1.0f); mPlayerStarted = false; mReportedDrawnToSurface = false; - mReportedWeakSignal = false; + mPreparingStartTimeMs = INVALID_TIME; mBufferingStartTimeMs = INVALID_TIME; mReadyStartTimeMs = INVALID_TIME; mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE); + mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); } } @@ -1186,8 +1253,7 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (DEBUG) Log.d(TAG, "Channel " + mChannel + " does not have audio."); // Playbacks with video-only stream have not been tested yet. // No video-only channel has been found. - mSession.notifyVideoUnavailable( - TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); return; } if (mChannel != null && ((mChannel.hasAudio() && !mPlayer.hasAudio()) @@ -1204,11 +1270,10 @@ public class TunerSessionWorker implements PlaybackBufferListener, mPlayer.setPlayWhenReady(true); mPlayer.setVolume(mVolume); if (mChannel != null && !mChannel.hasVideo() && mChannel.hasAudio()) { - mSession.notifyVideoUnavailable( - TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY); - } else { - mSession.notifyVideoUnavailable( - TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING); + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY); + } else if (!mReportedWeakSignal) { + // Doesn't show buffering during weak signal. + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING); } mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); mPlayerStarted = true; @@ -1232,15 +1297,11 @@ public class TunerSessionWorker implements PlaybackBufferListener, mSourceManager.setKeepTuneStatus(false); player.release(); if (!mHandler.hasMessages(MSG_TUNE)) { - if (mRetryCount < MAX_RETRY_COUNT) { - // When prepare failed, there may be some errors related to hardware. In that - // case, retry playback immediately may not help. - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer), - PLAYBACK_RETRY_DELAY_MS); - } else { - mSession.notifyVideoUnavailable( - TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); - } + // When prepare failed, there may be some errors related to hardware. In that + // case, retry playback immediately may not help. + notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer), + PLAYBACK_RETRY_DELAY_MS); } } else { mPlayer = player; @@ -1263,7 +1324,6 @@ public class TunerSessionWorker implements PlaybackBufferListener, if (mChannelBlocked || mSurface == null) { return; } - mSession.notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); preparePlayback(); } @@ -1506,4 +1566,18 @@ public class TunerSessionWorker implements PlaybackBufferListener, return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS >= mBufferStartTimeMs - mRecordStartTimeMs; } + + private void notifyVideoUnavailable(final int reason) { + mReportedWeakSignal = (reason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); + if (mSession != null) { + mSession.notifyVideoUnavailable(reason); + } + } + + private void notifyVideoAvailable() { + mReportedWeakSignal = false; + if (mSession != null) { + mSession.notifyVideoAvailable(); + } + } } diff --git a/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java new file mode 100644 index 00000000..e734b779 --- /dev/null +++ b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.tuner.tvinput; + +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.media.tv.TvContract; +import android.net.Uri; +import android.os.AsyncTask; + +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrStorageStatusManager; +import com.android.tv.util.Utils; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Creates {@link JobService} to clean up recorded program files which are not referenced + * from database. + */ +public class TunerStorageCleanUpService extends JobService { + private CleanUpStorageTask mTask; + + @Override + public void onCreate() { + TvApplication.setCurrentRunningProcess(this, false); + super.onCreate(); + mTask = new CleanUpStorageTask(this, this); + } + + @Override + public boolean onStartJob(JobParameters params) { + mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + + /** + * Cleans up recorded program files which are not referenced from database. + * Cleaning up will be done periodically. + */ + public static class CleanUpStorageTask extends AsyncTask<JobParameters, Void, JobParameters[]> { + private final static String[] mProjection = { + TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME, + TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI + }; + private final static long ELAPSED_MILLIS_TO_DELETE = TimeUnit.DAYS.toMillis(1); + + private final Context mContext; + private final DvrStorageStatusManager mDvrStorageStatusManager; + private final JobService mJobService; + private final ContentResolver mContentResolver; + + /** + * Creates a recurring storage cleaning task. + * + * @param context {@link Context} + * @param jobService {@link JobService} + */ + public CleanUpStorageTask(Context context, JobService jobService) { + mContext = context; + mDvrStorageStatusManager = + TvApplication.getSingletons(mContext).getDvrStorageStatusManager(); + mJobService = jobService; + mContentResolver = mContext.getContentResolver(); + } + + private Set<String> getRecordedProgramsDirs() { + try (Cursor c = mContentResolver.query( + TvContract.RecordedPrograms.CONTENT_URI, mProjection, null, null, null)) { + if (c == null) { + return null; + } + Set<String> recordedProgramDirs = new HashSet<>(); + while (c.moveToNext()) { + String packageName = c.getString(0); + String dataUriString = c.getString(1); + if (dataUriString == null) { + continue; + } + Uri dataUri = Uri.parse(dataUriString); + if (!Utils.isInBundledPackageSet(packageName) + || dataUri == null || dataUri.getPath() == null + || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) { + continue; + } + File recordedProgramDir = new File(dataUri.getPath()); + try { + recordedProgramDirs.add(recordedProgramDir.getCanonicalPath()); + } catch (IOException | SecurityException e) { + } + } + return recordedProgramDirs; + } + } + + @Override + protected JobParameters[] doInBackground(JobParameters... params) { + if (mDvrStorageStatusManager.getDvrStorageStatus() + == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { + return params; + } + File dvrRecordingDir = mDvrStorageStatusManager.getRecordingRootDataDirectory(); + if (dvrRecordingDir == null || !dvrRecordingDir.isDirectory()) { + return params; + } + Set<String> recordedProgramDirs = getRecordedProgramsDirs(); + if (recordedProgramDirs == null) { + return params; + } + File[] files = dvrRecordingDir.listFiles(); + if (files == null || files.length == 0) { + return params; + } + for (File recordingDir : files) { + try { + if (!recordedProgramDirs.contains(recordingDir.getCanonicalPath())) { + long lastModified = recordingDir.lastModified(); + long now = System.currentTimeMillis(); + if (lastModified != 0 + && lastModified < now - ELAPSED_MILLIS_TO_DELETE) { + // To prevent current recordings from being deleted, + // deletes recordings which was not modified for long enough time. + Utils.deleteDirOrFile(recordingDir); + } + } + } catch (IOException | SecurityException e) { + // would not happen + } + } + return params; + } + + @Override + protected void onPostExecute(JobParameters[] params) { + for (JobParameters param : params) { + mJobService.jobFinished(param, false); + } + } + } +} diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java index baefe61f..684ebdbd 100644 --- a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java +++ b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java @@ -16,6 +16,8 @@ package com.android.tv.tuner.tvinput; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; import android.content.ComponentName; import android.content.Context; import android.media.tv.TvContract; @@ -25,6 +27,7 @@ import android.util.Log; import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; import com.android.tv.TvApplication; +import com.android.tv.common.feature.CommonFeatures; import com.android.tv.tuner.exoplayer.buffer.BufferManager; import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; import com.android.tv.tuner.util.SystemPropertiesProxy; @@ -32,6 +35,7 @@ import com.android.tv.tuner.util.SystemPropertiesProxy; import java.util.Collections; import java.util.Set; import java.util.WeakHashMap; +import java.util.concurrent.TimeUnit; /** * {@link TunerTvInputService} serves TV channels coming from a tuner device. @@ -41,10 +45,10 @@ public class TunerTvInputService extends TvInputService private static final String TAG = "TunerTvInputService"; private static final boolean DEBUG = false; - private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes"; private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB + private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100; // WeakContainer for {@link TvInputSessionImpl} private final Set<TunerSession> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>()); @@ -62,6 +66,19 @@ public class TunerTvInputService extends TvInputService mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); mAudioCapabilitiesReceiver.register(); mBufferManager = createBufferManager(); + if (CommonFeatures.DVR.isEnabled(this)) { + JobScheduler jobScheduler = + (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); + JobInfo pendingJob = jobScheduler.getPendingJob(DVR_STORAGE_CLEANUP_JOB_ID); + if (pendingJob != null) { + // storage cleaning job is already scheduled. + } else { + JobInfo job = new JobInfo.Builder(DVR_STORAGE_CLEANUP_JOB_ID, + new ComponentName(this, TunerStorageCleanUpService.class)) + .setPersisted(true).setPeriodic(TimeUnit.DAYS.toMillis(1)).build(); + jobScheduler.schedule(job); + } + } if (mBufferManager == null) { Log.i(TAG, "Trickplay is disabled"); } else { diff --git a/src/com/android/tv/tuner/util/GlobalSettingsUtils.java b/src/com/android/tv/tuner/util/GlobalSettingsUtils.java new file mode 100644 index 00000000..0cefcbed --- /dev/null +++ b/src/com/android/tv/tuner/util/GlobalSettingsUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.tuner.util; + +import android.content.Context; +import android.provider.Settings; + +/** + * Utility class that get information of global settings. + */ +public class GlobalSettingsUtils { + // Since global surround setting is hided, add the related variable here for checking surround + // sound setting when the audio is unavailable. Remove this workaround after b/31254857 fixed. + private static final String ENCODED_SURROUND_OUTPUT = "encoded_surround_output"; + public static final int ENCODED_SURROUND_OUTPUT_NEVER = 1; + + private GlobalSettingsUtils () { } + + public static int getEncodedSurroundOutputSettings(Context context) { + return Settings.Global.getInt(context.getContentResolver(), ENCODED_SURROUND_OUTPUT, 0); + } +} diff --git a/src/com/android/tv/ui/AppLayerTvView.java b/src/com/android/tv/ui/AppLayerTvView.java index eef05136..09acb36b 100644 --- a/src/com/android/tv/ui/AppLayerTvView.java +++ b/src/com/android/tv/ui/AppLayerTvView.java @@ -55,7 +55,7 @@ public class AppLayerTvView extends TvView { public void onViewAdded(View child) { if (child instanceof SurfaceView) { // Note: See b/29118070 for detail. - ((SurfaceView) child).setSecure(Experiments.ENABLE_DEVELOPER_FEATURES.get()); + ((SurfaceView) child).setSecure(!Experiments.ENABLE_DEVELOPER_FEATURES.get()); } super.onViewAdded(child); } diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java index cb371e76..3cf4de83 100644 --- a/src/com/android/tv/ui/ChannelBannerView.java +++ b/src/com/android/tv/ui/ChannelBannerView.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.Bitmap; +import android.media.tv.TvContentRating; import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.net.Uri; @@ -57,6 +58,7 @@ import com.android.tv.data.Program; import com.android.tv.data.StreamInfo; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.parental.ContentRatingsManager; import com.android.tv.util.ImageCache; import com.android.tv.util.ImageLoader; import com.android.tv.util.ImageLoader.ImageLoaderCallback; @@ -92,6 +94,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage */ public static final int LOCK_CHANNEL_INFO = 2; + private static final int DISPLAYED_CONTENT_RATINGS_COUNT = 3; + private static final String EMPTY_STRING = ""; private static Program sNoProgram; @@ -114,6 +118,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private TextView mAspectRatioTextView; private TextView mResolutionTextView; private TextView mAudioChannelTextView; + private TextView[] mContentRatingsTextViews = new TextView[DISPLAYED_CONTENT_RATINGS_COUNT]; private TextView mProgramDescriptionTextView; private String mProgramDescriptionText; private View mAnchorView; @@ -121,6 +126,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage private Program mLastUpdatedProgram; private final Handler mHandler = new Handler(); private final DvrManager mDvrManager; + private ContentRatingsManager mContentRatingsManager; + private TvContentRating mBlockingContentRating; private int mLockType; @@ -233,6 +240,8 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } else { mDvrManager = null; } + mContentRatingsManager = TvApplication.getSingletons(getContext()) + .getTvInputManagerHelper().getContentRatingsManager(); if (sNoProgram == null) { sNoProgram = new Program.Builder() @@ -284,6 +293,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mAspectRatioTextView = (TextView) findViewById(R.id.aspect_ratio); mResolutionTextView = (TextView) findViewById(R.id.resolution); mAudioChannelTextView = (TextView) findViewById(R.id.audio_channel); + mContentRatingsTextViews[0] = (TextView) findViewById(R.id.content_ratings_0); + mContentRatingsTextViews[1] = (TextView) findViewById(R.id.content_ratings_1); + mContentRatingsTextViews[2] = (TextView) findViewById(R.id.content_ratings_2); mProgramDescriptionTextView = (TextView) findViewById(R.id.program_description); mAnchorView = findViewById(R.id.anchor); @@ -349,6 +361,15 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } /** + * Sets the content rating that blocks the current watched channel for displaying it in the + * channel banner. + */ + public void setBlockingContentRating(TvContentRating rating) { + mBlockingContentRating = rating; + updateProgramRatings(mMainActivity.getCurrentProgram()); + } + + /** * Update channel banner view. * * @param info A StreamInfo that includes stream information. @@ -357,8 +378,11 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage public void updateViews(StreamInfo info) { resetAnimationEffects(); Channel channel = mMainActivity.getCurrentChannel(); - if (!Objects.equals(mCurrentChannel, channel) && isShown()) { - scheduleHide(); + if (!Objects.equals(mCurrentChannel, channel)) { + mBlockingContentRating = null; + if (isShown()) { + scheduleHide(); + } } mCurrentChannel = channel; mChannelView.setVisibility(VISIBLE); @@ -390,6 +414,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage mAspectRatioTextView.setVisibility(View.GONE); mResolutionTextView.setVisibility(View.GONE); mAudioChannelTextView.setVisibility(View.GONE); + for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { + mContentRatingsTextViews[i].setVisibility(View.GONE); + } } } @@ -545,6 +572,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage } updateProgramTimeInfo(program); updateRecordingStatus(program); + updateProgramRatings(program); // When the program is changed, but the previous resize animation has not ended yet, // cancel the animation. @@ -626,6 +654,28 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage : R.dimen.channel_banner_anchor_two_line_y); } + private void updateProgramRatings(Program program) { + if (mBlockingContentRating != null) { + mContentRatingsTextViews[0].setText( + mContentRatingsManager.getDisplayNameForRating(mBlockingContentRating)); + mContentRatingsTextViews[0].setVisibility(View.VISIBLE); + for (int i = 1; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { + mContentRatingsTextViews[i].setVisibility(View.GONE); + } + return; + } + TvContentRating[] ratings = (program == null) ? null : program.getContentRatings(); + for (int i = 0; i < DISPLAYED_CONTENT_RATINGS_COUNT; i++) { + if (ratings == null || ratings.length <= i) { + mContentRatingsTextViews[i].setVisibility(View.GONE); + } else { + mContentRatingsTextViews[i].setText( + mContentRatingsManager.getDisplayNameForRating(ratings[i])); + mContentRatingsTextViews[i].setVisibility(View.VISIBLE); + } + } + } + private void updateProgramTimeInfo(Program program) { long durationMs = program.getDurationMillis(); long startTimeMs = program.getStartTimeUtcMillis(); @@ -775,4 +825,4 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage animator.addListener(mResizeAnimatorListener); return animator; } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java index 44656de5..cbe459fb 100644 --- a/src/com/android/tv/ui/TunableTvView.java +++ b/src/com/android/tv/ui/TunableTvView.java @@ -62,7 +62,6 @@ import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.StreamInfo; import com.android.tv.data.WatchedHistoryManager; -import com.android.tv.dvr.DvrManager; import com.android.tv.parental.ContentRatingsManager; import com.android.tv.recommendation.NotificationService; import com.android.tv.util.NetworkUtils; @@ -171,7 +170,6 @@ public class TunableTvView extends FrameLayout implements StreamInfo { @BlockScreenType private int mBlockScreenType; - private final DvrManager mDvrManager; private final TvInputManagerHelper mInputManager; private final ConnectivityManager mConnectivityManager; private final InputSessionManager mInputSessionManager; @@ -352,10 +350,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo { ApplicationSingletons appSingletons = TvApplication.getSingletons(context); if (CommonFeatures.DVR.isEnabled(context)) { - mDvrManager = appSingletons.getDvrManager(); mInputSessionManager = appSingletons.getInputSessionManager(); } else { - mDvrManager = null; mInputSessionManager = null; } mInputManager = appSingletons.getTvInputManagerHelper(); @@ -764,6 +760,7 @@ public class TunableTvView extends FrameLayout implements StreamInfo { /** * Returns currently blocked content rating. {@code null} if it's not blocked. */ + @Override public TvContentRating getBlockedContentRating() { return mBlockedContentRating; } @@ -1005,6 +1002,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo { mute(); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: + mHideScreenView.setVisibility(VISIBLE); + mHideScreenView.setImageVisibility(false); + mHideScreenView.setText(null); + mBufferingSpinnerView.setVisibility(VISIBLE); + mute(); + break; case VIDEO_UNAVAILABLE_REASON_NOT_TUNED: mHideScreenView.setVisibility(VISIBLE); mHideScreenView.setImageVisibility(false); @@ -1035,12 +1038,14 @@ public class TunableTvView extends FrameLayout implements StreamInfo { } private String getTuneConflictMessage(String inputId) { - if (mDvrManager != null && inputId != null) { + if (inputId != null) { TvInputInfo input = mInputManager.getTvInputInfo(inputId); - long time = mDvrManager.getEarliestRecordingEndTime(inputId); - return getResources().getQuantityString(R.plurals.tvview_msg_input_no_resource, - input.getTunerCount(), - DateUtils.formatDateTime(getContext(), time, DateUtils.FORMAT_SHOW_TIME)); + Long timeMs = mInputSessionManager.getEarliestRecordingSessionEndTimeMs(inputId); + if (timeMs != null) { + return getResources().getQuantityString(R.plurals.tvview_msg_input_no_resource, + input.getTunerCount(), + DateUtils.formatDateTime(getContext(), timeMs, DateUtils.FORMAT_SHOW_TIME)); + } } return null; } diff --git a/src/com/android/tv/ui/ViewUtils.java b/src/com/android/tv/ui/ViewUtils.java index 5a853dcd..ac181752 100644 --- a/src/com/android/tv/ui/ViewUtils.java +++ b/src/com/android/tv/ui/ViewUtils.java @@ -16,8 +16,11 @@ package com.android.tv.ui; +import android.animation.Animator; +import android.animation.ValueAnimator; import android.util.Log; import android.view.View; +import android.view.ViewGroup.LayoutParams; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -42,4 +45,49 @@ public class ViewUtils { Log.e(TAG, "Fail to call View.setTransitionAlpha", e); } } -} + + /** + * Creates an animator in view's height + * @param target the {@link view} animator performs on. + */ + public static Animator createHeightAnimator( + final View target, int initialHeight, int targetHeight) { + ValueAnimator animator = ValueAnimator.ofInt(initialHeight, targetHeight); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + int value = (Integer) animation.getAnimatedValue(); + if (value == 0) { + if (target.getVisibility() != View.GONE) { + target.setVisibility(View.GONE); + } + } else { + if (target.getVisibility() != View.VISIBLE) { + target.setVisibility(View.VISIBLE); + } + setLayoutHeight(target, value); + } + } + }); + return animator; + } + + /** + * Gets view's layout height. + */ + public static int getLayoutHeight(View view) { + LayoutParams layoutParams = view.getLayoutParams(); + return layoutParams.height; + } + + /** + * Sets view's layout height. + */ + public static void setLayoutHeight(View view, int height) { + LayoutParams layoutParams = view.getLayoutParams(); + if (height != layoutParams.height) { + layoutParams.height = height; + view.setLayoutParams(layoutParams); + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java index 067c6292..0d189cca 100644 --- a/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java +++ b/src/com/android/tv/ui/sidepanel/DeveloperOptionFragment.java @@ -18,6 +18,8 @@ package com.android.tv.ui.sidepanel; import android.accounts.Account; import android.app.Activity; +import android.app.ApplicationErrorReport; +import android.content.Intent; import android.support.annotation.NonNull; import android.util.Log; import android.widget.Toast; @@ -27,6 +29,7 @@ import com.android.tv.TvApplication; import com.android.tv.common.BuildConfig; import com.android.tv.data.epg.EpgFetcher; import com.android.tv.experiments.Experiments; +import com.android.tv.tuner.TunerPreferences; import java.util.ArrayList; import java.util.List; @@ -59,6 +62,33 @@ public class DeveloperOptionFragment extends SideFragment { } }); } + items.add(new ActionItem(getString(R.string.dev_item_send_feedback)) { + @Override + protected void onSelected() { + Intent intent = new Intent(Intent.ACTION_APP_ERROR); + ApplicationErrorReport report = new ApplicationErrorReport(); + report.packageName = report.processName = getContext().getPackageName(); + report.time = System.currentTimeMillis(); + report.type = ApplicationErrorReport.TYPE_NONE; + intent.putExtra(Intent.EXTRA_BUG_REPORT, report); + startActivityForResult(intent, 0); + } + }); + items.add(new SwitchItem(getString(R.string.dev_item_store_ts_on), + getString(R.string.dev_item_store_ts_off), + getString(R.string.dev_item_store_ts_description)) { + @Override + protected void onUpdate() { + super.onUpdate(); + setChecked(TunerPreferences.getStoreTsStream(getContext())); + } + + @Override + protected void onSelected() { + super.onSelected(); + TunerPreferences.setStoreTsStream(getContext(), isChecked()); + } + }); return items; } diff --git a/src/com/android/tv/ui/sidepanel/SideFragment.java b/src/com/android/tv/ui/sidepanel/SideFragment.java index 30e885a5..8df56cd2 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragment.java +++ b/src/com/android/tv/ui/sidepanel/SideFragment.java @@ -235,6 +235,9 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { void onSideFragmentViewDestroyed(); } + /** + * Preloads the view holders. + */ public static void preloadRecycledViews(Context context) { if (sRecycledViewPool != null) { return; @@ -252,6 +255,13 @@ public abstract class SideFragment extends Fragment implements HasTrackerLabel { } } + /** + * Releases the pre-loaded view holders. + */ + public static void releasePreloadedRecycledViews() { + sRecycledViewPool = null; + } + private static class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> { private final LayoutInflater mLayoutInflater; private List<Item> mItems; diff --git a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java index faccbc66..553cd9d7 100644 --- a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java +++ b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java @@ -22,6 +22,7 @@ import android.animation.AnimatorListenerAdapter; import android.app.Activity; import android.app.FragmentManager; import android.app.FragmentTransaction; +import android.os.Handler; import android.view.View; import com.android.tv.R; @@ -42,6 +43,7 @@ public class SideFragmentManager { private final Animator mShowAnimator; private final Animator mHideAnimator; + private final Handler mHandler = new Handler(); private final Runnable mHideAllRunnable = new Runnable() { @Override public void run() { @@ -154,6 +156,7 @@ public class SideFragmentManager { } private void hideAllInternal() { + mHandler.removeCallbacksAndMessages(null); if (mFragmentCount == 0) { return; } @@ -192,8 +195,8 @@ public class SideFragmentManager { * stack. If you want to empty the back stack, call {@link #hideAll}. */ public void hideSidePanel(boolean withAnimation) { + mHandler.removeCallbacks(mHideAllRunnable); if (withAnimation) { - mPanel.removeCallbacks(mHideAllRunnable); Animator hideAnimator = AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_exit); hideAnimator.setTarget(mPanel); @@ -213,9 +216,12 @@ public class SideFragmentManager { return mPanel.getVisibility() == View.VISIBLE; } + /** + * Resets the timer for hiding side fragment. + */ public void scheduleHideAll() { - mPanel.removeCallbacks(mHideAllRunnable); - mPanel.postDelayed(mHideAllRunnable, mShowDurationMillis); + mHandler.removeCallbacks(mHideAllRunnable); + mHandler.postDelayed(mHideAllRunnable, mShowDurationMillis); } /** diff --git a/src/com/android/tv/util/AsyncDbTask.java b/src/com/android/tv/util/AsyncDbTask.java index 8bcdb294..78243642 100644 --- a/src/com/android/tv/util/AsyncDbTask.java +++ b/src/com/android/tv/util/AsyncDbTask.java @@ -29,9 +29,9 @@ import android.util.Log; import android.util.Range; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.dvr.RecordedProgram; import java.util.ArrayList; import java.util.List; @@ -162,7 +162,7 @@ public abstract class AsyncDbTask<Params, Progress, Result> @Override public String toString() { - return this.getClass().getSimpleName() + "(" + mUri + ")"; + return this.getClass().getName() + "(" + mUri + ")"; } } diff --git a/src/com/android/tv/tuner/TunerFlags.java b/src/com/android/tv/util/CompositeComparator.java index 9370b092..47cf50fe 100644 --- a/src/com/android/tv/tuner/TunerFlags.java +++ b/src/com/android/tv/util/CompositeComparator.java @@ -14,11 +14,29 @@ * limitations under the License. */ -package com.android.tv.tuner; +package com.android.tv.util; + +import java.util.Comparator; /** - * Defines flags which are useful for experimenting new feature on tuner implementation. + * A comparator which runs multiple comparators sequentially. */ -public final class TunerFlags { - public static final boolean USE_EXTRACTOR_IN_EXOPLAYER = false; +public class CompositeComparator<T> implements Comparator<T> { + private final Comparator<T>[] mComparators; + + @SafeVarargs + public CompositeComparator(Comparator<T>... comparators) { + mComparators = comparators; + } + + @Override + public int compare(T lhs, T rhs) { + for (Comparator<T> comparator : mComparators) { + int result = comparator.compare(lhs, rhs); + if (result != 0) { + return result; + } + } + return 0; + } } diff --git a/src/com/android/tv/util/DvrTunerStorageUtils.java b/src/com/android/tv/util/DvrTunerStorageUtils.java deleted file mode 100644 index 534a95ef..00000000 --- a/src/com/android/tv/util/DvrTunerStorageUtils.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.tv.util; - -import android.os.Environment; -import android.content.Context; -import android.os.StatFs; -import android.os.storage.StorageManager; -import android.os.storage.StorageVolume; - -import com.android.tv.common.feature.CommonFeatures; - -import java.io.File; - -/** - * A utility class for storage usage of DVR recording. - */ -public class DvrTunerStorageUtils { - // STOPSHIP: turn off ALLOW_REMOVABLE_STORAGE. b/30768857 - private static final boolean ALLOW_REMOVABLE_STORAGE = true; - - private static final long MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES = 50 * 1024 * 1024 * 1024L; - private static final long MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES = 10 * 1024 * 1024 * 1024L; - private static final String RECORDING_DATA_SUB_PATH = "/recording/"; - // Since {@link StorageVolume#getUuid} will return null for internal storage and {@code null} - // should be used for missing storage status, we need the internal storage specifier. - private static final String INTERNAL_STORAGE_UUID = "internal_storage_uuid"; - - /** - * Returns the path to DVR recording data directory. - * @param context {@link Context} - * @param recordingId unique {@link String} specifier for each recording - * @return {@link File} - */ - public static File getRecordingDataDirectory(Context context, String recordingId) { - File recordingDataRootDir = getRootDirectory(context); - if (recordingDataRootDir == null) { - return null; - } - return new File(recordingDataRootDir + RECORDING_DATA_SUB_PATH + recordingId); - } - - /** - * Returns the unique identifier for the storage which will be used to store recordings. - * @param context {@link Context} - * @return {@link String} of the unique identifier when storage exists, {@code null} otherwise - */ - public static String getRecordingStorageUuid(Context context) { - File recordingDataRootDir = getRootDirectory(context); - StorageManager manager = (StorageManager) context.getSystemService(context.STORAGE_SERVICE); - StorageVolume volume = manager.getStorageVolume(recordingDataRootDir); - if (volume == null) { - return null; - } - if (!Environment.MEDIA_MOUNTED.equals(volume.getState())) { - return null; - } - String uuid = volume.getUuid(); - return uuid == null ? INTERNAL_STORAGE_UUID : uuid; - } - - /** - * Returns whether the storage has sufficient storage. - * @param context {@link Context} - * @return {@code true} when there is sufficient storage, {@code false} otherwise - */ - public static boolean isStorageSufficient(Context context) { - File recordingDataRootDir = getRootDirectory(context); - if (recordingDataRootDir == null || !recordingDataRootDir.isDirectory()) { - return false; - } - if (CommonFeatures.FORCE_RECORDING_UNTIL_NO_SPACE.isEnabled(context)) { - return true; - } - StatFs statFs = new StatFs(recordingDataRootDir.toString()); - return statFs.getTotalBytes() >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES - && statFs.getAvailableBytes() >= MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES; - } - - private static File getRootDirectory(Context context) { - if (!ALLOW_REMOVABLE_STORAGE) { - return context.getExternalFilesDir(null); - } - File[] dirs = context.getExternalFilesDirs(null); - if (dirs == null) { - return null; - } - for (File dir : dirs) { - if (dir == null) { - continue; - } - StatFs statFs = new StatFs(dir.toString()); - if (statFs.getTotalBytes() >= MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES - && statFs.getAvailableBytes() >= MIN_FREE_STORAGE_SIZE_FOR_DVR_IN_BYTES) { - return dir; - } - } - return dirs[0]; - } -} diff --git a/src/com/android/tv/util/LocationUtils.java b/src/com/android/tv/util/LocationUtils.java index 406a9d88..8e3b59e9 100644 --- a/src/com/android/tv/util/LocationUtils.java +++ b/src/com/android/tv/util/LocationUtils.java @@ -25,6 +25,7 @@ import android.location.LocationManager; import android.os.Bundle; import android.util.Log; + import java.io.IOException; import java.util.List; import java.util.Locale; @@ -36,36 +37,6 @@ public class LocationUtils { private static final String TAG = "LocationUtils"; private static final boolean DEBUG = false; - private static final LocationListener LOCATION_LISTENER = new LocationListener() { - @Override - public void onLocationChanged(Location location) { - Geocoder geocoder = new Geocoder(sApplicationContext, Locale.getDefault()); - try { - List<Address> addresses = geocoder.getFromLocation(location.getLatitude(), - location.getLongitude(), 1); - if (addresses != null) { - sAddress = addresses.get(0); - if (DEBUG) Log.d(TAG, "returned address: " + sAddress); - } else { - if (DEBUG) Log.d(TAG, "No address returned"); - } - sError = null; - } catch (IOException e) { - Log.w(TAG, "Error in retrieving address", e); - sError = e; - } - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { } - - @Override - public void onProviderEnabled(String provider) { } - - @Override - public void onProviderDisabled(String provider) { } - }; - private static Context sApplicationContext; private static Address sAddress; private static IOException sError; @@ -83,19 +54,67 @@ public class LocationUtils { } if (sApplicationContext == null) { sApplicationContext = context.getApplicationContext(); - LocationManager mLocationManager = (LocationManager) context.getSystemService( - Context.LOCATION_SERVICE); - try { - mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 1000, 10, - LOCATION_LISTENER, null); - } catch (SecurityException e) { - // Enables requesting the location updates again. - sApplicationContext = null; - throw e; - } } + LocationUtilsHelper.startLocationUpdates(); return null; } + private static void updateAddress(Location location) { + if (DEBUG) Log.d(TAG, "Updating address with " + location); + if (location == null) { + return; + } + Geocoder geocoder = new Geocoder(sApplicationContext, Locale.getDefault()); + try { + List<Address> addresses = geocoder.getFromLocation( + location.getLatitude(), location.getLongitude(), 1); + if (addresses != null) { + sAddress = addresses.get(0); + if (DEBUG) Log.d(TAG, "Got " + sAddress); + } else { + if (DEBUG) Log.d(TAG, "No address returned"); + } + sError = null; + } catch (IOException e) { + Log.w(TAG, "Error in updating address", e); + sError = e; + } + } + private LocationUtils() { } + + private static class LocationUtilsHelper { + private static final LocationListener LOCATION_LISTENER = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + updateAddress(location); + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { } + + @Override + public void onProviderEnabled(String provider) { } + + @Override + public void onProviderDisabled(String provider) { } + }; + + private static LocationManager sLocationManager; + + public static void startLocationUpdates() { + if (sLocationManager == null) { + sLocationManager = (LocationManager) sApplicationContext.getSystemService( + Context.LOCATION_SERVICE); + try { + sLocationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, 1000, 10, LOCATION_LISTENER, null); + } catch (SecurityException e) { + // Enables requesting the location updates again. + sLocationManager = null; + throw e; + } + } + } + } } diff --git a/src/com/android/tv/util/PermissionUtils.java b/src/com/android/tv/util/PermissionUtils.java index a443ede7..453885a4 100644 --- a/src/com/android/tv/util/PermissionUtils.java +++ b/src/com/android/tv/util/PermissionUtils.java @@ -7,6 +7,11 @@ import android.content.pm.PackageManager; * Util class to handle permissions. */ public class PermissionUtils { + /** + * Permission to read the TV listings. + */ + public static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS"; + private static Boolean sHasAccessAllEpgPermission; private static Boolean sHasAccessWatchedHistoryPermission; private static Boolean sHasModifyParentalControlsPermission; @@ -39,7 +44,7 @@ public class PermissionUtils { } public static boolean hasReadTvListings(Context context) { - return context.checkSelfPermission("android.permission.READ_TV_LISTINGS") + return context.checkSelfPermission(PERMISSION_READ_TV_LISTINGS) == PackageManager.PERMISSION_GRANTED; } } diff --git a/src/com/android/tv/util/PipInputManager.java b/src/com/android/tv/util/PipInputManager.java index 03bdc681..2c51d5a0 100644 --- a/src/com/android/tv/util/PipInputManager.java +++ b/src/com/android/tv/util/PipInputManager.java @@ -149,6 +149,7 @@ public class PipInputManager { if (mStarted) { return; } + mStarted = true; mInputManager.addCallback(mTvInputCallback); mChannelTuner.addListener(mChannelTunerListener); initializePipInputList(); @@ -161,6 +162,7 @@ public class PipInputManager { if (!mStarted) { return; } + mStarted = false; mInputManager.removeCallback(mTvInputCallback); mChannelTuner.removeListener(mChannelTunerListener); mPipInputMap.clear(); diff --git a/src/com/android/tv/util/RecurringRunner.java b/src/com/android/tv/util/RecurringRunner.java index 469170ed..4135bd4e 100644 --- a/src/com/android/tv/util/RecurringRunner.java +++ b/src/com/android/tv/util/RecurringRunner.java @@ -126,10 +126,7 @@ public final class RecurringRunner { return next; } - /** - * Resets the next run time. - */ - public long resetNextRunTime() { + private long resetNextRunTime() { long next = System.currentTimeMillis() + mIntervalMs; getSharedPreferences().edit().putLong(mName, next).apply(); return next; diff --git a/src/com/android/tv/util/SetupUtils.java b/src/com/android/tv/util/SetupUtils.java index 1213d317..8223a81c 100644 --- a/src/com/android/tv/util/SetupUtils.java +++ b/src/com/android/tv/util/SetupUtils.java @@ -20,6 +20,8 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.media.tv.TvContract; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; @@ -35,6 +37,8 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.experiments.Experiments; import com.android.tv.tuner.tvinput.TunerTvInputService; import java.util.Collections; @@ -267,7 +271,8 @@ public class SetupUtils { // Find all already-verified packages. Set<String> setUpPackages = new HashSet<>(); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - for (String input : sp.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.<String>emptySet())) { + for (String input : sp.getStringSet(PREF_KEY_SET_UP_INPUTS, + Collections.<String>emptySet())) { if (!TextUtils.isEmpty(input)) { ComponentName componentName = ComponentName.unflattenFromString(input); if (componentName != null) { @@ -330,14 +335,28 @@ public class SetupUtils { removedInputList.remove(mTunerInputId); if (!removedInputList.isEmpty()) { + boolean inputPackageDeleted = false; for (String input : removedInputList) { - mRecognizedInputs.remove(input); - mSetUpInputs.remove(input); - mKnownInputs.remove(input); + try { + // Just after booting, input list from TvInputManager are not reliable. + // So we need to double-check package existence. b/29034900 + mTvApplication.getPackageManager().getPackageInfo( + ComponentName.unflattenFromString(input) + .getPackageName(), PackageManager.GET_ACTIVITIES); + Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted"); + } catch (NameNotFoundException e) { + Log.i(TAG, "TV input (" + input + ") and its package are removed"); + mRecognizedInputs.remove(input); + mSetUpInputs.remove(input); + mKnownInputs.remove(input); + inputPackageDeleted = true; + } + } + if (inputPackageDeleted) { + mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs) + .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) + .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply(); } - mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs) - .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) - .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply(); } } @@ -345,7 +364,7 @@ public class SetupUtils { * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true} * for {@code inputId}. */ - public void onSetupDone(String inputId) { + private void onSetupDone(String inputId) { SoftPreconditions.checkState(inputId != null); if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId); if (!mRecognizedInputs.contains(inputId)) { @@ -363,5 +382,13 @@ public class SetupUtils { mSetUpInputs.add(inputId); mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply(); } + // Start fetching program guide data for internal tuners. + Context context = mTvApplication.getApplicationContext(); + if (Utils.isInternalTvInput(context, inputId)) { + if (context.checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) + == PackageManager.PERMISSION_GRANTED && Experiments.CLOUD_EPG.get()) { + EpgFetcher.getInstance(context).startImmediately(); + } + } } } diff --git a/src/com/android/tv/util/SystemProperties.java b/src/com/android/tv/util/SystemProperties.java index 235161b6..e737f233 100644 --- a/src/com/android/tv/util/SystemProperties.java +++ b/src/com/android/tv/util/SystemProperties.java @@ -36,12 +36,6 @@ public final class SystemProperties { "tv_allow_strict_mode", true); /** - * Allow Strict death penalty for eng builds. - */ - public static final BooleanSystemProperty ALLOW_DEATH_PENALTY = new BooleanSystemProperty( - "tv_allow_death_penalty", true); - - /** * When true {@link android.view.KeyEvent}s are logged. Defaults to false. */ public static final BooleanSystemProperty LOG_KEYEVENT = new BooleanSystemProperty( diff --git a/src/com/android/tv/util/ToastUtils.java b/src/com/android/tv/util/ToastUtils.java new file mode 100644 index 00000000..34346b2a --- /dev/null +++ b/src/com/android/tv/util/ToastUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.util; + +import android.content.Context; +import android.support.annotation.MainThread; +import android.widget.Toast; + +import java.lang.ref.WeakReference; + +/** + * A utility class for the toast message. + */ +public class ToastUtils { + private static WeakReference<Toast> sToast; + + /** + * Shows the toast message after canceling the previous one. + */ + @MainThread + public static void show(Context context, CharSequence text, int duration) { + if (sToast != null && sToast.get() != null) { + sToast.get().cancel(); + } + Toast toast = Toast.makeText(context, text, duration); + toast.show(); + sToast = new WeakReference<>(toast); + } +} diff --git a/src/com/android/tv/util/TvSettings.java b/src/com/android/tv/util/TvSettings.java index 4a65cbbe..97ff59d6 100644 --- a/src/com/android/tv/util/TvSettings.java +++ b/src/com/android/tv/util/TvSettings.java @@ -34,9 +34,6 @@ import java.util.Set; public final class TvSettings { private TvSettings() {} - public static final String PREFS_FILE = "settings"; - public static final String PREF_TV_WATCH_LOGGING_ENABLED = "tv_watch_logging_enabled"; - public static final String PREF_CLOSED_CAPTION_ENABLED = "is_cc_enabled"; // boolean value public static final String PREF_DISPLAY_MODE = "display_mode"; // int value public static final String PREF_PIP_LAYOUT = "pip_layout"; // int value public static final String PREF_PIP_SIZE = "pip_size"; // int value @@ -49,7 +46,6 @@ public final class TvSettings { public @interface PipSound {} public static final int PIP_SOUND_MAIN = 0; public static final int PIP_SOUND_PIP_WINDOW = PIP_SOUND_MAIN + 1; - public static final int PIP_SOUND_LAST = PIP_SOUND_PIP_WINDOW; // PIP layouts @Retention(RetentionPolicy.SOURCE) diff --git a/src/com/android/tv/util/Utils.java b/src/com/android/tv/util/Utils.java index 9f90c69f..99d34431 100644 --- a/src/com/android/tv/util/Utils.java +++ b/src/com/android/tv/util/Utils.java @@ -37,6 +37,7 @@ import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.text.format.DateUtils; +import android.util.ArraySet; import android.util.Log; import android.view.View; @@ -77,7 +78,6 @@ public class Utils { public static final String EXTRA_KEY_ACTION = "action"; public static final String EXTRA_ACTION_SHOW_TV_INPUT ="show_tv_input"; public static final String EXTRA_KEY_FROM_LAUNCHER = "from_launcher"; - public static final String EXTRA_KEY_RECORDING_URI = "recording_uri"; public static final String EXTRA_KEY_RECORDED_PROGRAM_ID = "recorded_program_id"; public static final String EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME = "recorded_program_seek_time"; public static final String EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED = @@ -119,7 +119,7 @@ public class Utils { // Hardcoded list for known bundled inputs not written by OEM/SOCs. // Bundled (system) inputs not in the list will get the high priority // so they and their channels come first in the UI. - private static final Set<String> BUNDLED_PACKAGE_SET = new HashSet<>(); + private static final Set<String> BUNDLED_PACKAGE_SET = new ArraySet<>(); static { BUNDLED_PACKAGE_SET.add("com.android.tv"); @@ -803,6 +803,18 @@ public class Utils { } /** + * Checks whether a given input is a bundled input. + */ + public static boolean isBundledInput(String inputId) { + for (String prefix : BUNDLED_PACKAGE_SET) { + if (inputId.startsWith(prefix + "/")) { + return true; + } + } + return false; + } + + /** * Returns the canonical genre ID's from the {@code genres}. */ public static int[] getCanonicalGenreIds(String genres) { |