diff options
Diffstat (limited to 'src/com/android/tv/dvr/ui')
36 files changed, 739 insertions, 606 deletions
diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java index 32679421..9cd91a64 100644 --- a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java +++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java @@ -27,13 +27,15 @@ import android.view.View; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import com.android.tv.R; +import com.android.tv.ui.DetailsActivity; + import java.util.Map; /** * TODO: Remove this class once b/32405620 is fixed. This class is for the workaround of b/32405620 * and only for the shared element transition between {@link * com.android.tv.dvr.ui.browse.RecordingCardView} and {@link - * com.android.tv.dvr.ui.browse.DvrDetailsActivity}. + * DetailsActivity}. */ public class ChangeImageTransformWithScaledParent extends ChangeImageTransform { private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix"; diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java index fce94230..5e3caa9c 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java @@ -71,7 +71,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment { public Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getString(R.string.dvr_already_recorded_dialog_title); String description = getString(R.string.dvr_already_recorded_dialog_description); - Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null); + Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null); return new Guidance(title, description, null, image); } diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java index 456ad830..a6bbe137 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java @@ -78,7 +78,7 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment { getContext(), mDuplicate.getStartTimeMs(), DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE)); - Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null); + Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null); return new Guidance(title, description, null, image); } diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java index 65759555..649cc89a 100644 --- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java +++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java @@ -205,7 +205,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { if (description == null) { dismissDialog(); } - Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); + Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null); return new Guidance(title, descriptionPrefix + " " + description, null, icon); } @@ -265,7 +265,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { if (description == null) { dismissDialog(); } - Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); + Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null); return new Guidance(title, descriptionPrefix + " " + description, null, icon); } diff --git a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java b/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java deleted file mode 100644 index 677a6cbb..00000000 --- a/src/com/android/tv/dvr/ui/DvrFutureProgramInfoFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2018 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.widget.GuidanceStylist; -import android.support.v17.leanback.widget.GuidedAction; -import com.android.tv.TvSingletons; -import com.android.tv.data.Program; -import com.android.tv.dvr.data.ScheduledRecording; -import com.android.tv.util.Utils; -import java.util.List; - -/** - * A fragment which shows the formation of a program. - */ -public class DvrFutureProgramInfoFragment extends DvrGuidedStepFragment { - private static final long ACTION_ID_VIEW_SCHEDULE = 1; - private ScheduledRecording mScheduledRecording; - private Program mProgram; - - @Override - public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { - long startTime = mProgram.getStartTimeUtcMillis(); - // TODO(b/71717923): use R.string when the strings are finalized - StringBuilder description = new StringBuilder() - .append("This program will start at ") - .append(Utils.getDurationString(getContext(), startTime, startTime, false)); - if (mScheduledRecording != null) { - description.append("\nThis program has been scheduled for recording."); - } - return new GuidanceStylist.Guidance( - mProgram.getTitle(), description.toString(), null, null); - } - - @Override - public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - Activity activity = getActivity(); - mProgram = getArguments().getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); - mScheduledRecording = - TvSingletons.getSingletons(getContext()) - .getDvrDataManager() - .getScheduledRecordingForProgramId(mProgram.getId()); - actions.add( - new GuidedAction.Builder(activity) - .id(GuidedAction.ACTION_ID_OK) - .title(android.R.string.ok) - .build()); - if (mScheduledRecording != null) { - actions.add( - new GuidedAction.Builder(activity) - .id(ACTION_ID_VIEW_SCHEDULE) - .title("View schedules") - .build()); - } - - } - - @Override - public void onTrackedGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_ID_VIEW_SCHEDULE) { - DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording); - return; - } - dismissDialog(); - } - - @Override - public String getTrackerPrefix() { - return "DvrFutureProgramInfoFragment"; - } -} diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java index 4a713703..e6b54f67 100644 --- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java @@ -18,17 +18,20 @@ package com.android.tv.dvr.ui; import android.app.Activity; import android.content.Context; +import android.content.DialogInterface; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; 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.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelWatchConflictFragment; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.guide.ProgramGuide; +import com.android.tv.ui.DetailsActivity; public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { /** Key for input ID. Type: String. */ @@ -187,11 +190,27 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } } - /** A dialog fragment for {@link DvrFutureProgramInfoFragment}. */ - public static class DvrFutureProgramInfoDialogFragment extends DvrGuidedStepDialogFragment { + /** A dialog fragment for {@link DvrWriteStoragePermissionRationaleFragment}. */ + public static class DvrWriteStoragePermissionRationaleDialogFragment + extends DvrGuidedStepDialogFragment { @Override - protected DvrGuidedStepFragment onCreateGuidedStepFragment() { - return new DvrFutureProgramInfoFragment(); + protected DvrWriteStoragePermissionRationaleFragment onCreateGuidedStepFragment() { + return new DvrWriteStoragePermissionRationaleFragment(); + } + + @Override + public void onDismiss(DialogInterface dialog) { + Activity activity = getActivity(); + if (activity instanceof DetailsActivity) { + activity.requestPermissions( + new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"}, + DetailsActivity.REQUEST_DELETE); + } else if (activity instanceof DvrSeriesDeletionActivity) { + activity.requestPermissions( + new String[] {"android.permission.WRITE_EXTERNAL_STORAGE"}, + DvrSeriesDeletionActivity.REQUEST_DELETE); + } + super.onDismiss(dialog); } } } diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java index e5f40260..02b2da1d 100644 --- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java @@ -25,7 +25,7 @@ import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.util.Log; import com.android.tv.R; -import com.android.tv.dvr.ui.browse.DvrDetailsActivity; +import com.android.tv.ui.DetailsActivity; import java.util.List; public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { @@ -65,7 +65,7 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { @Override public void onTrackedGuidedActionClicked(GuidedAction action) { Activity activity = getActivity(); - if (activity instanceof DvrDetailsActivity) { + if (activity instanceof DetailsActivity) { activity.finish(); } else { dismissDialog(); diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java index 5251e140..72603d03 100644 --- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java +++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java @@ -34,7 +34,6 @@ import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; -import com.android.tv.util.Utils; import java.util.Collections; import java.util.List; @@ -104,12 +103,7 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { mProgram.getEndTimeUtcMillis(), DateUtils.FORMAT_SHOW_TIME)); } else { - description = - Utils.getDurationString( - context, - mProgram.getStartTimeUtcMillis(), - mProgram.getEndTimeUtcMillis(), - true); + description = mProgram.getDurationString(context); } actions.add( new GuidedAction.Builder(context) diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index a2ae1f97..a237f1d2 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java @@ -17,16 +17,34 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.content.pm.PackageManager; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v17.leanback.app.GuidedStepFragment; +import android.util.Log; +import android.widget.Toast; + import com.android.tv.R; import com.android.tv.Starter; +import com.android.tv.TvSingletons; +import com.android.tv.dvr.DvrManager; + +import java.util.ArrayList; +import java.util.List; /** Activity to show details view in DVR. */ public class DvrSeriesDeletionActivity extends Activity { + private static final String TAG = "DvrSeriesDeletionActivity"; + /** Name of series id added to the Intent. */ public static final String SERIES_RECORDING_ID = "series_recording_id"; + public static final int REQUEST_DELETE = 1; + public static final long INVALID_SERIES_RECORDING_ID = -1; + + private long mSeriesRecordingId = INVALID_SERIES_RECORDING_ID; + private final List<Long> mIdsToDelete = new ArrayList<>(); + @Override public void onCreate(Bundle savedInstanceState) { Starter.start(this); @@ -34,9 +52,61 @@ public class DvrSeriesDeletionActivity extends Activity { setContentView(R.layout.activity_dvr_series_settings); // Check savedInstanceState to prevent that activity is being showed with animation. if (savedInstanceState == null) { + mSeriesRecordingId = + getIntent().getLongExtra(SERIES_RECORDING_ID, INVALID_SERIES_RECORDING_ID); DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment(); deletionFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame); } } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case REQUEST_DELETE: + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + deleteSelectedIds(true); + } else { + // NOTE: If Live TV ever supports both embedded and separate DVR inputs + // then we should try to do the delete regardless. + Log.i( + TAG, + "Write permission denied, Not trying to delete the files for series " + + mSeriesRecordingId); + deleteSelectedIds(false); + } + break; + default: + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + private void deleteSelectedIds(boolean deleteFiles) { + TvSingletons singletons = TvSingletons.getSingletons(this); + int recordingSize = + singletons.getDvrDataManager().getRecordedPrograms(mSeriesRecordingId).size(); + if (!mIdsToDelete.isEmpty()) { + DvrManager dvrManager = singletons.getDvrManager(); + dvrManager.removeRecordedPrograms(mIdsToDelete, deleteFiles); + } + Toast.makeText( + this, + getResources() + .getQuantityString( + R.plurals.dvr_msg_episodes_deleted, + mIdsToDelete.size(), + mIdsToDelete.size(), + recordingSize), + Toast.LENGTH_LONG) + .show(); + finish(); + } + + void setIdsToDelete(List<Long> ids) { + mIdsToDelete.clear(); + mIdsToDelete.addAll(ids); + } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java index 685f0a58..ff213231 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java @@ -29,6 +29,7 @@ import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; @@ -53,10 +54,12 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { private static final long ACTION_ID_SELECT_ALL = -111; private static final long ACTION_ID_DELETE = -112; + private DvrManager mDvrManager; private DvrDataManager mDvrDataManager; private DvrWatchedPositionManager mDvrWatchedPositionManager; private List<RecordedProgram> mRecordings; private final Set<Long> mWatchedRecordings = new HashSet<>(); + private final List<Long> mIdsToDelete = new ArrayList<>(); private boolean mAllSelected; private long mSeriesRecordingId; private int mOneLineActionHeight; @@ -67,9 +70,10 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { mSeriesRecordingId = getArguments().getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1); SoftPreconditions.checkArgument(mSeriesRecordingId != -1); - mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); - mDvrWatchedPositionManager = - TvSingletons.getSingletons(context).getDvrWatchedPositionManager(); + TvSingletons singletons = TvSingletons.getSingletons(context); + mDvrManager = singletons.getDvrManager(); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrWatchedPositionManager = singletons.getDvrWatchedPositionManager(); mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId); mOneLineActionHeight = getResources() @@ -158,28 +162,7 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { public void onGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == ACTION_ID_DELETE) { - List<Long> idsToDelete = new ArrayList<>(); - for (GuidedAction guidedAction : getActions()) { - if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID - && guidedAction.isChecked()) { - idsToDelete.add(guidedAction.getId()); - } - } - if (!idsToDelete.isEmpty()) { - DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager(); - dvrManager.removeRecordedPrograms(idsToDelete); - } - Toast.makeText( - getContext(), - getResources() - .getQuantityString( - R.plurals.dvr_msg_episodes_deleted, - idsToDelete.size(), - idsToDelete.size(), - mRecordings.size()), - Toast.LENGTH_LONG) - .show(); - finishGuidedStepFragments(); + delete(); } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { finishGuidedStepFragments(); } else if (actionId == ACTION_ID_SELECT_WATCHED) { @@ -234,6 +217,51 @@ public class DvrSeriesDeletionFragment extends GuidedStepFragment { }; } + private void delete() { + mIdsToDelete.clear(); + for (GuidedAction guidedAction : getActions()) { + if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID + && guidedAction.isChecked()) { + mIdsToDelete.add(guidedAction.getId()); + } + } + ((DvrSeriesDeletionActivity) getActivity()).setIdsToDelete(mIdsToDelete); + if (!PermissionUtils.hasWriteExternalStorage(getContext()) + && doesAnySelectedRecordedProgramNeedWritePermission()) { + DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity()); + } else { + deleteSelectedIds(); + } + } + + private boolean doesAnySelectedRecordedProgramNeedWritePermission() { + for (RecordedProgram r : mRecordings) { + if (mIdsToDelete.contains(r.getId()) + && DvrManager.isFile(r.getDataUri()) + && !DvrManager.isFromBundledInput(r)) { + return true; + } + } + return false; + } + + private void deleteSelectedIds() { + if (!mIdsToDelete.isEmpty()) { + mDvrManager.removeRecordedPrograms(mIdsToDelete, true); + } + Toast.makeText( + getContext(), + getResources() + .getQuantityString( + R.plurals.dvr_msg_episodes_deleted, + mIdsToDelete.size(), + mIdsToDelete.size(), + mRecordings.size()), + Toast.LENGTH_LONG) + .show(); + finishGuidedStepFragments(); + } + private String getWatchedString(long watchedPositionMs, long durationMs) { if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { return getResources() diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java index edb62c96..c6e26850 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java @@ -101,9 +101,9 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { 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); + icon = getResources().getDrawable(R.drawable.quantum_ic_check_circle_white_48, null); } else { - icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); + icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null); } return new GuidanceStylist.Guidance(title, getDescription(), null, icon); } diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java index e93387ab..1ab4c500 100644 --- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java @@ -126,7 +126,7 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { } else { description = getString(R.string.dvr_stop_recording_dialog_description); } - Drawable image = getResources().getDrawable(R.drawable.ic_warning_white_96dp, null); + Drawable image = getResources().getDrawable(R.drawable.quantum_ic_warning_white_96, null); return new Guidance(title, description, null, image); } diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java index 16afbdef..a121cf99 100644 --- a/src/com/android/tv/dvr/ui/DvrUiHelper.java +++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java @@ -37,10 +37,10 @@ import android.text.TextUtils; import android.text.style.TextAppearanceSpan; import android.widget.ImageView; import android.widget.Toast; + import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvSingletons; -import com.android.tv.common.BuildConfig; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.recording.RecordingStorageStatusManager; import com.android.tv.common.util.CommonUtils; @@ -57,7 +57,6 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialog import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrFutureProgramInfoDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment; @@ -65,15 +64,17 @@ import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialog 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.DvrWriteStoragePermissionRationaleDialogFragment; import com.android.tv.dvr.ui.browse.DvrBrowseActivity; -import com.android.tv.dvr.ui.browse.DvrDetailsActivity; import com.android.tv.dvr.ui.list.DvrHistoryActivity; import com.android.tv.dvr.ui.list.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.DvrSchedulesFragment; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import com.android.tv.dvr.ui.playback.DvrPlaybackActivity; +import com.android.tv.ui.DetailsActivity; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; + import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -241,13 +242,9 @@ public class DvrUiHelper { } /** Shows program information dialog. */ - public static void showProgramInfoDialog(Activity activity, Program program) { - if (program == null || !BuildConfig.ENG) { - return; - } - Bundle args = new Bundle(); - args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); - showDialogFragment(activity, new DvrFutureProgramInfoDialogFragment(), args, false, true); + public static void showWriteStoragePermissionRationaleDialog(Activity activity) { + showDialogFragment(activity, new DvrWriteStoragePermissionRationaleDialogFragment(), + new Bundle(), false, false); } /** @@ -577,47 +574,43 @@ public class DvrUiHelper { if (dvrItem == null) { return; } - Intent intent = new Intent(activity, DvrDetailsActivity.class); + Intent intent = new Intent(activity, DetailsActivity.class); long recordingId; int viewType; if (dvrItem instanceof ScheduledRecording) { ScheduledRecording schedule = (ScheduledRecording) dvrItem; recordingId = schedule.getId(); if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { - viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; + viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW; } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW; + viewType = DetailsActivity.CURRENT_RECORDING_VIEW; } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED && schedule.getRecordedProgramId() != null) { recordingId = schedule.getRecordedProgramId(); - viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; + viewType = DetailsActivity.RECORDED_PROGRAM_VIEW; } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { - viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW; + viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW; hideViewSchedule = true; - // TODO(b/72638385): pass detailed error message - intent.putExtra( - DvrDetailsActivity.EXTRA_FAILED_MESSAGE, - activity.getString(R.string.dvr_recording_failed)); } else { return; } } else if (dvrItem instanceof RecordedProgram) { recordingId = ((RecordedProgram) dvrItem).getId(); - viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW; + viewType = DetailsActivity.RECORDED_PROGRAM_VIEW; } else if (dvrItem instanceof SeriesRecording) { recordingId = ((SeriesRecording) dvrItem).getId(); - viewType = DvrDetailsActivity.SERIES_RECORDING_VIEW; + viewType = DetailsActivity.SERIES_RECORDING_VIEW; } else { return; } - intent.putExtra(DvrDetailsActivity.RECORDING_ID, recordingId); - intent.putExtra(DvrDetailsActivity.DETAILS_VIEW_TYPE, viewType); - intent.putExtra(DvrDetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); + intent.putExtra(DetailsActivity.RECORDING_ID, recordingId); + intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, viewType); + intent.putExtra(DetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule); Bundle bundle = null; if (imageView != null) { bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( - activity, imageView, DvrDetailsActivity.SHARED_ELEMENT_NAME) + activity, imageView, DetailsActivity.SHARED_ELEMENT_NAME) .toBundle(); } activity.startActivity(intent, bundle); diff --git a/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java new file mode 100644 index 00000000..c93f5831 --- /dev/null +++ b/src/com/android/tv/dvr/ui/DvrWriteStoragePermissionRationaleFragment.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 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.content.res.Resources; +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 shows the rationale when requesting android.permission.WRITE_EXTERNAL_STORAGE. + */ +public class DvrWriteStoragePermissionRationaleFragment extends DvrGuidedStepFragment { + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + Resources res = getContext().getResources(); + String title = res.getString(R.string.write_storage_permission_rationale_title); + String description = res.getString(R.string.write_storage_permission_rationale_description); + return new GuidanceStylist.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 onTrackedGuidedActionClicked(GuidedAction action) { + dismissDialog(); + } + + @Override + public String getTrackerPrefix() { + return "DvrWriteStoragePermissionRationaleFragment"; + } +} diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java index f3a6fea4..41ace9a4 100644 --- a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java +++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java @@ -27,9 +27,11 @@ import android.view.View; import android.view.ViewGroup; import android.widget.Button; -// This class is adapted from Leanback's library, which does not support action icon with one-line -// label. This class modified its getPresenter method to support the above situation. -class ActionPresenterSelector extends PresenterSelector { +/** + * This class is adapted from Leanback's library, which does not support action icon with one-line + * label. This class modified its getPresenter method to support the above situation. + */ +public class ActionPresenterSelector extends PresenterSelector { private final Presenter mOneLineActionPresenter = new OneLineActionPresenter(); private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter(); private final Presenter[] mPresenters = diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java index 7e7e1f75..8c311d68 100644 --- a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java @@ -18,23 +18,34 @@ package com.android.tv.dvr.ui.browse; import android.content.Context; import android.content.res.Resources; +import android.media.tv.TvInputManager; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import com.android.tv.R; import com.android.tv.TvSingletons; +import com.android.tv.common.flags.has.HasConcurrentDvrPlaybackFlags; import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.common.flags.ConcurrentDvrPlaybackFlags; /** {@link RecordingDetailsFragment} for current recording in DVR. */ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { private static final int ACTION_STOP_RECORDING = 1; + private static final int ACTION_RESUME_PLAYING = 2; + private static final int ACTION_PLAY_FROM_BEGINNING = 3; private DvrDataManager mDvrDataManger; + private RecordedProgram mRecordedProgram; + private DvrWatchedPositionManager mDvrWatchedPositionManager; + private ConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags; + private boolean mPaused; private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = new DvrDataManager.ScheduledRecordingListener() { @Override @@ -68,10 +79,32 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { super.onAttach(context); mDvrDataManger = TvSingletons.getSingletons(context).getDvrDataManager(); mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener); + mDvrWatchedPositionManager = + TvSingletons.getSingletons(getActivity()).getDvrWatchedPositionManager(); + mConcurrentDvrPlaybackFlags = HasConcurrentDvrPlaybackFlags.fromContext(context); + } + + @Override + public void onResume() { + super.onResume(); + if (mPaused) { + updateActions(); + mPaused = false; + } + } + + @Override + public void onPause() { + super.onPause(); + mPaused = true; } @Override protected SparseArrayObjectAdapter onCreateActionsAdapter() { + Long recordedProgramId = getRecording().getRecordedProgramId(); + if (recordedProgramId != null) { + mRecordedProgram = mDvrDataManger.getRecordedProgram(recordedProgramId); + } SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); Resources res = getResources(); @@ -82,6 +115,35 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { res.getString(R.string.dvr_detail_stop_recording), null, res.getDrawable(R.drawable.lb_ic_stop))); + if (mConcurrentDvrPlaybackFlags.enabled() + && mRecordedProgram != null + && mRecordedProgram.isPartial()) { + 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))); + adapter.set( + ACTION_PLAY_FROM_BEGINNING, + new Action( + ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_play_from_beginning), + null, + res.getDrawable(R.drawable.lb_ic_replay))); + } else { + adapter.set( + ACTION_PLAY_FROM_BEGINNING, + new Action( + ACTION_PLAY_FROM_BEGINNING, + res.getString(R.string.dvr_detail_watch), + null, + res.getDrawable(R.drawable.lb_ic_play))); + } + } return adapter; } @@ -107,6 +169,13 @@ public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { } } }); + } else if (action.getId() == ACTION_RESUME_PLAYING) { + startPlayback( + mRecordedProgram, + mDvrWatchedPositionManager.getWatchedPosition( + mRecordedProgram.getId())); + } else if (action.getId() == ACTION_PLAY_FROM_BEGINNING) { + startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME); } } }; diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java index cba6293b..e179743c 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java @@ -22,6 +22,7 @@ import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvSingletons; +import com.android.tv.data.Program; import com.android.tv.data.api.Channel; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; @@ -29,7 +30,7 @@ import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrUiHelper; /** A class for details content. */ -class DetailsContent { +public class DetailsContent { /** Constant for invalid time. */ public static final long INVALID_TIME = -1; @@ -40,6 +41,7 @@ class DetailsContent { private String mLogoImageUri; private String mBackgroundImageUri; private boolean mUsingChannelLogo; + private boolean mShowErrorMessage; static DetailsContent createFromRecordedProgram( Context context, RecordedProgram recordedProgram) { @@ -59,6 +61,23 @@ class DetailsContent { .build(context); } + public static DetailsContent createFromProgram(Context context, Program program) { + return new DetailsContent.Builder() + .setChannelId(program.getChannelId()) + .setProgramTitle(program.getTitle()) + .setSeasonNumber(program.getSeasonNumber()) + .setEpisodeNumber(program.getEpisodeNumber()) + .setStartTimeUtcMillis(program.getStartTimeUtcMillis()) + .setEndTimeUtcMillis(program.getEndTimeUtcMillis()) + .setDescription( + TextUtils.isEmpty(program.getLongDescription()) + ? program.getDescription() + : program.getLongDescription()) + .setPosterArtUri(program.getPosterArtUri()) + .setThumbnailUri(program.getThumbnailUri()) + .build(context); + } + static DetailsContent createFromSeriesRecording( Context context, SeriesRecording seriesRecording) { return new DetailsContent.Builder() @@ -79,37 +98,9 @@ class DetailsContent { TvSingletons.getSingletons(context) .getChannelDataManager() .getChannel(scheduledRecording.getChannelId()); - String description = - !TextUtils.isEmpty(scheduledRecording.getProgramDescription()) - ? scheduledRecording.getProgramDescription() - : scheduledRecording.getProgramLongDescription(); - if (TextUtils.isEmpty(description)) { - description = channel != null ? channel.getDescription() : null; - } - return new DetailsContent.Builder() - .setChannelId(scheduledRecording.getChannelId()) - .setProgramTitle(scheduledRecording.getProgramTitle()) - .setSeasonNumber(scheduledRecording.getSeasonNumber()) - .setEpisodeNumber(scheduledRecording.getEpisodeNumber()) - .setStartTimeUtcMillis(scheduledRecording.getStartTimeMs()) - .setEndTimeUtcMillis(scheduledRecording.getEndTimeMs()) - .setDescription(description) - .setPosterArtUri(scheduledRecording.getProgramPosterArtUri()) - .setThumbnailUri(scheduledRecording.getProgramThumbnailUri()) - .build(context); - } - - static DetailsContent createFromFailedScheduledRecording( - Context context, ScheduledRecording scheduledRecording, String errMsg) { - Channel channel = - TvSingletons.getSingletons(context) - .getChannelDataManager() - .getChannel(scheduledRecording.getChannelId()); String description; - if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED - && errMsg != null) { - description = errMsg - + " (Error code: " + scheduledRecording.getFailedReason() + ")"; + if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + description = getErrorMessage(context, scheduledRecording); } else { description = !TextUtils.isEmpty(scheduledRecording.getProgramDescription()) @@ -129,9 +120,39 @@ class DetailsContent { .setDescription(description) .setPosterArtUri(scheduledRecording.getProgramPosterArtUri()) .setThumbnailUri(scheduledRecording.getProgramThumbnailUri()) + .setShowErrorMessage( + scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) .build(context); } + private static String getErrorMessage(Context context, ScheduledRecording recording) { + int reason = recording.getFailedReason() == null + ? ScheduledRecording.FAILED_REASON_OTHER + : recording.getFailedReason(); + switch (reason) { + case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED: + return context.getString(R.string.dvr_recording_failed_not_started); + case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY: + return context.getString(R.string.dvr_recording_failed_resource_busy); + case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE: + return context.getString( + R.string.dvr_recording_failed_input_unavailable, + recording.getInputId()); + case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED: + return context.getString(R.string.dvr_recording_failed_input_dvr_unsupported); + case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE: + return context.getString(R.string.dvr_recording_failed_insufficient_space); + case ScheduledRecording.FAILED_REASON_OTHER: // fall through + case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through + case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through + case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through + case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through + case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through + default: + return context.getString(R.string.dvr_recording_failed_system_failure, reason); + } + } + private DetailsContent() {} /** Returns title. */ @@ -169,6 +190,11 @@ class DetailsContent { return mUsingChannelLogo; } + /** Returns if the error message should be shown. */ + public boolean shouldShowErrorMessage() { + return mShowErrorMessage; + } + /** Copies other details content. */ public void copyFrom(DetailsContent other) { if (this == other) { @@ -181,6 +207,7 @@ class DetailsContent { mLogoImageUri = other.mLogoImageUri; mBackgroundImageUri = other.mBackgroundImageUri; mUsingChannelLogo = other.mUsingChannelLogo; + mShowErrorMessage = other.mShowErrorMessage; } /** A class for building details content. */ @@ -266,6 +293,11 @@ class DetailsContent { return this; } + private Builder setShowErrorMessage(boolean showErrorMessage) { + mDetailsContent.mShowErrorMessage = showErrorMessage; + return this; + } + private void createStyledTitle(Context context, Channel channel) { CharSequence title = DvrUiHelper.getStyledTitleWithEpisodeNumber( diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java index aec8c411..6b5fd1fd 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java @@ -45,12 +45,13 @@ import com.android.tv.util.Utils; * The latter class are re-used to provide a customized version of {@link * android.support.v17.leanback.widget.DetailsOverviewRow}. */ -class DetailsContentPresenter extends Presenter { +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 LinearLayout mErrorMessage; final TextView mBody; final TextView mReadMoreView; final int mTitleMargin; @@ -150,6 +151,8 @@ class DetailsContentPresenter extends Presenter { }); mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title); mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle); + mErrorMessage = + (LinearLayout) view.findViewById(R.id.dvr_details_description_error_message); mBody = (TextView) view.findViewById(R.id.dvr_details_description_body); mDescriptionContainer = (LinearLayout) view.findViewById(R.id.dvr_details_description_container); @@ -321,6 +324,9 @@ class DetailsContentPresenter extends Presenter { if (TextUtils.isEmpty(detailsContent.getDescription())) { vh.mBody.setVisibility(View.GONE); } else { + if (detailsContent.shouldShowErrorMessage()) { + vh.mErrorMessage.setVisibility(View.VISIBLE); + } vh.mBody.setText(detailsContent.getDescription()); vh.mBody.setVisibility(View.VISIBLE); vh.mBody.setLineSpacing( diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java index 849360b8..4e41daee 100644 --- a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java @@ -24,7 +24,7 @@ import android.os.Handler; import android.support.v17.leanback.app.BackgroundManager; /** The Background Helper. */ -class DetailsViewBackgroundHelper { +public class DetailsViewBackgroundHelper { // Background delay serves to avoid kicking off expensive bitmap loading // in case multiple backgrounds are set in quick succession. private static final int SET_BACKGROUND_DELAY_MS = 100; diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java index 6cc1c7a1..5743ea5c 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java @@ -22,9 +22,15 @@ import android.media.tv.TvInputManager; import android.os.Bundle; import com.android.tv.R; import com.android.tv.Starter; +import com.android.tv.perf.PerformanceMonitorManagerFactory; /** {@link android.app.Activity} for DVR UI. */ public class DvrBrowseActivity extends Activity { + + { + PerformanceMonitorManagerFactory.create().getStartupMeasure().onActivityInit(); + } + private DvrBrowseFragment mFragment; @Override diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java index 40b3a1f0..17ba1939 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java @@ -31,9 +31,7 @@ import android.support.v17.leanback.widget.TitleViewAdapter; import android.util.Log; import android.view.View; import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; - import com.android.tv.R; -import com.android.tv.TvFeatures; import com.android.tv.TvSingletons; import com.android.tv.data.GenreItems; import com.android.tv.dvr.DvrDataManager; @@ -47,7 +45,7 @@ import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.SortedArrayAdapter; - +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -66,7 +64,7 @@ public class DvrBrowseFragment extends BrowseFragment private static final String TAG = "DvrBrowseFragment"; private static final boolean DEBUG = false; - private static final int MAX_RECENT_ITEM_COUNT = 10; + private static final int MAX_RECENT_ITEM_COUNT = 4; private static final int MAX_SCHEDULED_ITEM_COUNT = 4; private boolean mShouldShowScheduleRow; @@ -104,93 +102,84 @@ public class DvrBrowseFragment extends BrowseFragment }; private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = - new Comparator<Object>() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof SeriesRecording) { - lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); - } - if (rhs instanceof SeriesRecording) { - rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); - } - if (lhs instanceof RecordedProgram) { - if (rhs instanceof RecordedProgram) { - return RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .reversed() - .compare((RecordedProgram) lhs, (RecordedProgram) rhs); - } else { - return -1; - } - } else if (rhs instanceof RecordedProgram) { - return 1; + (Object lhs, Object rhs) -> { + if (lhs instanceof SeriesRecording) { + lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId()); + } + if (rhs instanceof SeriesRecording) { + rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId()); + } + if (lhs instanceof RecordedProgram) { + if (rhs instanceof RecordedProgram) { + return RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .reversed() + .compare((RecordedProgram) lhs, (RecordedProgram) rhs); } else { - return 0; + return -1; } + } else if (rhs instanceof RecordedProgram) { + return 1; + } else { + return 0; } }; private static final Comparator<Object> SCHEDULE_COMPARATOR = - new Comparator<Object>() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof ScheduledRecording) { - if (rhs instanceof ScheduledRecording) { - return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR - .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); - } else { - return -1; - } - } else if (rhs instanceof ScheduledRecording) { - return 1; + (Object lhs, Object rhs) -> { + if (lhs instanceof ScheduledRecording) { + if (rhs instanceof ScheduledRecording) { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); } else { - return 0; + return -1; } + } else if (rhs instanceof ScheduledRecording) { + return 1; + } else { + return 0; } }; static final Comparator<Object> RECENT_ROW_COMPARATOR = - new Comparator<Object>() { - @Override - public int compare(Object lhs, Object rhs) { - if (lhs instanceof ScheduledRecording) { - if (rhs instanceof ScheduledRecording) { - return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR - .reversed() - .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); - } else if (rhs instanceof RecordedProgram) { - ScheduledRecording scheduled = (ScheduledRecording) lhs; - RecordedProgram recorded = (RecordedProgram) rhs; - int compare = - Long.compare( - recorded.getStartTimeUtcMillis(), - scheduled.getStartTimeMs()); - // recorded program first when the start times are the same - return compare == 0 ? 1 : compare; - } else { - return -1; - } - } else if (lhs instanceof RecordedProgram) { - if (rhs instanceof RecordedProgram) { - return RecordedProgram.START_TIME_THEN_ID_COMPARATOR - .reversed() - .compare((RecordedProgram) lhs, (RecordedProgram) rhs); - } else if (rhs instanceof ScheduledRecording) { - RecordedProgram recorded = (RecordedProgram) lhs; - ScheduledRecording scheduled = (ScheduledRecording) rhs; - int compare = - Long.compare( - scheduled.getStartTimeMs(), - recorded.getStartTimeUtcMillis()); - // recorded program first when the start times are the same - return compare == 0 ? -1 : compare; - } else { - return -1; - } + (Object lhs, Object rhs) -> { + if (lhs instanceof ScheduledRecording) { + if (rhs instanceof ScheduledRecording) { + return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR + .reversed() + .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs); + } else if (rhs instanceof RecordedProgram) { + ScheduledRecording scheduled = (ScheduledRecording) lhs; + RecordedProgram recorded = (RecordedProgram) rhs; + int compare = + Long.compare( + recorded.getStartTimeUtcMillis(), + scheduled.getStartTimeMs()); + // recorded program first when the start times are the same + return compare == 0 ? 1 : compare; + } else { + return -1; + } + } else if (lhs instanceof RecordedProgram) { + if (rhs instanceof RecordedProgram) { + return RecordedProgram.START_TIME_THEN_ID_COMPARATOR + .reversed() + .compare((RecordedProgram) lhs, (RecordedProgram) rhs); + } else if (rhs instanceof ScheduledRecording) { + RecordedProgram recorded = (RecordedProgram) lhs; + ScheduledRecording scheduled = (ScheduledRecording) rhs; + int compare = + Long.compare( + scheduled.getStartTimeMs(), + recorded.getStartTimeUtcMillis()); + // recorded program first when the start times are the same + return compare == 0 ? -1 : compare; } else { - return !(rhs instanceof RecordedProgram) - && !(rhs instanceof ScheduledRecording) - ? 0 : 1; + return -1; } + } else { + return !(rhs instanceof RecordedProgram) && !(rhs instanceof ScheduledRecording) + ? 0 + : 1; } }; @@ -207,13 +196,7 @@ public class DvrBrowseFragment extends BrowseFragment } }; - private final Runnable mUpdateRowsRunnable = - new Runnable() { - @Override - public void run() { - updateRows(); - } - }; + private final Runnable mUpdateRowsRunnable = this::updateRows; @Override public void onCreate(Bundle savedInstanceState) { @@ -233,13 +216,10 @@ public class DvrBrowseFragment extends BrowseFragment SeriesRecording.class, new SeriesRecordingPresenter(context)) .addClassPresenter( FullScheduleCardHolder.class, - new FullSchedulesCardPresenter(context)); + new FullSchedulesCardPresenter(context)) + .addClassPresenter( + DvrHistoryCardHolder.class, new DvrHistoryCardPresenter(context)); - if (TvFeatures.DVR_FAILED_LIST.isEnabled(context)) { - mPresenterSelector.addClassPresenter( - DvrHistoryCardHolder.class, - new DvrHistoryCardPresenter(context)); - } mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); mGenreLabels.add(getString(R.string.dvr_main_others)); prepareUiElements(); @@ -310,7 +290,9 @@ public class DvrBrowseFragment extends BrowseFragment @Override public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { for (RecordedProgram recordedProgram : recordedPrograms) { - handleRecordedProgramChanged(recordedProgram); + if (recordedProgram.isVisible()) { + handleRecordedProgramChanged(recordedProgram); + } } postUpdateRows(); } @@ -340,6 +322,9 @@ public class DvrBrowseFragment extends BrowseFragment public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { for (ScheduledRecording scheduleRecording : scheduledRecordings) { mScheduleAdapter.remove(scheduleRecording); + if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + mRecentAdapter.remove(scheduleRecording); + } } } @@ -351,6 +336,9 @@ public class DvrBrowseFragment extends BrowseFragment } else { mScheduleAdapter.removeWithId(scheduleRecording); } + if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + mRecentAdapter.change(scheduleRecording); + } } } @@ -443,16 +431,17 @@ public class DvrBrowseFragment extends BrowseFragment mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); // Recorded Programs. for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, false); - } - if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())) { - // only get failed recordings - for (ScheduledRecording scheduledRecording - : mDvrDataManager.getFailedScheduledRecordings()) { - onScheduledRecordingAdded(scheduledRecording); + if (recordedProgram.isVisible()) { + handleRecordedProgramAdded(recordedProgram, false); } - mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER); } + // only get failed recordings + for (ScheduledRecording scheduledRecording : + mDvrDataManager.getFailedScheduledRecordings()) { + onScheduledRecordingAdded(scheduledRecording); + } + mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER); + // Series Recordings. Series recordings should be added after recorded programs, because // we build series recordings' latest program information while adding recorded // programs. @@ -592,9 +581,9 @@ public class DvrBrowseFragment extends BrowseFragment } } - private List<RecordedProgramAdapter> getGenreAdapters(String[] genres) { + private List<RecordedProgramAdapter> getGenreAdapters(ImmutableList<String> genres) { List<RecordedProgramAdapter> result = new ArrayList<>(); - if (genres == null || genres.length == 0) { + if (genres == null || genres.isEmpty()) { result.add(mGenreAdapters[mGenreAdapters.length - 1]); } else { for (String genre : genres) { @@ -642,8 +631,8 @@ public class DvrBrowseFragment extends BrowseFragment private void updateRows() { int visibleRowsCount = 1; // Schedule's Row will never be empty - int recentRowMinSize = TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) ? 1 : 0; - if (mRecentAdapter.size() <= recentRowMinSize) { + if (mRecentAdapter.size() <= 1) { + // remove the row if there is only the DVR history card mRowsAdapter.remove(mRecentRow); } else { if (mRowsAdapter.indexOf(mRecentRow) < 0) { @@ -673,6 +662,9 @@ public class DvrBrowseFragment extends BrowseFragment } } } + if (getSelectedPosition() >= mRowsAdapter.size()) { + setSelectedPosition(mRecentAdapter.size() - 1); + } } private boolean needToShowScheduledRecording(ScheduledRecording recording) { @@ -713,16 +705,13 @@ public class DvrBrowseFragment extends BrowseFragment 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); + (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); }); } diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java deleted file mode 100644 index 0336b319..00000000 --- a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java +++ /dev/null @@ -1,144 +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.browse; - -import android.app.Activity; -import android.os.Bundle; -import android.support.v17.leanback.app.DetailsFragment; -import android.transition.Transition; -import android.transition.Transition.TransitionListener; -import android.view.View; -import com.android.tv.R; -import com.android.tv.Starter; -import com.android.tv.dialog.PinDialogFragment; - -/** Activity to show details view in DVR. */ -public class DvrDetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener { - /** Name of record id added to the Intent. */ - public static final String RECORDING_ID = "record_id"; - - /** - * Name of flag added to the Intent to determine if details view should hide "View schedule" - * button. - */ - public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule"; - - /** Name of details view's type added to the intent. */ - public static final String DETAILS_VIEW_TYPE = "details_view_type"; - - /** Name of shared element between activities. */ - public static final String SHARED_ELEMENT_NAME = "shared_element"; - - /** Name of error message of a failed recording */ - public static final String EXTRA_FAILED_MESSAGE = "failed_message"; - - /** CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. */ - public static final int CURRENT_RECORDING_VIEW = 1; - - /** SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR. */ - public static final int SCHEDULED_RECORDING_VIEW = 2; - - /** RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR. */ - public static final int RECORDED_PROGRAM_VIEW = 3; - - /** SERIES_RECORDING_VIEW refers to series recording in DVR. */ - public static final int SERIES_RECORDING_VIEW = 4; - - private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener; - - @Override - public void onCreate(Bundle savedInstanceState) { - Starter.start(this); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_dvr_details); - long recordId = getIntent().getLongExtra(RECORDING_ID, -1); - int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1); - boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false); - String failedMsg = getIntent().getStringExtra(EXTRA_FAILED_MESSAGE); - if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) { - Bundle args = new Bundle(); - args.putLong(RECORDING_ID, recordId); - DetailsFragment detailsFragment = null; - if (detailsViewType == CURRENT_RECORDING_VIEW) { - detailsFragment = new CurrentRecordingDetailsFragment(); - } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) { - args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule); - args.putString(EXTRA_FAILED_MESSAGE, failedMsg); - detailsFragment = new ScheduledRecordingDetailsFragment(); - } else if (detailsViewType == RECORDED_PROGRAM_VIEW) { - detailsFragment = new RecordedProgramDetailsFragment(); - } else if (detailsViewType == SERIES_RECORDING_VIEW) { - detailsFragment = new SeriesRecordingDetailsFragment(); - } - detailsFragment.setArguments(args); - getFragmentManager() - .beginTransaction() - .replace(R.id.dvr_details_view_frame, detailsFragment) - .commit(); - } - - // This is a workaround for the focus on O device - addTransitionListener(); - } - - @Override - public void onPinChecked(boolean checked, int type, String rating) { - if (mOnPinCheckedListener != null) { - mOnPinCheckedListener.onPinChecked(checked, type, rating); - } - } - - void setOnPinCheckListener(PinDialogFragment.OnPinCheckedListener listener) { - mOnPinCheckedListener = listener; - } - - private void addTransitionListener() { - getWindow() - .getSharedElementEnterTransition() - .addListener( - new TransitionListener() { - @Override - public void onTransitionStart(Transition transition) { - // Do nothing - } - - @Override - public void onTransitionEnd(Transition transition) { - View actions = findViewById(R.id.details_overview_actions); - if (actions != null) { - actions.requestFocus(); - } - } - - @Override - public void onTransitionCancel(Transition transition) { - // Do nothing - - } - - @Override - public void onTransitionPause(Transition transition) { - // Do nothing - } - - @Override - public void onTransitionResume(Transition transition) { - // Do nothing - } - }); - } -} diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java index 8f4e4dab..f90981f0 100644 --- a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java @@ -47,8 +47,10 @@ import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener; import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.parental.ParentalControlSettings; +import com.android.tv.ui.DetailsActivity; import com.android.tv.util.ToastUtils; import com.android.tv.util.images.ImageLoader; +import com.google.common.collect.ImmutableList; import java.io.File; abstract class DvrDetailsFragment extends DetailsFragment { @@ -89,7 +91,7 @@ abstract class DvrDetailsFragment extends DetailsFragment { rowPresenter.setBackgroundColor( getResources().getColor(R.color.common_tv_background, null)); rowPresenter.setSharedElementEnterTransition( - getActivity(), DvrDetailsActivity.SHARED_ELEMENT_NAME); + getActivity(), DetailsActivity.SHARED_ELEMENT_NAME); rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); setAdapter(mRowsAdapter); @@ -221,7 +223,7 @@ abstract class DvrDetailsFragment extends DetailsFragment { checkPinToPlay(recordedProgram, seekTimeMs); return; } - TvContentRating[] ratings = recordedProgram.getContentRatings(); + ImmutableList<TvContentRating> ratings = recordedProgram.getContentRatings(); TvContentRating blockRatings = parental.getBlockedRating(ratings); if (blockRatings != null) { checkPinToPlay(recordedProgram, seekTimeMs); @@ -245,15 +247,14 @@ abstract class DvrDetailsFragment extends DetailsFragment { } private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { - SoftPreconditions.checkState(getActivity() instanceof DvrDetailsActivity); - if (getActivity() instanceof DvrDetailsActivity) { - ((DvrDetailsActivity) getActivity()) + SoftPreconditions.checkState(getActivity() instanceof DetailsActivity); + if (getActivity() instanceof DetailsActivity) { + ((DetailsActivity) getActivity()) .setOnPinCheckListener( new OnPinCheckedListener() { @Override public void onPinChecked(boolean checked, int type, String rating) { - ((DvrDetailsActivity) getActivity()) - .setOnPinCheckListener(null); + ((DetailsActivity) getActivity()).setOnPinCheckListener(null); if (checked && type == PinDialogFragment diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java index 47b1a198..bf963547 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java @@ -24,10 +24,13 @@ import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import com.android.tv.R; import com.android.tv.TvSingletons; +import com.android.tv.common.util.PermissionUtils; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.ui.DetailsActivity; /** {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. */ public class RecordedProgramDetailsFragment extends DvrDetailsFragment @@ -80,7 +83,7 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment @Override protected boolean onLoadRecordingDetails(Bundle args) { - long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID); + long recordedProgramId = args.getLong(DetailsActivity.RECORDING_ID); mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId); return mRecordedProgram != null; } @@ -138,15 +141,24 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment mDvrWatchedPositionManager.getWatchedPosition( mRecordedProgram.getId())); } else if (action.getId() == ACTION_DELETE_RECORDING) { - DvrManager dvrManager = - TvSingletons.getSingletons(getActivity()).getDvrManager(); - dvrManager.removeRecordedProgram(mRecordedProgram); - getActivity().finish(); + delete(); } } }; } + private void delete() { + if (!PermissionUtils.hasWriteExternalStorage(getContext()) + && DvrManager.isFile(mRecordedProgram.getDataUri()) + && !DvrManager.isFromBundledInput(mRecordedProgram)) { + DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity()); + } else { + DvrManager dvrManager = TvSingletons.getSingletons(getActivity()).getDvrManager(); + dvrManager.removeRecordedProgram(mRecordedProgram, true); + getActivity().finish(); + } + } + @Override public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {} diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java index fe3c52d9..c83ceaf0 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java +++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java @@ -48,11 +48,10 @@ public class RecordingCardView extends BaseCardView { private final int mImageWidth; private final int mImageHeight; private String mImageUri; + private final ImageView mContentIconView; private final TextView mMajorContentView; private final TextView mMinorContentView; private final ProgressBar mProgressBar; - private final View mAffiliatedIconContainer; - private final ImageView mAffiliatedIcon; private final Drawable mDefaultImage; private final FrameLayout mTitleArea; private final TextView mFoldedTitleView; @@ -94,8 +93,7 @@ public 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); + mContentIconView = (ImageView) findViewById(R.id.content_icon); mMajorContentView = (TextView) findViewById(R.id.content_major); mMinorContentView = (TextView) findViewById(R.id.content_minor); mTitleArea = (FrameLayout) findViewById(R.id.title_area); @@ -184,6 +182,7 @@ public class RecordingCardView extends BaseCardView { } void setContent(CharSequence majorContent, CharSequence minorContent) { + mContentIconView.setVisibility(View.GONE); if (!TextUtils.isEmpty(majorContent)) { mMajorContentView.setText(majorContent); mMajorContentView.setVisibility(View.VISIBLE); @@ -198,6 +197,24 @@ public class RecordingCardView extends BaseCardView { } } + void setRecordingFailedContent(Context context) { + mContentIconView.setVisibility(View.VISIBLE); + mContentIconView.setImageResource(R.drawable.ic_error_outline_pink_24dp); + mMajorContentView.setText(context.getString(R.string.dvr_recording_failed_no_period)); + mMajorContentView.setVisibility(View.VISIBLE); + mMajorContentView.setTextColor( + getResources().getColor(R.color.dvr_recording_failed_text_color, null)); + } + + void setRecordingConflictContent(Context context) { + mContentIconView.setVisibility(View.VISIBLE); + mContentIconView.setImageResource(R.drawable.ic_warning_yellow_24dp); + mMajorContentView.setText(context.getString(R.string.dvr_recording_conflict)); + mMajorContentView.setVisibility(View.VISIBLE); + mMajorContentView.setTextColor( + getResources().getColor(R.color.dvr_recording_conflict_text_color, null)); + } + /** Sets progress bar. If progress is {@code null}, hides progress bar. */ void setProgressBar(Integer progress) { if (progress == null) { @@ -245,19 +262,6 @@ public class RecordingCardView extends BaseCardView { } /** - * Sets the affiliated icon of the card view, which will be displayed at the lower-right corner - * of the poster. - */ - public void setAffiliatedIcon(int imageResId) { - if (imageResId > 0) { - mAffiliatedIconContainer.setVisibility(View.VISIBLE); - mAffiliatedIcon.setImageResource(imageResId); - } else { - mAffiliatedIconContainer.setVisibility(View.INVISIBLE); - } - } - - /** * Sets the background image URI of the card view, which will be displayed as background when * the view is clicked and shows its details fragment. */ diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java index aa2ccf75..243681c6 100644 --- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java @@ -20,6 +20,7 @@ import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; import com.android.tv.TvSingletons; import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.ui.DetailsActivity; /** {@link DetailsFragment} for recordings in DVR. */ abstract class RecordingDetailsFragment extends DvrDetailsFragment { @@ -33,7 +34,7 @@ abstract class RecordingDetailsFragment extends DvrDetailsFragment { @Override protected boolean onLoadRecordingDetails(Bundle args) { - long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); + long scheduledRecordingId = args.getLong(DetailsActivity.RECORDING_ID); mRecording = TvSingletons.getSingletons(getContext()) .getDvrDataManager() diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java index 302b8318..f08bb12b 100644 --- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java @@ -21,10 +21,12 @@ import android.os.Bundle; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; + import com.android.tv.R; import com.android.tv.TvSingletons; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.ui.DvrUiHelper; +import com.android.tv.ui.DetailsActivity; /** {@link RecordingDetailsFragment} for scheduled recording in DVR. */ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment { @@ -34,14 +36,12 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment private DvrManager mDvrManager; private Action mScheduleAction; private boolean mHideViewSchedule; - private String mFailedMessage; @Override public void onCreate(Bundle savedInstance) { Bundle args = getArguments(); mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager(); - mHideViewSchedule = args.getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE); - mFailedMessage = args.getString(DvrDetailsActivity.EXTRA_FAILED_MESSAGE); + mHideViewSchedule = args.getBoolean(DetailsActivity.HIDE_VIEW_SCHEDULE); super.onCreate(savedInstance); } @@ -54,17 +54,6 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment } @Override - protected void onCreateInternal() { - if (mFailedMessage == null) { - super.onCreateInternal(); - return; - } - setDetailsOverviewRow( - DetailsContent.createFromFailedScheduledRecording( - getContext(), getScheduledRecording(), mFailedMessage)); - } - - @Override protected SparseArrayObjectAdapter onCreateActionsAdapter() { SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java index 8e028689..3d279354 100644 --- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java @@ -119,21 +119,17 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> { DetailsContent details = DetailsContent.createFromScheduledRecording(mContext, recording); cardView.setTitle(details.getTitle()); cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo()); - if (mDvrManager.isConflicting(recording)) { - cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); - } else if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { - cardView.setAffiliatedIcon(R.drawable.ic_error_white_48dp); + if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + cardView.setRecordingFailedContent(mContext); + } else if (mDvrManager.isConflicting(recording)) { + cardView.setRecordingConflictContent(mContext); } else { - cardView.setAffiliatedIcon(0); + cardView.setContent(generateMajorContent(recording), null); } - cardView.setContent(generateMajorContent(recording), null); cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri()); } private String generateMajorContent(ScheduledRecording recording) { - if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { - return mContext.getString(R.string.dvr_recording_failed); - } int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(), recording.getStartTimeMs()); if (dateDifference <= 0) { diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java index 2cd191a7..9104ef10 100644 --- a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java @@ -20,6 +20,7 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.media.tv.TvInputManager; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v17.leanback.app.DetailsFragment; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.ArrayObjectAdapter; @@ -41,6 +42,7 @@ import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.dvr.ui.SortedArrayAdapter; +import com.android.tv.ui.DetailsActivity; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -135,7 +137,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment @Override protected boolean onLoadRecordingDetails(Bundle args) { - long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID); + long recordId = args.getLong(DetailsActivity.RECORDING_ID); mSeries = TvSingletons.getSingletons(getActivity()) .getDvrDataManager() @@ -215,6 +217,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment } /** The programs are sorted by season number and episode number. */ + @Nullable private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) { for (int i = programs.size() - 1; i >= 0; i--) { RecordedProgram program = programs.get(i); @@ -289,7 +292,8 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment } } } - if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) { + if (mRecommendRecordedProgram != null + && recordedProgram.getId() == mRecommendRecordedProgram.getId()) { updateWatchAction(); } } @@ -339,14 +343,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment new ListRow( header, new SeasonRowAdapter( - selector, - new Comparator<RecordedProgram>() { - @Override - public int compare(RecordedProgram lhs, RecordedProgram rhs) { - return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs); - } - }, - seasonNumber)); + selector, BaseProgram.EPISODE_COMPARATOR::compare, seasonNumber)); getRowsAdapter().add(position, row); return row; } diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java index 38d3d582..11680a0d 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java @@ -37,7 +37,6 @@ import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.android.tv.R; -import com.android.tv.TvFeatures; import com.android.tv.TvSingletons; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.api.Channel; @@ -90,18 +89,19 @@ class ScheduleRowPresenter extends RowPresenter { private ScheduleRowPresenter mPresenter; @ScheduleRowAction private int[] mActions; private boolean mLtr; - private LinearLayout mInfoContainer; + private final LinearLayout mInfoContainer; // 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 mSecondActionView; - private ImageView mFirstActionView; + private final RelativeLayout mSecondActionContainer; + private final RelativeLayout mFirstActionContainer; + private final View mSelectorView; + private final TextView mTimeView; + private final TextView mProgramTitleView; + private final TextView mInfoSeparatorView; + private final TextView mChannelNameView; + private final ImageView mExtraInfoIcon; + private final TextView mExtraInfoView; + private final ImageView mSecondActionView; + private final ImageView mFirstActionView; private Runnable mPendingAnimationRunnable; @@ -117,14 +117,11 @@ class ScheduleRowPresenter extends RowPresenter { @Override public void onFocusChange(View view, boolean focused) { view.post( - new Runnable() { - @Override - public void run() { - if (view.isFocused()) { - mPresenter.mLastFocusedViewId = view.getId(); - } - updateSelector(); + () -> { + if (view.isFocused()) { + mPresenter.mLastFocusedViewId = view.getId(); } + updateSelector(); }); } }; @@ -146,7 +143,8 @@ class ScheduleRowPresenter extends RowPresenter { 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); + mExtraInfoIcon = (ImageView) view.findViewById(R.id.extra_info_icon); + mExtraInfoView = (TextView) view.findViewById(R.id.extra_info); Resources res = view.getResources(); mSelectorTranslationDelta = res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) @@ -311,7 +309,7 @@ class ScheduleRowPresenter extends RowPresenter { mInfoContainer .getResources() .getColor(R.color.dvr_schedules_item_info_grey, null)); - mConflictInfoView.setTextColor( + mExtraInfoView.setTextColor( mInfoContainer .getResources() .getColor(R.color.dvr_schedules_item_info_grey, null)); @@ -327,7 +325,7 @@ class ScheduleRowPresenter extends RowPresenter { mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); mChannelNameView.setTextColor( mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); - mConflictInfoView.setTextColor( + mExtraInfoView.setTextColor( mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); } } @@ -426,39 +424,76 @@ class ScheduleRowPresenter extends RowPresenter { } } ScheduledRecording schedule = row.getSchedule(); - if (mDvrManager.isConflicting(schedule) - || (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) - && schedule != null - && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) { - String conflictInfo; - if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext()) - && schedule != null - && schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { - // TODO(b/72638385): show real error messages - // TODO(b/72638385): use a better name for ConflictInfoXXX - conflictInfo = "Failed"; - if (schedule.getFailedReason() != null) { - conflictInfo += " (Error code: " + schedule.getFailedReason() + ")"; - } + viewHolder.mExtraInfoIcon.setVisibility(View.GONE); + if (mDvrManager.isConflicting(schedule) || isFailedRecording(schedule)) { + String extraInfo; + if (isFailedRecording(schedule)) { + extraInfo = + mContext.getString(R.string.dvr_recording_failed_short) + + " " + + getErrorMessage(schedule); + viewHolder.mExtraInfoIcon.setVisibility(View.VISIBLE); } else if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) { - conflictInfo = mTunerConflictWillBePartiallyRecordedInfo; + extraInfo = mTunerConflictWillBePartiallyRecordedInfo; } else { - conflictInfo = mTunerConflictWillNotBeRecordedInfo; + extraInfo = mTunerConflictWillNotBeRecordedInfo; } - viewHolder.mConflictInfoView.setText(conflictInfo); - viewHolder.mConflictInfoView.setVisibility(View.VISIBLE); + viewHolder.mExtraInfoView.setText(extraInfo); + viewHolder.mExtraInfoView.setVisibility(View.VISIBLE); } else { - viewHolder.mConflictInfoView.setVisibility(View.GONE); + viewHolder.mExtraInfoView.setVisibility(View.GONE); } if (shouldBeGrayedOut(row)) { viewHolder.greyOutInfo(); } else { viewHolder.whiteBackInfo(); } + if (isFailedRecording(schedule)) { + viewHolder.mExtraInfoView.setTextColor( + viewHolder + .mInfoContainer + .getResources() + .getColor(R.color.dvr_recording_failed_text_color, null)); + } viewHolder.mInfoContainer.setFocusable(isInfoClickable(row)); updateActionContainer(viewHolder, viewHolder.isSelected()); } + private boolean isFailedRecording(ScheduledRecording scheduledRecording) { + return scheduledRecording != null + && scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED; + } + + private String getErrorMessage(ScheduledRecording recording) { + int reason = + recording.getFailedReason() == null + ? ScheduledRecording.FAILED_REASON_OTHER + : recording.getFailedReason(); + switch (reason) { + case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED: + return mContext.getString(R.string.dvr_recording_failed_not_started_short); + case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY: + return mContext.getString(R.string.dvr_recording_failed_resource_busy_short); + case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE: + return mContext.getString( + R.string.dvr_recording_failed_input_unavailable_short, + recording.getInputId()); + case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED: + return mContext.getString( + R.string.dvr_recording_failed_input_dvr_unsupported_short); + case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE: + return mContext.getString(R.string.dvr_recording_failed_insufficient_space_short); + case ScheduledRecording.FAILED_REASON_OTHER: // fall through + case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through + case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through + case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through + case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through + case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through + default: + return mContext.getString(R.string.dvr_recording_failed_system_failure, reason); + } + } + private int getImageForAction(@ScheduleRowAction int action) { switch (action) { case ACTION_START_RECORDING: @@ -512,7 +547,8 @@ class ScheduleRowPresenter extends RowPresenter { return schedule != null && (schedule.isNotStarted() || schedule.isInProgress() - || schedule.isFinished()); + || schedule.isFinished() + || schedule.isFailed()); } /** Called when the button in a row is clicked. */ @@ -702,23 +738,17 @@ class ScheduleRowPresenter extends RowPresenter { prepareShowActionView(viewHolder.mSecondActionContainer); prepareShowActionView(viewHolder.mFirstActionContainer); viewHolder.mPendingAnimationRunnable = - new Runnable() { - @Override - public void run() { - showActionView(viewHolder.mSecondActionContainer); - showActionView(viewHolder.mFirstActionContainer); - } + () -> { + 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); - } + () -> { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + showActionView(viewHolder.mFirstActionContainer); }; if (mLastFocusedViewId == R.id.action_second_container) { mLastFocusedViewId = R.id.info_container; @@ -727,12 +757,9 @@ class ScheduleRowPresenter extends RowPresenter { case 0: default: viewHolder.mPendingAnimationRunnable = - new Runnable() { - @Override - public void run() { - hideActionView(viewHolder.mSecondActionContainer, View.GONE); - hideActionView(viewHolder.mFirstActionContainer, View.GONE); - } + () -> { + hideActionView(viewHolder.mSecondActionContainer, View.GONE); + hideActionView(viewHolder.mFirstActionContainer, View.GONE); }; mLastFocusedViewId = R.id.info_container; SoftPreconditions.checkState( diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java index eb01aba2..28a44bf3 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java @@ -211,13 +211,7 @@ abstract class SchedulesHeaderRowPresenter extends RowPresenter { new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean focused) { - view.post( - new Runnable() { - @Override - public void run() { - updateSelector(view); - } - }); + view.post(() -> updateSelector(view)); } }; mSeriesSettingsButton.setOnFocusChangeListener(onFocusChangeListener); diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java index b8b19adc..f24ad2c0 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java @@ -74,8 +74,10 @@ public class DvrPlaybackActivity extends Activity implements OnPinCheckedListene private Intent createProgramIntent(Intent intent) { if (Intent.ACTION_VIEW.equals(intent.getAction())) { Uri uri = intent.getData(); - long recordedProgramId = ContentUris.parseId(uri); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId); + if (uri != null) { + long recordedProgramId = ContentUris.parseId(uri); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, recordedProgramId); + } } return intent; } diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java index 59c90d11..791d26bb 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java @@ -39,6 +39,7 @@ import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.View; +import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.util.TimeShiftUtils; import java.util.ArrayList; @@ -53,10 +54,13 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { private static final boolean DEBUG = false; private static final int AUDIO_ACTION_ID = 1001; + private static final long INVALID_TIME = -1; private int mPlaybackState = PlaybackState.STATE_NONE; private int mPlaybackSpeedLevel; private int mPlaybackSpeedId; + private long mProgramStartTimeMs = INVALID_TIME; + private boolean mEnableBuffering = false; private boolean mReadyToControl; private final DvrPlaybackOverlayFragment mFragment; @@ -67,6 +71,8 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { private final MultiAction mClosedCaptioningAction; private final MultiAction mMultiAudioAction; private ArrayObjectAdapter mSecondaryActionsAdapter; + private PlaybackControlsRow mPlaybackControlsRow; + @Nullable private View mPlayPauseButton; DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { super(activity, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); @@ -79,13 +85,18 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); mClosedCaptioningAction = new ClosedCaptioningAction(activity); mMultiAudioAction = new MultiAudioAction(activity); + mProgramStartTimeMs = overlayFragment.getProgramStartTimeMs(); + if (mProgramStartTimeMs != INVALID_TIME) { + mEnableBuffering = true; + } createControlsRowPresenter(); } void createControlsRow() { - PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); - setControlsRow(controlsRow); - mSecondaryActionsAdapter = (ArrayObjectAdapter) controlsRow.getSecondaryActionsAdapter(); + mPlaybackControlsRow = new PlaybackControlsRow(this); + setControlsRow(mPlaybackControlsRow); + mSecondaryActionsAdapter = + (ArrayObjectAdapter) mPlaybackControlsRow.getSecondaryActionsAdapter(); } private void createControlsRowPresenter() { @@ -118,6 +129,8 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { super.onBindRowViewHolder(vh, item); vh.setOnKeyListener(DvrPlaybackControlHelper.this); + ViewGroup controlBar = (ViewGroup) vh.view.findViewById(R.id.control_bar); + mPlayPauseButton = controlBar.getChildAt(1); } @Override @@ -265,6 +278,13 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { getHost().notifyPlaybackRowChanged(); } + /** Update the focus to play pause button. */ + public void onPlaybackResume() { + if (mPlayPauseButton != null) { + mPlayPauseButton.requestFocus(); + } + } + @Nullable Boolean hasSecondaryRow() { if (mSecondaryActionsAdapter == null) { @@ -292,6 +312,15 @@ class DvrPlaybackControlHelper extends PlaybackControlGlue { mTransportControls.pause(); } + @Override + public void updateProgress() { + if (mEnableBuffering) { + super.updateProgress(); + long bufferedTimeMs = System.currentTimeMillis() - mProgramStartTimeMs; + mPlaybackControlsRow.setBufferedPosition(bufferedTimeMs); + } + } + /** Notifies closed caption being enabled/disabled to update related UI. */ void onSubtitleTrackStateChanged(boolean enabled) { mClosedCaptioningAction.setIndex( diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java index bef036eb..81abb8e4 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java @@ -39,9 +39,6 @@ import com.android.tv.util.Utils; import com.android.tv.util.images.ImageLoader; class DvrPlaybackMediaSessionHelper { - private static final String TAG = "DvrPlaybackMediaSessionHelper"; - private static final boolean DEBUG = false; - private int mNowPlayingCardWidth; private int mNowPlayingCardHeight; private int mSpeedLevel; @@ -73,6 +70,9 @@ class DvrPlaybackMediaSessionHelper { @Override public void onPlaybackPositionChanged(long positionMs) { updateMediaSessionPlaybackState(); + if (getProgram().isPartial()) { + overlayFragment.updateProgress(); + } if (mDvrPlayer.isPlaybackPrepared()) { mDvrWatchedPositionManager.setWatchedPosition( mDvrPlayer.getProgram().getId(), positionMs); @@ -94,6 +94,11 @@ class DvrPlaybackMediaSessionHelper { mActivity.startActivity(intent); } } + + @Override + public void onPlaybackResume() { + overlayFragment.onPlaybackResume(); + } }); initializeMediaSession(mediaSessionTag); } diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java index d3374cfa..1059e852 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java @@ -25,7 +25,6 @@ import android.media.session.PlaybackState; import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; -import android.media.tv.TvView; import android.os.Bundle; import android.support.v17.leanback.app.PlaybackFragment; import android.support.v17.leanback.app.PlaybackFragmentGlueHost; @@ -52,7 +51,7 @@ import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.SortedArrayAdapter; import com.android.tv.dvr.ui.browse.DvrListRowPresenter; import com.android.tv.dvr.ui.browse.RecordingCardView; -import com.android.tv.parental.ContentRatingsManager; +import com.android.tv.ui.AppLayerTvView; import com.android.tv.util.TvSettings; import com.android.tv.util.TvTrackInfoUtils; import com.android.tv.util.Utils; @@ -66,6 +65,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; + private static final long INVALID_TIME = -1; // mProgram is only used to store program from intent. Don't use it elsewhere. private RecordedProgram mProgram; @@ -76,8 +76,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter; private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; private DvrDataManager mDvrDataManager; - private ContentRatingsManager mContentRatingsManager; - private TvView mTvView; + private AppLayerTvView mTvView; private View mBlockScreenView; private ListRow mRelatedRecordingsRow; private int mVerticalPaddingBase; @@ -117,10 +116,6 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { .getDimensionPixelOffset( R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); mDvrDataManager = TvSingletons.getSingletons(getActivity()).getDvrDataManager(); - mContentRatingsManager = - TvSingletons.getSingletons(getContext()) - .getTvInputManagerHelper() - .getContentRatingsManager(); if (!mDvrDataManager.isRecordedProgramLoadFinished()) { mDvrDataManager.addRecordedProgramLoadFinishedListener( new DvrDataManager.OnRecordedProgramLoadFinishedListener() { @@ -157,9 +152,9 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); + mTvView = getActivity().findViewById(R.id.dvr_tv_view); mBlockScreenView = getActivity().findViewById(R.id.block_screen); - mDvrPlayer = new DvrPlayer(mTvView); + mDvrPlayer = new DvrPlayer(mTvView, getActivity()); mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); @@ -279,6 +274,7 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { mPlaybackControlHelper.unregisterCallback(); mMediaSessionHelper.release(); mRelatedRecordingCardPresenter.unbindAllViewHolders(); + mDvrPlayer.release(); super.onDestroy(); } @@ -503,6 +499,20 @@ public class DvrPlaybackOverlayFragment extends PlaybackFragment { } } + public void onPlaybackResume() { + mPlaybackControlHelper.onPlaybackResume(); + } + + public long getProgramStartTimeMs() { + return (mProgram != null && mProgram.isPartial()) + ? mProgram.getStartTimeUtcMillis() + : INVALID_TIME; + } + + public void updateProgress() { + mPlaybackControlHelper.updateProgress(); + } + private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> { RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java index 85bb31b2..d14646b8 100644 --- a/src/com/android/tv/dvr/ui/playback/DvrPlayer.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java @@ -16,6 +16,7 @@ package com.android.tv.dvr.ui.playback; +import android.content.Context; import android.media.PlaybackParams; import android.media.session.PlaybackState; import android.media.tv.TvContentRating; @@ -24,12 +25,16 @@ import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.text.TextUtils; import android.util.Log; +import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat; +import com.android.tv.dvr.DvrTvView; import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.ui.AppLayerTvView; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -class DvrPlayer { +/** Player for recorded programs. */ +public class DvrPlayer { private static final String TAG = "DvrPlayer"; private static final boolean DEBUG = false; @@ -40,10 +45,11 @@ class DvrPlayer { private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2); private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826 + private static final long FORWARD_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(5); private RecordedProgram mProgram; private long mInitialSeekPositionMs; - private final TvView mTvView; + private final DvrTvView mTvView; private DvrPlayerCallback mCallback; private OnAspectRatioChangedListener mOnAspectRatioChangedListener; private OnContentBlockedListener mOnContentBlockedListener; @@ -63,6 +69,7 @@ class DvrPlayer { private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; private boolean mTimeShiftPlayAvailable; + /** Callback of DVR player. */ public static class DvrPlayerCallback { /** * Called when the playback position is changed. The normal updating frequency is around 1 @@ -74,8 +81,11 @@ class DvrPlayer { public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {} /** Called when the playback toward the end. */ public void onPlaybackEnded() {} + /** Called when the playback is resumed to live position. */ + public void onPlaybackResume() {} } + /** Listener for aspect ratio changed events. */ public interface OnAspectRatioChangedListener { /** * Called when the Video's aspect ratio is changed. @@ -86,27 +96,32 @@ class DvrPlayer { void onAspectRatioChanged(float videoAspectRatio); } + /** Listener for content blocked events. */ public interface OnContentBlockedListener { /** Called when the Video's aspect ratio is changed. */ void onContentBlocked(TvContentRating rating); } + /** Listener for tracks availability changed events */ public interface OnTracksAvailabilityChangedListener { /** Called when the Video's subtitle or audio tracks are changed. */ void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); } + /** Listener for track selected events */ public interface OnTrackSelectedListener { /** Called when certain subtitle or audio track is selected. */ void onTrackSelected(String selectedTrackId); } - public DvrPlayer(TvView tvView) { - mTvView = tvView; + /** Constructor of DvrPlayer. */ + public DvrPlayer(AppLayerTvView tvView, Context context) { + mTvView = new DvrTvView(context, tvView, this); mTvView.setCaptionEnabled(true); mPlaybackParams.setSpeed(1.0f); setTvViewCallbacks(); setCallback(null); + mTvView.init(); } /** @@ -333,7 +348,8 @@ class DvrPlayer { /** Returns the audio tracks of the current playback. */ public ArrayList<TvTrackInfo> getAudioTracks() { - return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); + List<TvTrackInfo> tracks = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO); + return tracks == null ? new ArrayList<>() : new ArrayList<>(tracks); } /** Returns the ID of the selected track of the given type. */ @@ -352,6 +368,10 @@ class DvrPlayer { && mPlaybackState != PlaybackState.STATE_CONNECTING; } + public void release() { + mTvView.release(); + } + /** * Selects the given track. * @@ -426,9 +446,16 @@ class DvrPlayer { resumeToWatchedPositionIfNeeded(); } timeMs -= mStartPositionMs; - if (mPlaybackState == PlaybackState.STATE_REWINDING - && timeMs <= REWIND_POSITION_MARGIN_MS) { + long bufferedTimeMs = + System.currentTimeMillis() + - mProgram.getStartTimeUtcMillis() + - FORWARD_POSITION_MARGIN_MS; + if ((mPlaybackState == PlaybackState.STATE_REWINDING + && timeMs <= REWIND_POSITION_MARGIN_MS) + || (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING + && timeMs > bufferedTimeMs)) { play(); + mCallback.onPlaybackResume(); } else { mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0); mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs); @@ -440,7 +467,7 @@ class DvrPlayer { } }); mTvView.setCallback( - new TvView.TvInputCallback() { + new TvInputCallbackCompat() { @Override public void onTimeShiftStatusChanged(String inputId, int status) { if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status); |