diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2017-11-29 00:20:48 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2017-11-29 00:20:48 +0000 |
commit | 7b32e8a67561a7989dbfc0f1d3480dca162c6769 (patch) | |
tree | d31e2adc1f9cce4f27ca07d30bee921032e33a3c /src/com/android/tv/dvr | |
parent | bc7f430decab0bc34a533811efe457d4615f28aa (diff) | |
parent | bb2e798ef4d546dd54cd9e95796403062b860c15 (diff) | |
download | TV-oreo-m3-release.tar.gz |
Snap for 4448085 from bb2e798ef4d546dd54cd9e95796403062b860c15 to oc-m3-releaseandroid-8.1.0_r9android-8.1.0_r7android-8.1.0_r22android-8.1.0_r21android-8.1.0_r18android-8.1.0_r17android-8.1.0_r14android-8.1.0_r13oreo-m5-releaseoreo-m3-release
Change-Id: I0aff16fbf430ba5e4dbba83b8e06e4a23b38060a
Diffstat (limited to 'src/com/android/tv/dvr')
100 files changed, 4855 insertions, 2933 deletions
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java index 89661df3..a8637449 100644 --- a/src/com/android/tv/dvr/BaseDvrDataManager.java +++ b/src/com/android/tv/dvr/BaseDvrDataManager.java @@ -26,7 +26,10 @@ import android.util.Log; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Clock; import java.util.ArrayList; @@ -318,5 +321,41 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager { } @Override + public void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds) { + List<SeriesRecording> toRemove = new ArrayList<>(); + for (long rId : seriesRecordingIds) { + SeriesRecording seriesRecording = getSeriesRecording(rId); + if (seriesRecording != null && isEmptySeriesRecording(seriesRecording)) { + toRemove.add(seriesRecording); + } + } + removeSeriesRecording(SeriesRecording.toArray(toRemove)); + } + + /** + * Returns {@code true}, if the series recording is empty and can be removed. If a series + * recording is in NORMAL state or has recordings or schedules, it is not empty and cannot be + * removed. + */ + protected final boolean isEmptySeriesRecording(@NonNull SeriesRecording seriesRecording) { + if (!seriesRecording.isStopped()) { + return false; + } + long seriesRecordingId = seriesRecording.getId(); + for (ScheduledRecording r : getAvailableScheduledRecordings()) { + if (r.getSeriesRecordingId() == seriesRecordingId) { + return false; + } + } + String seriesId = seriesRecording.getSeriesId(); + for (RecordedProgram r : getRecordedPrograms()) { + if (seriesId.equals(r.getSeriesId())) { + return false; + } + } + return true; + } + + @Override public void forgetStorage(String inputId) { } } diff --git a/src/com/android/tv/dvr/DvrDataManager.java b/src/com/android/tv/dvr/DvrDataManager.java index 06613667..6d400b82 100644 --- a/src/com/android/tv/dvr/DvrDataManager.java +++ b/src/com/android/tv/dvr/DvrDataManager.java @@ -21,7 +21,10 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Range; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import java.util.Collection; import java.util.List; @@ -211,6 +214,13 @@ public interface DvrDataManager { Collection<Long> getDisallowedProgramIds(); /** + * Checks each of the give series recordings to see if it's empty, i.e., it doesn't contains + * any available schedules or recorded programs, and it's status is + * {@link SeriesRecording#STATE_SERIES_STOPPED}; and removes those empty series recordings. + */ + void checkAndRemoveEmptySeriesRecording(long... seriesRecordingIds); + + /** * Listens for the DVR schedules loading finished. */ interface OnDvrScheduleLoadFinishedListener { diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java index 46682a48..6094ca72 100644 --- a/src/com/android/tv/dvr/DvrDataManagerImpl.java +++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java @@ -42,7 +42,11 @@ import android.util.Range; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrStorageStatusManager.OnStorageMountChangedListener; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.IdGenerator; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask; @@ -51,12 +55,14 @@ import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask; import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask; +import com.android.tv.dvr.provider.DvrDbSync; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask; import com.android.tv.util.Clock; import com.android.tv.util.Filter; import com.android.tv.util.TvInputManagerHelper; -import com.android.tv.util.TvProviderUriMatcher; +import com.android.tv.util.TvUriMatcher; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -267,11 +273,14 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { removeScheduledRecording(ScheduledRecording.toArray(toDelete)); } IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId); + if (mRecordedProgramLoadFinished) { + validateSeriesRecordings(); + } mDvrLoadFinished = true; notifyDvrScheduleLoadFinished(); - mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); - mDbSync.start(); if (isInitialized()) { + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); + mDbSync.start(); SeriesRecordingScheduler.getInstance(mContext).start(); } } @@ -306,8 +315,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (uri == null) { uri = RecordedPrograms.CONTENT_URI; } - int match = TvProviderUriMatcher.match(uri); - if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM) { + if (recordedPrograms == null) { + recordedPrograms = Collections.emptyList(); + } + int match = TvUriMatcher.match(uri); + if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM) { if (!mRecordedProgramLoadFinished) { for (RecordedProgram recorded : recordedPrograms) { if (isInputAvailable(recorded.getInputId())) { @@ -318,7 +330,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } mRecordedProgramLoadFinished = true; notifyRecordedProgramLoadFinished(); - } else if (recordedPrograms == null || recordedPrograms.isEmpty()) { + if (isInitialized()) { + mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this); + mDbSync.start(); + } + } else if (recordedPrograms.isEmpty()) { List<RecordedProgram> oldRecordedPrograms = new ArrayList<>(mRecordedPrograms.values()); mRecordedPrograms.clear(); @@ -355,19 +371,24 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (isInitialized()) { + validateSeriesRecordings(); SeriesRecordingScheduler.getInstance(mContext).start(); } - } else if (match == TvProviderUriMatcher.MATCH_RECORDED_PROGRAM_ID) { + } else if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM_ID) { if (!mRecordedProgramLoadFinished) { return; } long id = ContentUris.parseId(uri); if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms); - if (recordedPrograms == null || recordedPrograms.isEmpty()) { + if (recordedPrograms.isEmpty()) { mRecordedProgramsForRemovedInput.remove(id); RecordedProgram old = mRecordedPrograms.remove(id); if (old != null) { notifyRecordedProgramsRemoved(old); + SeriesRecording r = mSeriesId2SeriesRecordings.get(old.getSeriesId()); + if (r != null && isEmptySeriesRecording(r)) { + removeSeriesRecording(r); + } } } else { RecordedProgram recordedProgram = recordedPrograms.get(0); @@ -592,10 +613,16 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>(); + Set<Long> seriesRecordingIdsToCheck = new HashSet<>(); for (ScheduledRecording r : schedules) { mScheduledRecordings.remove(r.getId()); - getDeletedScheduleMap().remove(r.getId()); + getDeletedScheduleMap().remove(r.getProgramId()); mProgramId2ScheduledRecordings.remove(r.getProgramId()); + if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET + && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); + } boolean isScheduleForRemovedInput = mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null; // If it belongs to the series recording and it's not started yet, just mark delete @@ -614,8 +641,19 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } if (mDvrLoadFinished) { + if (mRecordedProgramLoadFinished) { + checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); + } notifyScheduledRecordingRemoved(schedules); } + Iterator<ScheduledRecording> iterator = schedulesNotToDelete.iterator(); + while (iterator.hasNext()) { + ScheduledRecording r = iterator.next(); + if (!mSeriesRecordings.containsKey(r.getSeriesRecordingId())) { + iterator.remove(); + schedulesToDelete.add(r); + } + } if (!schedulesToDelete.isEmpty()) { new AsyncDeleteScheduleTask(mContext).executeOnDbThread( ScheduledRecording.toArray(schedulesToDelete)); @@ -669,6 +707,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) { List<ScheduledRecording> toUpdate = new ArrayList<>(); + Set<Long> seriesRecordingIdsToCheck = new HashSet<>(); for (ScheduledRecording r : schedules) { if (!SoftPreconditions.checkState(mScheduledRecordings.containsKey(r.getId()), TAG, "Recording not found for: " + r)) { @@ -691,6 +730,13 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (programId != ScheduledRecording.ID_NOT_SET) { mProgramId2ScheduledRecordings.put(programId, r); } + if (r.getState() == ScheduledRecording.STATE_RECORDING_FAILED + && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) { + // If the scheduled recording is failed, it may cause the automatically generated + // series recording for this schedule becomes invalid (with no future schedules and + // past recordings.) We should check and remove these series recordings. + seriesRecordingIdsToCheck.add(r.getSeriesRecordingId()); + } } if (toUpdate.isEmpty()) { return; @@ -702,12 +748,17 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (updateDb) { new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray); } + checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck); removeDeletedSchedules(schedules); } @Override public void updateSeriesRecording(final SeriesRecording... seriesRecordings) { for (SeriesRecording r : seriesRecordings) { + if (!SoftPreconditions.checkArgument(mSeriesRecordings.containsKey(r.getId()), TAG, + "Non Existing Series ID: " + r)) { + continue; + } SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r); SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r); SoftPreconditions.checkArgument(old1.equals(old2), TAG, "Series ID cannot be" @@ -769,14 +820,6 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { return r.getInputId().equals(inputId); } }); - List<SeriesRecording> movedSeriesRecordings = - moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, - new Filter<SeriesRecording>() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); List<RecordedProgram> movedRecordedPrograms = moveElements(mRecordedProgramsForRemovedInput, mRecordedPrograms, new Filter<RecordedProgram>() { @@ -785,6 +828,21 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { return r.getInputId().equals(inputId); } }); + List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); + List<SeriesRecording> movedSeriesRecordings = + moveElements(mSeriesRecordingsForRemovedInput, mSeriesRecordings, + new Filter<SeriesRecording>() { + @Override + public boolean filter(SeriesRecording r) { + if (r.getInputId().equals(inputId)) { + if (!isEmptySeriesRecording(r)) { + return true; + } + removedSeriesRecordings.add(r); + } + return false; + } + }); if (!movedSchedules.isEmpty()) { for (ScheduledRecording schedule : movedSchedules) { mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule); @@ -795,6 +853,11 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording); } } + for (SeriesRecording r : removedSeriesRecordings) { + mSeriesRecordingsForRemovedInput.remove(r.getId()); + } + new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread( + SeriesRecording.toArray(removedSeriesRecordings)); // Notify after all the data are moved. if (!movedSchedules.isEmpty()) { notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules)); @@ -811,20 +874,20 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { if (DEBUG) Log.d(TAG, "hideInput " + inputId); List<ScheduledRecording> movedSchedules = moveElements(mScheduledRecordings, mScheduledRecordingsForRemovedInput, - new Filter<ScheduledRecording>() { - @Override - public boolean filter(ScheduledRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter<ScheduledRecording>() { + @Override + public boolean filter(ScheduledRecording r) { + return r.getInputId().equals(inputId); + } + }); List<SeriesRecording> movedSeriesRecordings = moveElements(mSeriesRecordings, mSeriesRecordingsForRemovedInput, - new Filter<SeriesRecording>() { - @Override - public boolean filter(SeriesRecording r) { - return r.getInputId().equals(inputId); - } - }); + new Filter<SeriesRecording>() { + @Override + public boolean filter(SeriesRecording r) { + return r.getInputId().equals(inputId); + } + }); List<RecordedProgram> movedRecordedPrograms = moveElements(mRecordedPrograms, mRecordedProgramsForRemovedInput, new Filter<RecordedProgram>() { @@ -855,6 +918,15 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { } } + private void checkAndRemoveEmptySeriesRecording(Set<Long> seriesRecordingIds) { + int i = 0; + long[] rIds = new long[seriesRecordingIds.size()]; + for (long rId : seriesRecordingIds) { + rIds[i++] = rId; + } + checkAndRemoveEmptySeriesRecording(rIds); + } + @Override public void forgetStorage(String inputId) { List<ScheduledRecording> schedulesToDelete = new ArrayList<>(); @@ -901,6 +973,25 @@ public class DvrDataManagerImpl extends BaseDvrDataManager { }.executeOnDbThread(); } + private void validateSeriesRecordings() { + Iterator<SeriesRecording> iter = mSeriesRecordings.values().iterator(); + List<SeriesRecording> removedSeriesRecordings = new ArrayList<>(); + while (iter.hasNext()) { + SeriesRecording r = iter.next(); + if (isEmptySeriesRecording(r)) { + iter.remove(); + removedSeriesRecordings.add(r); + } + } + if (!removedSeriesRecordings.isEmpty()) { + SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings); + new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed); + if (mDvrLoadFinished) { + notifySeriesRecordingRemoved(removed); + } + } + } + private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask { private final Uri mUri; diff --git a/src/com/android/tv/dvr/DvrManager.java b/src/com/android/tv/dvr/DvrManager.java index 5fa6f90f..d222003d 100644 --- a/src/com/android/tv/dvr/DvrManager.java +++ b/src/com/android/tv/dvr/DvrManager.java @@ -46,7 +46,9 @@ import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener; -import com.android.tv.dvr.SeriesRecording.SeriesState; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.AsyncDbTask; import com.android.tv.util.Utils; @@ -142,7 +144,7 @@ public class DvrManager { } private void createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram) { - if (recordedProgram.getSeriesId() != null) { + if (recordedProgram.isEpisodic()) { SeriesRecording seriesRecording = mDataManager.getSeriesRecording(recordedProgram.getSeriesId()); if (seriesRecording == null) { @@ -234,7 +236,7 @@ public class DvrManager { * Adds a new series recording and schedules for the programs with the initial state. */ public SeriesRecording addSeriesRecording(Program selectedProgram, - List<Program> programsToSchedule, @SeriesState int initialState) { + List<Program> programsToSchedule, @SeriesRecording.SeriesState int initialState) { Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: " + programsToSchedule); if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { @@ -308,8 +310,7 @@ public class DvrManager { ScheduledRecording scheduleWithSameProgram = mDataManager.getScheduledRecordingForProgramId(program.getId()); if (scheduleWithSameProgram != null) { - if (scheduleWithSameProgram.getState() - == ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + if (scheduleWithSameProgram.isNotStarted()) { ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram) .setSeriesRecordingId(series.getId()) .build(); @@ -337,10 +338,10 @@ public class DvrManager { */ public void updateSeriesRecording(SeriesRecording series) { if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) { - SeriesRecordingScheduler scheduler = SeriesRecordingScheduler.getInstance(mAppContext); - scheduler.pauseUpdate(); SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId()); if (previousSeries != null) { + // If the channel option of series changed, remove the existing schedules. The new + // schedules will be added by SeriesRecordingScheduler or by SeriesSettingsFragment. if (previousSeries.getChannelOption() != series.getChannelOption() || (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE && previousSeries.getChannelId() != series.getChannelId())) { @@ -350,6 +351,18 @@ public class DvrManager { for (ScheduledRecording schedule : schedules) { if (schedule.isNotStarted()) { schedulesToRemove.add(schedule); + } else if (schedule.isInProgress() + && series.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE + && schedule.getChannelId() != series.getChannelId()) { + stopRecording(schedule); + } + } + List<ScheduledRecording> deletedSchedules = + new ArrayList<>(mDataManager.getDeletedSchedules()); + for (ScheduledRecording deletedSchedule : deletedSchedules) { + if (deletedSchedule.getSeriesRecordingId() == series.getId() + && deletedSchedule.getEndTimeMs() > System.currentTimeMillis()) { + schedulesToRemove.add(deletedSchedule); } } mDataManager.removeScheduledRecording(true, @@ -363,7 +376,7 @@ public class DvrManager { List<ScheduledRecording> schedulesToUpdate = new ArrayList<>(); for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(series.getId())) { - if (schedule.isNotStarted()) { + if (schedule.isNotStarted() || schedule.isInProgress()) { schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule) .setPriority(priority).build()); } @@ -373,7 +386,6 @@ public class DvrManager { ScheduledRecording.toArray(schedulesToUpdate)); } } - scheduler.resumeUpdate(); } } @@ -400,33 +412,6 @@ public class DvrManager { } /** - * Returns true, if the series recording can be removed. If a series recording is NORMAL state - * or has recordings or schedules, it cannot be removed. - */ - public boolean canRemoveSeriesRecording(long seriesRecordingId) { - SeriesRecording seriesRecording = mDataManager.getSeriesRecording(seriesRecordingId); - if (seriesRecording == null) { - return false; - } - if (!seriesRecording.isStopped()) { - return false; - } - for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) { - if (r.getSeriesRecordingId() == seriesRecordingId) { - return false; - } - } - String seriesId = seriesRecording.getSeriesId(); - SoftPreconditions.checkNotNull(seriesId); - for (RecordedProgram r : mDataManager.getRecordedPrograms()) { - if (seriesId.equals(r.getSeriesId())) { - return false; - } - } - return true; - } - - /** * Stops the currently recorded program */ public void stopRecording(final ScheduledRecording recording) { @@ -509,13 +494,16 @@ public class DvrManager { if (!SoftPreconditions.checkState(mDataManager.isInitialized())) { return; } - new AsyncDbTask<Void, Void, Void>() { + new AsyncDbTask<Void, Void, Integer>() { @Override - protected Void doInBackground(Void... params) { + protected Integer doInBackground(Void... params) { ContentResolver resolver = mAppContext.getContentResolver(); - int deletedCounts = resolver.delete(recordedProgram.getUri(), null, null); + return resolver.delete(recordedProgram.getUri(), null, null); + } + + @Override + protected void onPostExecute(Integer deletedCounts) { if (deletedCounts > 0) { - // TODO: executeOnExecutor should be called on the main thread. new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { @@ -524,7 +512,6 @@ public class DvrManager { } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - return null; } }.executeOnDbThread(); } @@ -539,13 +526,22 @@ public class DvrManager { dbOperations.add(ContentProviderOperation.newDelete(r.getUri()).build()); } } - new AsyncDbTask<Void, Void, Void>() { + new AsyncDbTask<Void, Void, Boolean>() { @Override - protected Void doInBackground(Void... params) { + protected Boolean doInBackground(Void... params) { ContentResolver resolver = mAppContext.getContentResolver(); try { resolver.applyBatch(TvContract.AUTHORITY, dbOperations); - // TODO: executeOnExecutor should be called on the main thread. + } catch (RemoteException | OperationApplicationException e) { + Log.w(TAG, "Remove recorded programs from DB failed.", e); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean success) { + if (success) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { @@ -555,10 +551,7 @@ public class DvrManager { return null; } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } catch (RemoteException | OperationApplicationException e) { - Log.w(TAG, "Remove reocrded programs from DB failed.", e); } - return null; } }.executeOnDbThread(); } @@ -657,6 +650,9 @@ public class DvrManager { if (!mDataManager.isDvrScheduleLoadFinished() || channel == null) { return false; } + if (channel.isRecordingProhibited()) { + return false; + } TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId()); if (info == null) { Log.w(TAG, "Could not find TvInputInfo for " + channel); @@ -680,7 +676,12 @@ public class DvrManager { if (!mDataManager.isInitialized()) { return false; } - TvInputInfo info = Utils.getTvInputInfoForProgram(mAppContext, program); + Channel channel = TvApplication.getSingletons(mAppContext).getChannelDataManager() + .getChannel(program.getChannelId()); + if (channel == null || channel.isRecordingProhibited()) { + return false; + } + TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId()); if (info == null) { Log.w(TAG, "Could not find TvInputInfo for " + program); return false; @@ -733,6 +734,17 @@ public class DvrManager { return mDataManager.getSeriesRecording(program.getSeriesId()); } + /** + * Returns if there are valid items. Valid item contains {@link RecordedProgram}, + * available {@link ScheduledRecording} and {@link SeriesRecording}. + */ + public boolean hasValidItems() { + return !(mDataManager.getRecordedPrograms().isEmpty() + && mDataManager.getStartedRecordings().isEmpty() + && mDataManager.getNonStartedScheduledRecordings().isEmpty() + && mDataManager.getSeriesRecordings().isEmpty()); + } + @WorkerThread @VisibleForTesting // Should be public to use mock DvrManager object. @@ -840,9 +852,10 @@ public class DvrManager { } /** - * Listener internally used inside dvr package. + * Listener to stop recording request. Should only be internally used inside dvr and its + * sub-package. */ - interface Listener { + public interface Listener { void onStopRecordingRequested(ScheduledRecording scheduledRecording); } } diff --git a/src/com/android/tv/dvr/DvrRecordingService.java b/src/com/android/tv/dvr/DvrRecordingService.java deleted file mode 100644 index 8c40aaa8..00000000 --- a/src/com/android/tv/dvr/DvrRecordingService.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr; - -import android.app.AlarmManager; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.HandlerThread; -import android.os.IBinder; -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.util.Log; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.util.Clock; -import com.android.tv.util.RecurringRunner; - -/** - * DVR Scheduler service. - * - * <p> This service is responsible for: - * <ul> - * <li>Send record commands to TV inputs</li> - * <li>Wake up at proper timing for recording</li> - * <li>Deconflict schedule, handling overlapping times etc.</li> - * <li> - * - * </ul> - * - * <p>The service does not stop it self. - */ -public class DvrRecordingService extends Service { - private static final String TAG = "DvrRecordingService"; - private static final boolean DEBUG = false; - public static final String HANDLER_THREAD_NAME = "DvrRecordingService-handler"; - - public static void startService(Context context) { - Intent dvrSchedulerIntent = new Intent(context, DvrRecordingService.class); - context.startService(dvrSchedulerIntent); - } - - private final Clock mClock = Clock.SYSTEM; - private RecurringRunner mReaperRunner; - - private Scheduler mScheduler; - private HandlerThread mHandlerThread; - - @Override - public void onCreate() { - TvApplication.setCurrentRunningProcess(this, true); - if (DEBUG) Log.d(TAG, "onCreate"); - super.onCreate(); - SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); - ApplicationSingletons singletons = TvApplication.getSingletons(this); - WritableDvrDataManager dataManager = (WritableDvrDataManager) singletons.getDvrDataManager(); - - AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - // mScheduler may have been set for testing. - if (mScheduler == null) { - mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME); - mHandlerThread.start(); - mScheduler = new Scheduler(mHandlerThread.getLooper(), singletons.getDvrManager(), - singletons.getInputSessionManager(), dataManager, - singletons.getChannelDataManager(), singletons.getTvInputManagerHelper(), this, - mClock, alarmManager); - mScheduler.start(); - } - mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1), - new ScheduledProgramReaper(dataManager, mClock), null); - mReaperRunner.start(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (DEBUG) Log.d(TAG, "onStartCommand (" + intent + "," + flags + "," + startId + ")"); - mScheduler.update(); - return START_STICKY; - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - mReaperRunner.stop(); - mScheduler.stop(); - mScheduler = null; - if (mHandlerThread != null) { - mHandlerThread.quitSafely(); - mHandlerThread = null; - } - super.onDestroy(); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @VisibleForTesting - void setScheduler(Scheduler scheduler) { - Log.i(TAG, "Setting scheduler for tests to " + scheduler); - mScheduler = scheduler; - } -} diff --git a/src/com/android/tv/dvr/DvrScheduleManager.java b/src/com/android/tv/dvr/DvrScheduleManager.java index a5851a75..b72117aa 100644 --- a/src/com/android/tv/dvr/DvrScheduleManager.java +++ b/src/com/android/tv/dvr/DvrScheduleManager.java @@ -24,7 +24,6 @@ import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.util.ArraySet; -import android.util.LongSparseArray; import android.util.Range; import com.android.tv.ApplicationSingletons; @@ -35,7 +34,10 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.recorder.InputTaskScheduler; import com.android.tv.util.CompositeComparator; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -88,9 +90,8 @@ public class DvrScheduleManager { private final Map<String, List<ScheduledRecording>> mInputScheduleMap = new HashMap<>(); // The inner map is a hash map from scheduled recording to its conflicting status, i.e., // the boolean value true denotes the schedule is just partially conflicting, which means - // although there's conflictit, it might still be recorded partially. - private final Map<String, Map<ScheduledRecording, Boolean>> mInputConflictInfoMap = - new HashMap<>(); + // although there's conflict, it might still be recorded partially. + private final Map<String, Map<Long, ConflictInfo>> mInputConflictInfoMap = new HashMap<>(); private boolean mInitialized; @@ -171,10 +172,9 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } } - Map<ScheduledRecording, Boolean> conflictInfo = - mInputConflictInfoMap.get(inputId); + Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId); if (conflictInfo != null) { - conflictInfo.remove(schedule); + conflictInfo.remove(schedule.getId()); if (conflictInfo.isEmpty()) { mInputConflictInfoMap.remove(inputId); } @@ -221,21 +221,11 @@ public class DvrScheduleManager { mInputScheduleMap.remove(inputId); } // Update conflict list as well - Map<ScheduledRecording, Boolean> conflictInfo = - mInputConflictInfoMap.get(inputId); + Map<Long, ConflictInfo> conflictInfo = mInputConflictInfoMap.get(inputId); if (conflictInfo != null) { - // Compare ID because ScheduledRecording.equals() doesn't work if the state - // is changed. - ScheduledRecording oldSchedule = null; - for (ScheduledRecording s : conflictInfo.keySet()) { - if (s.getId() == schedule.getId()) { - oldSchedule = s; - break; - } - } - if (oldSchedule != null) { - conflictInfo.put(schedule, conflictInfo.get(oldSchedule)); - conflictInfo.remove(oldSchedule); + ConflictInfo oldConflictInfo = conflictInfo.get(schedule.getId()); + if (oldConflictInfo != null) { + oldConflictInfo.schedule = schedule; } } } @@ -317,24 +307,25 @@ public class DvrScheduleManager { List<ScheduledRecording> addedConflicts = new ArrayList<>(); List<ScheduledRecording> removedConflicts = new ArrayList<>(); for (String inputId : mInputScheduleMap.keySet()) { - Map<ScheduledRecording, Boolean> oldConflictsInfo = mInputConflictInfoMap.get(inputId); + Map<Long, ConflictInfo> oldConflictInfo = mInputConflictInfoMap.get(inputId); Map<Long, ScheduledRecording> oldConflictMap = new HashMap<>(); - if (oldConflictsInfo != null) { - for (ScheduledRecording r : oldConflictsInfo.keySet()) { - oldConflictMap.put(r.getId(), r); + if (oldConflictInfo != null) { + for (ConflictInfo conflictInfo : oldConflictInfo.values()) { + oldConflictMap.put(conflictInfo.schedule.getId(), conflictInfo.schedule); } } - Map<ScheduledRecording, Boolean> conflictInfo = getConflictingSchedulesInfo(inputId); - if (conflictInfo.isEmpty()) { + List<ConflictInfo> conflicts = getConflictingSchedulesInfo(inputId); + if (conflicts.isEmpty()) { mInputConflictInfoMap.remove(inputId); } else { - mInputConflictInfoMap.put(inputId, conflictInfo); - List<ScheduledRecording> conflicts = new ArrayList<>(conflictInfo.keySet()); - for (ScheduledRecording r : conflicts) { - if (oldConflictMap.remove(r.getId()) == null) { - addedConflicts.add(r); + Map<Long, ConflictInfo> conflictInfos = new HashMap<>(); + for (ConflictInfo conflictInfo : conflicts) { + conflictInfos.put(conflictInfo.schedule.getId(), conflictInfo); + if (oldConflictMap.remove(conflictInfo.schedule.getId()) == null) { + addedConflicts.add(conflictInfo.schedule); } } + mInputConflictInfoMap.put(inputId, conflictInfos); } removedConflicts.addAll(oldConflictMap.values()); } @@ -565,8 +556,7 @@ public class DvrScheduleManager { } /** - * Returns list of all conflicting scheduled recordings with schedules belonging to {@code - * seriesRecording} + * Returns list of all conflicting scheduled recordings for the given {@code seriesRecording} * recording. * <p> * Any empty list means there is no conflicts. @@ -581,9 +571,18 @@ public class DvrScheduleManager { if (input == null || !input.canRecord() || input.getTunerCount() <= 0) { return Collections.emptyList(); } - List<ScheduledRecording> schedulesForSeries = mDataManager.getScheduledRecordings( + List<ScheduledRecording> scheduledRecordingForSeries = mDataManager.getScheduledRecordings( seriesRecording.getId()); - return getConflictingSchedules(input, schedulesForSeries); + List<ScheduledRecording> availableScheduledRecordingForSeries = new ArrayList<>(); + for (ScheduledRecording scheduledRecording : scheduledRecordingForSeries) { + if (scheduledRecording.isNotStarted() || scheduledRecording.isInProgress()) { + availableScheduledRecordingForSeries.add(scheduledRecording); + } + } + if (availableScheduledRecordingForSeries.isEmpty()) { + return Collections.emptyList(); + } + return getConflictingSchedules(input, availableScheduledRecordingForSeries); } /** @@ -617,16 +616,16 @@ public class DvrScheduleManager { * the given input. */ @NonNull - private Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo(String inputId) { + private List<ConflictInfo> getConflictingSchedulesInfo(String inputId) { SoftPreconditions.checkState(mInitialized, TAG, "Not initialized yet"); TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, inputId); SoftPreconditions.checkState(input != null, TAG, "Can't find input for : " + inputId); if (!mInitialized || input == null) { - return Collections.emptyMap(); + return Collections.emptyList(); } List<ScheduledRecording> schedules = mInputScheduleMap.get(input.getId()); if (schedules == null || schedules.isEmpty()) { - return Collections.emptyMap(); + return Collections.emptyList(); } return getConflictingSchedulesInfo(schedules, input.getTunerCount()); } @@ -645,8 +644,8 @@ public class DvrScheduleManager { if (!mInitialized || input == null) { return false; } - Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); - return conflicts != null && conflicts.containsKey(schedule); + Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId()); + return conflicts != null && conflicts.containsKey(schedule.getId()); } /** @@ -664,8 +663,12 @@ public class DvrScheduleManager { if (!mInitialized || input == null) { return false; } - Map<ScheduledRecording, Boolean> conflicts = mInputConflictInfoMap.get(input.getId()); - return conflicts != null && conflicts.getOrDefault(schedule, false); + Map<Long, ConflictInfo> conflicts = mInputConflictInfoMap.get(input.getId()); + if (conflicts != null) { + ConflictInfo conflictInfo = conflicts.get(schedule.getId()); + return conflictInfo != null && conflictInfo.partialConflict; + } + return false; } /** @@ -813,15 +816,17 @@ public class DvrScheduleManager { @VisibleForTesting static List<ScheduledRecording> getConflictingSchedules( List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { - List<ScheduledRecording> result = new ArrayList<>( - getConflictingSchedulesInfo(schedules, tunerCount, periods).keySet()); - Collections.sort(result, RESULT_COMPARATOR); + List<ScheduledRecording> result = new ArrayList<>(); + for (ConflictInfo conflictInfo : + getConflictingSchedulesInfo(schedules, tunerCount, periods)) { + result.add(conflictInfo.schedule); + } return result; } @VisibleForTesting - static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( - List<ScheduledRecording> schedules, int tunerCount) { + static List<ConflictInfo> getConflictingSchedulesInfo(List<ScheduledRecording> schedules, + int tunerCount) { return getConflictingSchedulesInfo(schedules, tunerCount, null); } @@ -836,13 +841,13 @@ public class DvrScheduleManager { * to be partially recorded under the given schedules and tuner count {@code true}, * or not {@code false}. */ - private static Map<ScheduledRecording, Boolean> getConflictingSchedulesInfo( + private static List<ConflictInfo> getConflictingSchedulesInfo( List<ScheduledRecording> schedules, int tunerCount, List<Range<Long>> periods) { List<ScheduledRecording> schedulesToCheck = new ArrayList<>(schedules); // Sort by the same order as that in InputTaskScheduler. Collections.sort(schedulesToCheck, InputTaskScheduler.getRecordingOrderComparator()); List<ScheduledRecording> recordings = new ArrayList<>(); - Map<ScheduledRecording, Boolean> conflicts = new HashMap<>(); + Map<ScheduledRecording, ConflictInfo> conflicts = new HashMap<>(); Map<ScheduledRecording, ScheduledRecording> modified2OriginalSchedules = new HashMap<>(); // Simulate InputTaskScheduler. while (!schedulesToCheck.isEmpty()) { @@ -853,26 +858,29 @@ public class DvrScheduleManager { if (modified2OriginalSchedules.containsKey(schedule)) { // Schedule has been modified, which means it's already conflicted. // Modify its state to partially conflicted. - conflicts.put(modified2OriginalSchedules.get(schedule), true); + ScheduledRecording originalSchedule = modified2OriginalSchedules.get(schedule); + conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true)); } } else { ScheduledRecording candidate = findReplaceableRecording(recordings, schedule); if (candidate != null) { if (!modified2OriginalSchedules.containsKey(candidate)) { - conflicts.put(candidate, true); + conflicts.put(candidate, new ConflictInfo(candidate, true)); } recordings.remove(candidate); recordings.add(schedule); if (modified2OriginalSchedules.containsKey(schedule)) { // Schedule has been modified, which means it's already conflicted. // Modify its state to partially conflicted. - conflicts.put(modified2OriginalSchedules.get(schedule), true); + ScheduledRecording originalSchedule = + modified2OriginalSchedules.get(schedule); + conflicts.put(originalSchedule, new ConflictInfo(originalSchedule, true)); } } else { if (!modified2OriginalSchedules.containsKey(schedule)) { // if schedule has been modified, it's already conflicted. // No need to add it again. - conflicts.put(schedule, false); + conflicts.put(schedule, new ConflictInfo(schedule, false)); } long earliestEndTime = getEarliestEndTime(recordings); if (earliestEndTime < schedule.getEndTimeMs()) { @@ -912,7 +920,14 @@ public class DvrScheduleManager { } } } - return conflicts; + List<ConflictInfo> result = new ArrayList<>(conflicts.values()); + Collections.sort(result, new Comparator<ConflictInfo>() { + @Override + public int compare(ConflictInfo lhs, ConflictInfo rhs) { + return RESULT_COMPARATOR.compare(lhs.schedule, rhs.schedule); + } + }); + return result; } private static void removeFinishedRecordings(List<ScheduledRecording> recordings, @@ -954,6 +969,17 @@ public class DvrScheduleManager { return earliest; } + @VisibleForTesting + static class ConflictInfo { + public ScheduledRecording schedule; + public boolean partialConflict; + + ConflictInfo(ScheduledRecording schedule, boolean partialConflict) { + this.schedule = schedule; + this.partialConflict = partialConflict; + } + } + /** * A listener which is notified the initialization of schedule manager. */ @@ -970,6 +996,9 @@ public class DvrScheduleManager { public interface OnConflictStateChangeListener { /** * Called when the conflicting schedules change. + * <p> + * Note that this can be called before + * {@link ScheduledRecordingListener#onScheduledRecordingAdded} is called. * * @param conflict {@code true} if the {@code schedules} are the new conflicts, otherwise * {@code false}. diff --git a/src/com/android/tv/dvr/DvrStorageStatusManager.java b/src/com/android/tv/dvr/DvrStorageStatusManager.java index a653b5f4..2d41d732 100644 --- a/src/com/android/tv/dvr/DvrStorageStatusManager.java +++ b/src/com/android/tv/dvr/DvrStorageStatusManager.java @@ -25,6 +25,7 @@ import android.content.IntentFilter; import android.content.OperationApplicationException; import android.database.Cursor; import android.media.tv.TvContract; +import android.media.tv.TvInputInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Environment; @@ -36,8 +37,11 @@ import android.support.annotation.IntDef; import android.support.annotation.WorkerThread; import android.util.Log; +import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.tuner.tvinput.TunerTvInputService; +import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; import java.io.File; @@ -294,7 +298,7 @@ public class DvrStorageStatusManager { storageMounted, storageMountedDir, storageMountedCapacity); } - private class CleanUpDbTask extends AsyncTask<Void, Void, Void> { + private class CleanUpDbTask extends AsyncTask<Void, Void, Boolean> { private final ContentResolver mContentResolver; private CleanUpDbTask() { @@ -302,13 +306,15 @@ public class DvrStorageStatusManager { } @Override - protected Void doInBackground(Void... params) { + protected Boolean doInBackground(Void... params) { @DvrStorageStatusManager.StorageStatus int storageStatus = getDvrStorageStatus(); if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { return null; } - List<ContentProviderOperation> ops = getDeleteOps(storageStatus - == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL); + if (storageStatus == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { + return true; + } + List<ContentProviderOperation> ops = getDeleteOps(); if (ops == null || ops.isEmpty()) { return null; } @@ -329,13 +335,28 @@ public class DvrStorageStatusManager { } @Override - protected void onPostExecute(Void result) { + protected void onPostExecute(Boolean forgetStorage) { + if (forgetStorage != null && forgetStorage == true) { + DvrManager dvrManager = TvApplication.getSingletons(mContext).getDvrManager(); + TvInputManagerHelper tvInputManagerHelper = + TvApplication.getSingletons(mContext).getTvInputManagerHelper(); + List<TvInputInfo> tvInputInfoList = + tvInputManagerHelper.getTvInputInfos(true, false); + if (tvInputInfoList == null || tvInputInfoList.isEmpty()) { + return; + } + for (TvInputInfo info : tvInputInfoList) { + if (Utils.isBundledInput(info.getId())) { + dvrManager.forgetStorage(info.getId()); + } + } + } if (mCleanUpDbTask == this) { mCleanUpDbTask = null; } } - private List<ContentProviderOperation> getDeleteOps(boolean deleteAll) { + private List<ContentProviderOperation> getDeleteOps() { List<ContentProviderOperation> ops = new ArrayList<>(); try (Cursor c = mContentResolver.query( @@ -364,7 +385,7 @@ public class DvrStorageStatusManager { continue; } File recordedProgramDir = new File(dataUri.getPath()); - if (deleteAll || !recordedProgramDir.exists()) { + if (!recordedProgramDir.exists()) { ops.add(ContentProviderOperation.newDelete( TvContract.buildRecordedProgramUri(Long.parseLong(id))).build()); } diff --git a/src/com/android/tv/dvr/DvrWatchedPositionManager.java b/src/com/android/tv/dvr/DvrWatchedPositionManager.java index 4eada742..da6ddb1a 100644 --- a/src/com/android/tv/dvr/DvrWatchedPositionManager.java +++ b/src/com/android/tv/dvr/DvrWatchedPositionManager.java @@ -22,6 +22,7 @@ import android.media.tv.TvInputManager; import android.support.annotation.IntDef; import com.android.tv.common.SharedPreferencesUtils; +import com.android.tv.dvr.data.RecordedProgram; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -36,9 +37,6 @@ import java.util.concurrent.CopyOnWriteArraySet; * It will remember and provides previous watched position of DVR playback. */ public class DvrWatchedPositionManager { - private final static String TAG = "DvrWatchedPositionManager"; - private final boolean DEBUG = false; - private SharedPreferences mWatchedPositions; private final Map<Long, Set> mListeners = new HashMap<>(); diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java index bf72d912..129ba153 100644 --- a/src/com/android/tv/dvr/WritableDvrDataManager.java +++ b/src/com/android/tv/dvr/WritableDvrDataManager.java @@ -18,7 +18,9 @@ package com.android.tv.dvr; import android.support.annotation.MainThread; -import com.android.tv.dvr.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.RecordingState; +import com.android.tv.dvr.data.SeriesRecording; /** * Full data manager. @@ -27,7 +29,7 @@ import com.android.tv.dvr.ScheduledRecording.RecordingState; * for internal use only. Do not call them from UI directly. */ @MainThread -interface WritableDvrDataManager extends DvrDataManager { +public interface WritableDvrDataManager extends DvrDataManager { /** * Adds new recordings. */ diff --git a/src/com/android/tv/dvr/IdGenerator.java b/src/com/android/tv/dvr/data/IdGenerator.java index 0ed6362c..2ade1dad 100644 --- a/src/com/android/tv/dvr/IdGenerator.java +++ b/src/com/android/tv/dvr/data/IdGenerator.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.data; import java.util.concurrent.atomic.AtomicLong; diff --git a/src/com/android/tv/dvr/RecordedProgram.java b/src/com/android/tv/dvr/data/RecordedProgram.java index dd744f80..2e953a52 100644 --- a/src/com/android/tv/dvr/RecordedProgram.java +++ b/src/com/android/tv/dvr/data/RecordedProgram.java @@ -14,22 +14,23 @@ * limitations under the License */ -package com.android.tv.dvr; - -import static android.media.tv.TvContract.RecordedPrograms; +package com.android.tv.dvr.data; import android.annotation.TargetApi; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.media.tv.TvContentRating; import android.media.tv.TvContract; +import android.media.tv.TvContract.RecordedPrograms; import android.net.Uri; import android.os.Build; import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.tv.common.R; +import com.android.tv.common.TvContentRatingCache; import com.android.tv.data.BaseProgram; import com.android.tv.data.GenreItems; import com.android.tv.data.InternalDataUtils; @@ -105,7 +106,8 @@ public class RecordedProgram extends BaseProgram { .setVideoWidth(cursor.getInt(index++)) .setVideoHeight(cursor.getInt(index++)) .setAudioLanguage(cursor.getString(index++)) - .setContentRating(cursor.getString(index++)) + .setContentRatings( + TvContentRatingCache.getInstance().getRatings(cursor.getString(index++))) .setPosterArtUri(cursor.getString(index++)) .setThumbnailUri(cursor.getString(index++)) .setSearchable(cursor.getInt(index++) == 1) @@ -156,7 +158,8 @@ public class RecordedProgram extends BaseProgram { values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.mVideoHeight); } values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.mAudioLanguage); - values.put(RecordedPrograms.COLUMN_CONTENT_RATING, recordedProgram.mContentRating); + values.put(RecordedPrograms.COLUMN_CONTENT_RATING, + TvContentRatingCache.contentRatingsToString(recordedProgram.mContentRatings)); values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.mPosterArtUri); values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.mThumbnailUri); values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.mSearchable ? 1 : 0); @@ -201,7 +204,7 @@ public class RecordedProgram extends BaseProgram { private int mVideoWidth; private int mVideoHeight; private String mAudioLanguage; - private String mContentRating; + private TvContentRating[] mContentRatings; private String mPosterArtUri; private String mThumbnailUri; private boolean mSearchable = true; @@ -326,8 +329,8 @@ public class RecordedProgram extends BaseProgram { return this; } - public Builder setContentRating(String contentRating) { - mContentRating = contentRating; + public Builder setContentRatings(TvContentRating[] contentRatings) { + mContentRatings = contentRatings; return this; } @@ -404,15 +407,17 @@ public class RecordedProgram extends BaseProgram { } public RecordedProgram build() { - // Generate the series ID for the episodic program of other TV input. - if (TextUtils.isEmpty(mSeriesId) - && !TextUtils.isEmpty(mEpisodeNumber)) { + if (TextUtils.isEmpty(mTitle)) { + // If title is null, series cannot be generated for this program. + setSeriesId(null); + } else if (TextUtils.isEmpty(mSeriesId) && !TextUtils.isEmpty(mEpisodeNumber)) { + // If series ID is not set, generate it for the episodic program of other TV input. setSeriesId(BaseProgram.generateSeriesId(mPackageName, mTitle)); } return new RecordedProgram(mId, mPackageName, mInputId, mChannelId, mTitle, mSeriesId, mSeasonNumber, mSeasonTitle, mEpisodeNumber, mEpisodeTitle, mStartTimeUtcMillis, mEndTimeUtcMillis, mBroadcastGenres, mCanonicalGenres, mShortDescription, - mLongDescription, mVideoWidth, mVideoHeight, mAudioLanguage, mContentRating, + mLongDescription, mVideoWidth, mVideoHeight, mAudioLanguage, mContentRatings, mPosterArtUri, mThumbnailUri, mSearchable, mDataUri, mDataBytes, mDurationMillis, mExpireTimeUtcMillis, mInternalProviderFlag1, mInternalProviderFlag2, mInternalProviderFlag3, mInternalProviderFlag4, @@ -443,7 +448,7 @@ public class RecordedProgram extends BaseProgram { .setVideoWidth(orig.getVideoWidth()) .setVideoHeight(orig.getVideoHeight()) .setAudioLanguage(orig.getAudioLanguage()) - .setContentRating(orig.getContentRating()) + .setContentRatings(orig.getContentRatings()) .setPosterArtUri(orig.getPosterArtUri()) .setThumbnailUri(orig.getThumbnailUri()) .setSearchable(orig.isSearchable()) @@ -488,7 +493,7 @@ public class RecordedProgram extends BaseProgram { private final int mVideoWidth; private final int mVideoHeight; private final String mAudioLanguage; - private final String mContentRating; + private final TvContentRating[] mContentRatings; private final String mPosterArtUri; private final String mThumbnailUri; private final boolean mSearchable; @@ -507,10 +512,11 @@ public class RecordedProgram extends BaseProgram { String episodeNumber, String episodeTitle, long startTimeUtcMillis, long endTimeUtcMillis, String[] broadcastGenres, String[] canonicalGenres, String shortDescription, String longDescription, int videoWidth, int videoHeight, - String audioLanguage, String contentRating, String posterArtUri, String thumbnailUri, - boolean searchable, Uri dataUri, long dataBytes, long durationMillis, - long expireTimeUtcMillis, int internalProviderFlag1, int internalProviderFlag2, - int internalProviderFlag3, int internalProviderFlag4, int versionNumber) { + String audioLanguage, TvContentRating[] contentRatings, String posterArtUri, + String thumbnailUri, boolean searchable, Uri dataUri, long dataBytes, + long durationMillis, long expireTimeUtcMillis, int internalProviderFlag1, + int internalProviderFlag2, int internalProviderFlag3, int internalProviderFlag4, + int versionNumber) { mId = id; mPackageName = packageName; mInputId = inputId; @@ -531,7 +537,7 @@ public class RecordedProgram extends BaseProgram { mVideoHeight = videoHeight; mAudioLanguage = audioLanguage; - mContentRating = contentRating; + mContentRatings = contentRatings; mPosterArtUri = posterArtUri; mThumbnailUri = thumbnailUri; mSearchable = searchable; @@ -578,8 +584,10 @@ public class RecordedProgram extends BaseProgram { return mChannelId; } - public String getContentRating() { - return mContentRating; + @Nullable + @Override + public TvContentRating[] getContentRatings() { + return mContentRatings; } public Uri getDataUri() { @@ -605,44 +613,12 @@ public class RecordedProgram extends BaseProgram { return mEpisodeNumber; } - public String getEpisodeTitle() { - return mEpisodeTitle; - } - @Override - public String getEpisodeDisplayTitle(Context context) { - if (!TextUtils.isEmpty(mEpisodeNumber)) { - String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle; - if (TextUtils.equals(mSeasonNumber, "0")) { - // Do not show "S0: ". - return String.format(context.getResources().getString( - R.string.display_episode_title_format_no_season_number), - mEpisodeNumber, episodeTitle); - } else { - return String.format(context.getResources().getString( - R.string.display_episode_title_format), - mSeasonNumber, mEpisodeNumber, episodeTitle); - } - } + public String getEpisodeTitle() { return mEpisodeTitle; } @Nullable - @Override - public String getTitleWithEpisodeNumber(Context context) { - if (TextUtils.isEmpty(mTitle)) { - return mTitle; - } - if (TextUtils.isEmpty(mSeasonNumber) || mSeasonNumber.equals("0")) { - return TextUtils.isEmpty(mEpisodeNumber) ? mTitle : context.getString( - R.string.program_title_with_episode_number_no_season, mTitle, mEpisodeNumber); - } else { - return context.getString(R.string.program_title_with_episode_number, mTitle, - mSeasonNumber, mEpisodeNumber); - } - } - - @Nullable public String getEpisodeDisplayNumber(Context context) { if (!TextUtils.isEmpty(mEpisodeNumber)) { if (TextUtils.equals(mSeasonNumber, "0")) { @@ -796,7 +772,7 @@ public class RecordedProgram extends BaseProgram { Objects.equals(mShortDescription, that.mShortDescription) && Objects.equals(mLongDescription, that.mLongDescription) && Objects.equals(mAudioLanguage, that.mAudioLanguage) && - Objects.equals(mContentRating, that.mContentRating) && + Arrays.equals(mContentRatings, that.mContentRatings) && Objects.equals(mPosterArtUri, that.mPosterArtUri) && Objects.equals(mThumbnailUri, that.mThumbnailUri); } @@ -831,7 +807,8 @@ public class RecordedProgram extends BaseProgram { ", mVideoHeight=" + mVideoHeight + ", mVideoWidth=" + mVideoWidth + ", mAudioLanguage='" + mAudioLanguage + '\'' + - ", mContentRating='" + mContentRating + '\'' + + ", mContentRatings='" + + TvContentRatingCache.contentRatingsToString(mContentRatings) + '\'' + ", mPosterArtUri=" + mPosterArtUri + ", mThumbnailUri=" + mThumbnailUri + ", mSearchable=" + mSearchable + diff --git a/src/com/android/tv/dvr/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java index 2bda10ea..5d11c0f3 100644 --- a/src/com/android/tv/dvr/ScheduledRecording.java +++ b/src/com/android/tv/dvr/data/ScheduledRecording.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.data; import android.content.ContentValues; import android.content.Context; @@ -22,14 +22,15 @@ import android.database.Cursor; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.IntDef; -import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Range; import com.android.tv.R; +import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; +import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.util.CompositeComparator; import com.android.tv.util.Utils; @@ -43,7 +44,6 @@ import java.util.Objects; /** * A data class for one recording contents. */ -@VisibleForTesting public final class ScheduledRecording implements Parcelable { private static final String TAG = "ScheduledRecording"; @@ -141,7 +141,6 @@ public final class ScheduledRecording implements Parcelable { /** * Creates a new Builder with the values set from the {@link RecordedProgram}. */ - @VisibleForTesting public static Builder builder(RecordedProgram p) { boolean isProgramRecording = !TextUtils.isEmpty(p.getTitle()); return new Builder() @@ -667,23 +666,19 @@ public final class ScheduledRecording implements Parcelable { } /** - * Returns the program's title withe its season and episode number. + * Returns the program's display title, if the program title is not null, returns program title. + * Otherwise returns the channel name. */ - public String getProgramTitleWithEpisodeNumber(Context context) { - if (TextUtils.isEmpty(mProgramTitle)) { + public String getProgramDisplayTitle(Context context) { + if (!TextUtils.isEmpty(mProgramTitle)) { return mProgramTitle; } - if (TextUtils.isEmpty(mSeasonNumber) || mSeasonNumber.equals("0")) { - return TextUtils.isEmpty(mEpisodeNumber) ? mProgramTitle : context.getString( - R.string.program_title_with_episode_number_no_season, mProgramTitle, - mEpisodeNumber); - } else { - return context.getString(R.string.program_title_with_episode_number, mProgramTitle, - mSeasonNumber, mEpisodeNumber); - } + Channel channel = TvApplication.getSingletons(context).getChannelDataManager() + .getChannel(mChannelId); + return channel != null ? channel.getDisplayName() + : context.getString(R.string.no_program_information); } - /** * Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}. */ diff --git a/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java new file mode 100644 index 00000000..89533dbb --- /dev/null +++ b/src/com/android/tv/dvr/data/SeasonEpisodeNumber.java @@ -0,0 +1,72 @@ +/* + * 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.data; + +import android.text.TextUtils; + +import java.util.Objects; + +/** + * A plain java object which includes the season/episode number for the series recording. + */ +public class SeasonEpisodeNumber { + public final long seriesRecordingId; + public final String seasonNumber; + public final String episodeNumber; + + /** + * Creates a new Builder with the values set from an existing {@link ScheduledRecording}. + */ + public SeasonEpisodeNumber(ScheduledRecording r) { + this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); + } + + public SeasonEpisodeNumber(long seriesRecordingId, String seasonNumber, String episodeNumber) { + this.seriesRecordingId = seriesRecordingId; + this.seasonNumber = seasonNumber; + this.episodeNumber = episodeNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SeasonEpisodeNumber) + || TextUtils.isEmpty(seasonNumber) || TextUtils.isEmpty(episodeNumber)) { + return false; + } + SeasonEpisodeNumber that = (SeasonEpisodeNumber) o; + return seriesRecordingId == that.seriesRecordingId + && Objects.equals(seasonNumber, that.seasonNumber) + && Objects.equals(episodeNumber, that.episodeNumber); + } + + @Override + public int hashCode() { + return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber); + } + + @Override + public String toString() { + return "SeasonEpisodeNumber{" + + "seriesRecordingId=" + seriesRecordingId + + ", seasonNumber='" + seasonNumber + + ", episodeNumber=" + episodeNumber + + '}'; + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/SeriesInfo.java b/src/com/android/tv/dvr/data/SeriesInfo.java index 30256dc5..a0dec4a4 100644 --- a/src/com/android/tv/dvr/SeriesInfo.java +++ b/src/com/android/tv/dvr/data/SeriesInfo.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.data; /** * Series information. diff --git a/src/com/android/tv/dvr/SeriesRecording.java b/src/com/android/tv/dvr/data/SeriesRecording.java index f0690f5f..822e7320 100644 --- a/src/com/android/tv/dvr/SeriesRecording.java +++ b/src/com/android/tv/dvr/data/SeriesRecording.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.data; import android.content.ContentValues; import android.database.Cursor; @@ -26,6 +26,7 @@ import android.text.TextUtils; import com.android.tv.data.BaseProgram; import com.android.tv.data.Program; +import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; import com.android.tv.util.Utils; @@ -128,7 +129,6 @@ public class SeriesRecording implements Parcelable { /** * Creates a new Builder with the values set from an existing {@link SeriesRecording}. */ - @VisibleForTesting public static Builder buildFrom(SeriesRecording r) { return new Builder() .setId(r.mId) diff --git a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java index 1a12fb23..c5383d02 100644 --- a/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java +++ b/src/com/android/tv/dvr/provider/AsyncDvrDbTask.java @@ -21,8 +21,8 @@ import android.database.Cursor; import android.os.AsyncTask; import android.support.annotation.Nullable; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; import com.android.tv.util.NamedThreadFactory; diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java index 2f16ba5d..8b9481a9 100644 --- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java +++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java @@ -27,8 +27,8 @@ import android.provider.BaseColumns; import android.text.TextUtils; import android.util.Log; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.provider.DvrContract.Schedules; import com.android.tv.dvr.provider.DvrContract.SeriesRecordings; diff --git a/src/com/android/tv/dvr/DvrDbSync.java b/src/com/android/tv/dvr/provider/DvrDbSync.java index df181455..ff391959 100644 --- a/src/com/android/tv/dvr/DvrDbSync.java +++ b/src/com/android/tv/dvr/provider/DvrDbSync.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.provider; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -34,8 +34,13 @@ import com.android.tv.TvApplication; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrDataManagerImpl; +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.recorder.SeriesRecordingScheduler; import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask; -import com.android.tv.util.TvProviderUriMatcher; +import com.android.tv.util.TvUriMatcher; import java.util.ArrayList; import java.util.Collections; @@ -57,11 +62,12 @@ import java.util.Set; */ @MainThread @TargetApi(Build.VERSION_CODES.N) -class DvrDbSync { +public class DvrDbSync { private static final String TAG = "DvrDbSync"; private static final boolean DEBUG = false; private final Context mContext; + private final DvrManager mDvrManager; private final DvrDataManagerImpl mDataManager; private final ChannelDataManager mChannelDataManager; private final Queue<Long> mProgramIdQueue = new LinkedList<>(); @@ -72,12 +78,12 @@ class DvrDbSync { @SuppressLint("SwitchIntDef") @Override public void onChange(boolean selfChange, Uri uri) { - switch (TvProviderUriMatcher.match(uri)) { - case TvProviderUriMatcher.MATCH_PROGRAM: + switch (TvUriMatcher.match(uri)) { + case TvUriMatcher.MATCH_PROGRAM: if (DEBUG) Log.d(TAG, "onProgramsUpdated"); onProgramsUpdated(); break; - case TvProviderUriMatcher.MATCH_PROGRAM_ID: + case TvUriMatcher.MATCH_PROGRAM_ID: if (DEBUG) { Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri)); } @@ -129,17 +135,21 @@ class DvrDbSync { } }; - DvrDbSync(Context context, DvrDataManagerImpl dataManager) { - this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager()); + public DvrDbSync(Context context, DvrDataManagerImpl dataManager) { + this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(), + TvApplication.getSingletons(context).getDvrManager(), + SeriesRecordingScheduler.getInstance(context)); } @VisibleForTesting DvrDbSync(Context context, DvrDataManagerImpl dataManager, - ChannelDataManager channelDataManager) { + ChannelDataManager channelDataManager, DvrManager dvrManager, + SeriesRecordingScheduler seriesRecordingScheduler) { mContext = context; + mDvrManager = dvrManager; mDataManager = dataManager; mChannelDataManager = channelDataManager; - mSeriesRecordingScheduler = SeriesRecordingScheduler.getInstance(context); + mSeriesRecordingScheduler = seriesRecordingScheduler; } /** @@ -273,16 +283,15 @@ class DvrDbSync { // Check the series recording. SeriesRecording seriesRecordingForOldSchedule = mDataManager.getSeriesRecording(schedule.getSeriesRecordingId()); - if (program.getSeriesId() != null) { + if (program.isEpisodic()) { // New program belongs to a series. SeriesRecording seriesRecording = mDataManager.getSeriesRecording(program.getSeriesId()); if (seriesRecording == null) { // The new program is episodic while the previous one isn't. - SeriesRecording newSeriesRecording = TvApplication.getSingletons(mContext) - .getDvrManager().addSeriesRecording(program, - Collections.singletonList(program), - SeriesRecording.STATE_SERIES_STOPPED); + SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording( + program, Collections.singletonList(program), + SeriesRecording.STATE_SERIES_STOPPED); builder.setSeriesRecordingId(newSeriesRecording.getId()); needUpdate = true; } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) { @@ -306,8 +315,9 @@ class DvrDbSync { // Old program belongs to a series but the new one doesn't. seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule); } - // Change start time only when the recording start time has not passed. - boolean needToChangeStartTime = schedule.getStartTimeMs() > currentTimeMs + // Change start time only when the recording is not started yet. + boolean needToChangeStartTime = + schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS && program.getStartTimeUtcMillis() != schedule.getStartTimeMs(); if (needToChangeStartTime) { builder.setStartTimeMs(program.getStartTimeUtcMillis()); diff --git a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java index 15ca2700..ba0aca51 100644 --- a/src/com/android/tv/dvr/EpisodicProgramLoadTask.java +++ b/src/com/android/tv/dvr/provider/EpisodicProgramLoadTask.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.provider; import android.annotation.TargetApi; import android.content.Context; @@ -24,13 +24,15 @@ import android.media.tv.TvContract.Programs; import android.net.Uri; import android.os.Build; import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; -import android.text.TextUtils; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.data.SeasonEpisodeNumber; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask; import com.android.tv.util.AsyncDbTask.CursorFilter; import com.android.tv.util.PermissionUtils; @@ -40,7 +42,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; /** @@ -253,21 +254,13 @@ abstract public class EpisodicProgramLoadTask { return sqlParams; } - @VisibleForTesting - static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes, - ScheduledEpisode episode) { - // The episode whose season number or episode number is null will always be scheduled. - return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber) - && !TextUtils.isEmpty(episode.episodeNumber); - } - /** * Filter the programs which match the series recording. The episodes which the schedules are * already created for are filtered out too. */ private class SeriesRecordingCursorFilter implements CursorFilter { private final Set<Long> mDisallowedProgramIds = new HashSet<>(); - private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>(); + private final Set<SeasonEpisodeNumber> mSeasonEpisodeNumbers = new HashSet<>(); SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) { if (!mLoadDisallowedProgram) { @@ -282,7 +275,7 @@ abstract public class EpisodicProgramLoadTask { if (seriesRecordingIds.contains(r.getSeriesRecordingId()) && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { - mScheduledEpisodes.add(new ScheduledEpisode(r)); + mSeasonEpisodeNumbers.add(new SeasonEpisodeNumber(r)); } } } @@ -306,9 +299,9 @@ abstract public class EpisodicProgramLoadTask { } if (programMatches) { return mLoadScheduledEpisode - || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode( - seriesRecording.getId(), program.getSeasonNumber(), - program.getEpisodeNumber())); + || !mSeasonEpisodeNumbers.contains(new SeasonEpisodeNumber( + seriesRecording.getId(), program.getSeasonNumber(), + program.getEpisodeNumber())); } } return false; @@ -333,50 +326,4 @@ abstract public class EpisodicProgramLoadTask { public String[] selectionArgs; public CursorFilter filter; } - - /** - * A plain java object which includes the season/episode number for the series recording. - */ - public static class ScheduledEpisode { - public final long seriesRecordingId; - public final String seasonNumber; - public final String episodeNumber; - - /** - * Create a new Builder with the values set from an existing {@link ScheduledRecording}. - */ - ScheduledEpisode(ScheduledRecording r) { - this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber()); - } - - public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) { - this.seriesRecordingId = seriesRecordingId; - this.seasonNumber = seasonNumber; - this.episodeNumber = episodeNumber; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof ScheduledEpisode)) return false; - ScheduledEpisode that = (ScheduledEpisode) o; - return seriesRecordingId == that.seriesRecordingId - && Objects.equals(seasonNumber, that.seasonNumber) - && Objects.equals(episodeNumber, that.episodeNumber); - } - - @Override - public int hashCode() { - return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber); - } - - @Override - public String toString() { - return "ScheduledEpisode{" + - "seriesRecordingId=" + seriesRecordingId + - ", seasonNumber='" + seasonNumber + - ", episodeNumber=" + episodeNumber + - '}'; - } - } } diff --git a/src/com/android/tv/dvr/ConflictChecker.java b/src/com/android/tv/dvr/recorder/ConflictChecker.java index 201e379e..8aa90116 100644 --- a/src/com/android/tv/dvr/ConflictChecker.java +++ b/src/com/android/tv/dvr/recorder/ConflictChecker.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.annotation.TargetApi; import android.content.ContentUris; @@ -37,6 +37,9 @@ import com.android.tv.common.WeakHandler; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrScheduleManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/com/android/tv/dvr/recorder/DvrRecordingService.java b/src/com/android/tv/dvr/recorder/DvrRecordingService.java new file mode 100644 index 00000000..5d324ca5 --- /dev/null +++ b/src/com/android/tv/dvr/recorder/DvrRecordingService.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.recorder; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.annotation.VisibleForTesting; +import android.util.Log; + +import com.android.tv.ApplicationSingletons; +import com.android.tv.InputSessionManager; +import com.android.tv.InputSessionManager.OnRecordingSessionChangeListener; +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.feature.CommonFeatures; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.util.Clock; +import com.android.tv.util.RecurringRunner; + +/** + * DVR recording service. This service should be a foreground service and send a notification + * to users to do long-running recording task. + * + * <p>This service is waken up when there's a scheduled recording coming soon and at boot completed + * since schedules have to be loaded from databases in order to set new recording alarms, which + * might take a long time. + */ +@RequiresApi(Build.VERSION_CODES.N) +public class DvrRecordingService extends Service { + private static final String TAG = "DvrRecordingService"; + private static final boolean DEBUG = false; + + private static final String DVR_NOTIFICATION_CHANNEL_ID = "dvr_notification_channel"; + private static final int ONGOING_NOTIFICATION_ID = 1; + @VisibleForTesting static final String EXTRA_START_FOR_RECORDING = "start_for_recording"; + + private static DvrRecordingService sInstance; + private NotificationChannel mNotificationChannel; + private String mContentTitle; + private String mContentTextRecording; + private String mContentTextLoading; + + /** + * Starts the service in foreground. + * + * @param startForRecording {@code true} if there are upcoming recordings in + * {@link RecordingScheduler#SOON_DURATION_IN_MS} and the service is + * started in foreground for those recordings. + */ + @MainThread + static void startForegroundService(Context context, boolean startForRecording) { + if (sInstance == null) { + Intent intent = new Intent(context, DvrRecordingService.class); + intent.putExtra(EXTRA_START_FOR_RECORDING, startForRecording); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } + } else { + sInstance.startForeground(startForRecording); + } + } + + @MainThread + static void stopForegroundIfNotRecording() { + if (sInstance != null) { + sInstance.stopForegroundIfNotRecordingInternal(); + } + } + + private RecurringRunner mReaperRunner; + private InputSessionManager mSessionManager; + + @VisibleForTesting boolean mIsRecording; + private boolean mForeground; + + @VisibleForTesting final OnRecordingSessionChangeListener mOnRecordingSessionChangeListener = + new OnRecordingSessionChangeListener() { + @Override + public void onRecordingSessionChange(final boolean create, final int count) { + mIsRecording = count > 0; + if (create) { + startForeground(true); + } else { + stopForegroundIfNotRecordingInternal(); + } + } + }; + + @Override + public void onCreate() { + TvApplication.setCurrentRunningProcess(this, true); + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(); + SoftPreconditions.checkFeatureEnabled(this, CommonFeatures.DVR, TAG); + sInstance = this; + ApplicationSingletons singletons = TvApplication.getSingletons(this); + WritableDvrDataManager dataManager = + (WritableDvrDataManager) singletons.getDvrDataManager(); + mSessionManager = singletons.getInputSessionManager(); + mSessionManager.addOnRecordingSessionChangeListener(mOnRecordingSessionChangeListener); + mReaperRunner = new RecurringRunner(this, java.util.concurrent.TimeUnit.DAYS.toMillis(1), + new ScheduledProgramReaper(dataManager, Clock.SYSTEM), null); + mReaperRunner.start(); + mContentTitle = getString(R.string.dvr_notification_content_title); + mContentTextRecording = getString(R.string.dvr_notification_content_text_recording); + mContentTextLoading = getString(R.string.dvr_notification_content_text_loading); + createNotificationChannel(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (DEBUG) Log.d(TAG, "onStartCommand (" + intent + "," + flags + "," + startId + ")"); + if (intent != null) { + startForeground(intent.getBooleanExtra(EXTRA_START_FOR_RECORDING, false)); + } + return START_STICKY; + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy"); + mReaperRunner.stop(); + mSessionManager.removeRecordingSessionChangeListener(mOnRecordingSessionChangeListener); + sInstance = null; + super.onDestroy(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @VisibleForTesting + protected void stopForegroundIfNotRecordingInternal() { + if (mForeground && !mIsRecording) { + stopForeground(); + } + } + + private void startForeground(boolean hasUpcomingRecording) { + if (!mForeground || hasUpcomingRecording) { + // We may need to update notification for upcoming recordings. + mForeground = true; + startForegroundInternal(hasUpcomingRecording); + } + } + + private void stopForeground() { + stopForegroundInternal(); + mForeground = false; + } + + @VisibleForTesting + protected void startForegroundInternal(boolean hasUpcomingRecording) { + // STOPSHIP: Replace the content title with real UX strings + Notification.Builder builder = new Notification.Builder(this) + .setContentTitle(mContentTitle) + .setContentText(hasUpcomingRecording ? mContentTextRecording : mContentTextLoading) + .setSmallIcon(R.drawable.ic_dvr); + Notification notification = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? + builder.setChannelId(DVR_NOTIFICATION_CHANNEL_ID).build() : builder.build(); + startForeground(ONGOING_NOTIFICATION_ID, notification); + } + + @VisibleForTesting + protected void stopForegroundInternal() { + stopForeground(true); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // STOPSHIP: Replace the channel name with real UX strings + mNotificationChannel = new NotificationChannel(DVR_NOTIFICATION_CHANNEL_ID, + getString(R.string.dvr_notification_channel_name), + NotificationManager.IMPORTANCE_LOW); + ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)) + .createNotificationChannel(mNotificationChannel); + } + } +} diff --git a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java index 6d2f0d43..f1c0020b 100644 --- a/src/com/android/tv/dvr/DvrStartRecordingReceiver.java +++ b/src/com/android/tv/dvr/recorder/DvrStartRecordingReceiver.java @@ -14,21 +14,27 @@ * limitations under the License */ -package com.android.tv.dvr; - -import com.android.tv.TvApplication; +package com.android.tv.dvr.recorder; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.os.Build; +import android.support.annotation.RequiresApi; + +import com.android.tv.TvApplication; /** * Signals the DVR to start recording shows <i>soon</i>. */ +@RequiresApi(Build.VERSION_CODES.N) public class DvrStartRecordingReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { TvApplication.setCurrentRunningProcess(context, true); - DvrRecordingService.startService(context); + RecordingScheduler scheduler = TvApplication.getSingletons(context).getRecordingScheduler(); + if (scheduler != null) { + scheduler.updateAndStartServiceIfNeeded(); + } } } diff --git a/src/com/android/tv/dvr/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java index 53c89ebc..fee4568e 100644 --- a/src/com/android/tv/dvr/InputTaskScheduler.java +++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java @@ -14,14 +14,13 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.content.Context; import android.media.tv.TvInputInfo; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.ArrayMap; import android.util.Log; @@ -30,6 +29,10 @@ import android.util.LongSparseArray; import com.android.tv.InputSessionManager; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Clock; import com.android.tv.util.CompositeComparator; @@ -121,14 +124,13 @@ public class InputTaskScheduler { ChannelDataManager channelDataManager, DvrManager dvrManager, DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock) { this(context, input, looper, channelDataManager, dvrManager, dataManager, sessionManager, - clock, new Handler(Looper.getMainLooper()), null, null); + clock, null); } @VisibleForTesting InputTaskScheduler(Context context, TvInputInfo input, Looper looper, ChannelDataManager channelDataManager, DvrManager dvrManager, DvrDataManager dataManager, InputSessionManager sessionManager, Clock clock, - Handler mainThreadHandler, @Nullable Handler workerThreadHandler, RecordingTaskFactory recordingTaskFactory) { if (DEBUG) Log.d(TAG, "Creating scheduler for " + input); mContext = context; @@ -139,7 +141,7 @@ public class InputTaskScheduler { mDataManager = (WritableDvrDataManager) dataManager; mSessionManager = sessionManager; mClock = clock; - mMainThreadHandler = mainThreadHandler; + mMainThreadHandler = new Handler(Looper.getMainLooper()); mRecordingTaskFactory = recordingTaskFactory != null ? recordingTaskFactory : new RecordingTaskFactory() { @Override @@ -150,11 +152,7 @@ public class InputTaskScheduler { mDataManager, mClock); } }; - if (workerThreadHandler == null) { - mHandler = new WorkerThreadHandler(looper); - } else { - mHandler = workerThreadHandler; - } + mHandler = new WorkerThreadHandler(looper); } /** @@ -211,7 +209,7 @@ public class InputTaskScheduler { && schedule.getStartTimeMs() > wrapper.mTask.getStartTimeMs()) { // It shouldn't have started. Cancel and put to the waiting list. // The schedules will be rebuilt when the task is removed. - // The reschedule is called in Scheduler. + // The reschedule is called in RecordingScheduler. wrapper.mTask.cancel(); mWaitingSchedules.put(schedule.getId(), schedule); return; diff --git a/src/com/android/tv/dvr/Scheduler.java b/src/com/android/tv/dvr/recorder/RecordingScheduler.java index ce78e1be..cbaf46b5 100644 --- a/src/com/android/tv/dvr/Scheduler.java +++ b/src/com/android/tv/dvr/recorder/RecordingScheduler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.app.AlarmManager; import android.app.PendingIntent; @@ -22,18 +22,28 @@ import android.content.Context; import android.content.Intent; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager.TvInputCallback; +import android.os.Build; +import android.os.HandlerThread; import android.os.Looper; import android.support.annotation.MainThread; +import android.support.annotation.RequiresApi; import android.support.annotation.VisibleForTesting; import android.util.ArrayMap; import android.util.Log; import android.util.Range; +import com.android.tv.ApplicationSingletons; import com.android.tv.InputSessionManager; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.ChannelDataManager.Listener; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Clock; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -44,15 +54,25 @@ import java.util.Map; import java.util.concurrent.TimeUnit; /** - * The core class to manage schedule and run actual recording. + * The core class to manage DVR schedule and run recording task. + ** + * <p> This class is responsible for: + * <ul> + * <li>Sending record commands to TV inputs</li> + * <li>Resolving conflicting schedules, handling overlapping recording time durations, etc.</li> + * </ul> + * + * <p>This should be a singleton associated with application's main process. */ +@RequiresApi(Build.VERSION_CODES.N) @MainThread -public class Scheduler extends TvInputCallback implements ScheduledRecordingListener { - private static final String TAG = "Scheduler"; +public class RecordingScheduler extends TvInputCallback implements ScheduledRecordingListener { + private static final String TAG = "RecordingScheduler"; private static final boolean DEBUG = false; - private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(5); - @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.MINUTES.toMillis(1); + private static final String HANDLER_THREAD_NAME = "RecordingScheduler"; + private final static long SOON_DURATION_IN_MS = TimeUnit.MINUTES.toMillis(1); + @VisibleForTesting final static long MS_TO_WAKE_BEFORE_START = TimeUnit.SECONDS.toMillis(30); private final Looper mLooper; private final InputSessionManager mSessionManager; @@ -67,7 +87,52 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList private final Map<String, InputTaskScheduler> mInputSchedulerMap = new ArrayMap<>(); private long mLastStartTimePendingMs; - public Scheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, + private OnDvrScheduleLoadFinishedListener mDvrScheduleLoadListener = + new OnDvrScheduleLoadFinishedListener() { + @Override + public void onDvrScheduleLoadFinished() { + mDataManager.removeDvrScheduleLoadFinishedListener(this); + if (isDbLoaded()) { + updateInternal(); + } + } + }; + + private Listener mChannelDataLoadListener = new Listener() { + @Override + public void onLoadFinished() { + mChannelDataManager.removeListener(this); + if (isDbLoaded()) { + updateInternal(); + } + } + + @Override + public void onChannelListUpdated() { } + + @Override + public void onChannelBrowsableChanged() { } + }; + + /** + * Creates a scheduler to schedule alarms for scheduled recordings and create recording tasks. + * This method should be only called once in the life-cycle of the application. + */ + public static RecordingScheduler createScheduler(Context context) { + SoftPreconditions.checkState( + TvApplication.getSingletons(context).getRecordingScheduler() == null); + HandlerThread handlerThread = new HandlerThread(HANDLER_THREAD_NAME); + handlerThread.start(); + ApplicationSingletons singletons = TvApplication.getSingletons(context); + return new RecordingScheduler(handlerThread.getLooper(), + singletons.getDvrManager(), singletons.getInputSessionManager(), + (WritableDvrDataManager) singletons.getDvrDataManager(), + singletons.getChannelDataManager(), singletons.getTvInputManagerHelper(), context, + Clock.SYSTEM, (AlarmManager) context.getSystemService(Context.ALARM_SERVICE)); + } + + @VisibleForTesting + RecordingScheduler(Looper looper, DvrManager dvrManager, InputSessionManager sessionManager, WritableDvrDataManager dataManager, ChannelDataManager channelDataManager, TvInputManagerHelper inputManager, Context context, Clock clock, AlarmManager alarmManager) { @@ -80,89 +145,70 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList mContext = context; mClock = clock; mAlarmManager = alarmManager; - } - - /** - * Starts the scheduler. - */ - public void start() { mDataManager.addScheduledRecordingListener(this); mInputManager.addCallback(this); - if (mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished()) { + if (isDbLoaded()) { updateInternal(); } else { if (!mDataManager.isDvrScheduleLoadFinished()) { - mDataManager.addDvrScheduleLoadFinishedListener( - new OnDvrScheduleLoadFinishedListener() { - @Override - public void onDvrScheduleLoadFinished() { - mDataManager.removeDvrScheduleLoadFinishedListener(this); - updateInternal(); - } - }); + mDataManager.addDvrScheduleLoadFinishedListener(mDvrScheduleLoadListener); } if (!mChannelDataManager.isDbLoadFinished()) { - mChannelDataManager.addListener(new Listener() { - @Override - public void onLoadFinished() { - mChannelDataManager.removeListener(this); - updateInternal(); - } - - @Override - public void onChannelListUpdated() { } - - @Override - public void onChannelBrowsableChanged() { } - }); + mChannelDataManager.addListener(mChannelDataLoadListener); } } } /** - * Stops the scheduler. + * Start recording that will happen soon, and set the next alarm time. */ - public void stop() { - for (InputTaskScheduler inputTaskScheduler : mInputSchedulerMap.values()) { - inputTaskScheduler.stop(); + public void updateAndStartServiceIfNeeded() { + if (DEBUG) Log.d(TAG, "update and start service if needed"); + if (isDbLoaded()) { + updateInternal(); + } else { + // updateInternal will be called when DB is loaded. Start DvrRecordingService to + // prevent process being killed before that. + DvrRecordingService.startForegroundService(mContext, false); + } + } + + private void updateInternal() { + boolean recordingSoon = updatePendingRecordings(); + updateNextAlarm(); + if (recordingSoon) { + // Start DvrRecordingService to protect upcoming recording task from being killed. + DvrRecordingService.startForegroundService(mContext, true); + } else { + DvrRecordingService.stopForegroundIfNotRecording(); } - mInputManager.removeCallback(this); - mDataManager.removeScheduledRecordingListener(this); } - private void updatePendingRecordings() { + private boolean updatePendingRecordings() { List<ScheduledRecording> scheduledRecordings = mDataManager .getScheduledRecordings(new Range<>(mLastStartTimePendingMs, - mClock.currentTimeMillis() + SOON_DURATION_IN_MS), + mClock.currentTimeMillis() + SOON_DURATION_IN_MS), ScheduledRecording.STATE_RECORDING_NOT_STARTED); for (ScheduledRecording r : scheduledRecordings) { scheduleRecordingSoon(r); } + // update() may be called multiple times, under this situation, pending recordings may be + // already updated thus scheduledRecordings may have a size of 0. Therefore we also have to + // check mLastStartTimePendingMs to check if we have upcoming recordings and prevent the + // recording service being wrongly pushed back to background in updateInternal(). + return scheduledRecordings.size() > 0 + || (mLastStartTimePendingMs > mClock.currentTimeMillis() + && mLastStartTimePendingMs < mClock.currentTimeMillis() + SOON_DURATION_IN_MS); } - /** - * Start recording that will happen soon, and set the next alarm time. - */ - public void update() { - if (DEBUG) Log.d(TAG, "update"); - updateInternal(); - } - - private void updateInternal() { - if (isInitialized()) { - updatePendingRecordings(); - updateNextAlarm(); - } - } - - private boolean isInitialized() { + private boolean isDbLoaded() { return mDataManager.isDvrScheduleLoadFinished() && mChannelDataManager.isDbLoadFinished(); } @Override public void onScheduledRecordingAdded(ScheduledRecording... schedules) { if (DEBUG) Log.d(TAG, "added " + Arrays.asList(schedules)); - if (!isInitialized()) { + if (!isDbLoaded()) { return; } handleScheduleChange(schedules); @@ -171,14 +217,14 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList @Override public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { if (DEBUG) Log.d(TAG, "removed " + Arrays.asList(schedules)); - if (!isInitialized()) { + if (!isDbLoaded()) { return; } boolean needToUpdateAlarm = false; for (ScheduledRecording schedule : schedules) { - InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); - if (scheduler != null) { - scheduler.removeSchedule(schedule); + InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId()); + if (inputTaskScheduler != null) { + inputTaskScheduler.removeSchedule(schedule); needToUpdateAlarm = true; } } @@ -190,14 +236,14 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList @Override public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { if (DEBUG) Log.d(TAG, "state changed " + Arrays.asList(schedules)); - if (!isInitialized()) { + if (!isDbLoaded()) { return; } // Update the recordings. for (ScheduledRecording schedule : schedules) { - InputTaskScheduler scheduler = mInputSchedulerMap.get(schedule.getInputId()); - if (scheduler != null) { - scheduler.updateSchedule(schedule); + InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(schedule.getInputId()); + if (inputTaskScheduler != null) { + inputTaskScheduler.updateSchedule(schedule); } } handleScheduleChange(schedules); @@ -231,13 +277,13 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED); return; } - InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); - if (scheduler == null) { - scheduler = new InputTaskScheduler(mContext, input, mLooper, mChannelDataManager, - mDvrManager, mDataManager, mSessionManager, mClock); - mInputSchedulerMap.put(input.getId(), scheduler); + InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId()); + if (inputTaskScheduler == null) { + inputTaskScheduler = new InputTaskScheduler(mContext, input, mLooper, + mChannelDataManager, mDvrManager, mDataManager, mSessionManager, mClock); + mInputSchedulerMap.put(input.getId(), inputTaskScheduler); } - scheduler.addSchedule(schedule); + inputTaskScheduler.addSchedule(schedule); if (mLastStartTimePendingMs < schedule.getStartTimeMs()) { mLastStartTimePendingMs = schedule.getStartTimeMs(); } @@ -263,21 +309,21 @@ public class Scheduler extends TvInputCallback implements ScheduledRecordingList return mClock.currentTimeMillis() >= scheduledRecording.getStartTimeMs() - durationInMs; } - // No need to remove input task scheduler when the input is removed. If the input is removed - // temporarily, the scheduler should keep the non-started schedules. + // No need to remove input task schedule worker when the input is removed. If the input is + // removed temporarily, the scheduler should keep the non-started schedules. @Override public void onInputUpdated(String inputId) { - InputTaskScheduler scheduler = mInputSchedulerMap.get(inputId); - if (scheduler != null) { - scheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId)); + InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(inputId); + if (inputTaskScheduler != null) { + inputTaskScheduler.updateTvInputInfo(Utils.getTvInputInfoForInputId(mContext, inputId)); } } @Override public void onTvInputInfoUpdated(TvInputInfo input) { - InputTaskScheduler scheduler = mInputSchedulerMap.get(input.getId()); - if (scheduler != null) { - scheduler.updateTvInputInfo(input); + InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId()); + if (inputTaskScheduler != null) { + inputTaskScheduler.updateTvInputInfo(input); } } } diff --git a/src/com/android/tv/dvr/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java index c3d236b0..14888056 100644 --- a/src/com/android/tv/dvr/RecordingTask.java +++ b/src/com/android/tv/dvr/recorder/RecordingTask.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.annotation.TargetApi; import android.content.Context; @@ -37,7 +37,10 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; -import com.android.tv.dvr.InputTaskScheduler.HandlerWrapper; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.recorder.InputTaskScheduler.HandlerWrapper; import com.android.tv.util.Clock; import com.android.tv.util.Utils; @@ -51,7 +54,6 @@ import java.util.concurrent.TimeUnit; * There is only one looper so messages must be handled quickly or start a separate thread. */ @WorkerThread -@VisibleForTesting @TargetApi(Build.VERSION_CODES.N) public class RecordingTask extends RecordingCallback implements Handler.Callback, DvrManager.Listener { @@ -256,13 +258,21 @@ public class RecordingTask extends RecordingCallback implements Handler.Callback public void run() { if (TvApplication.getSingletons(mContext).getMainActivityWrapper() .isResumed()) { - Toast.makeText(mContext.getApplicationContext(), - R.string.dvr_error_insufficient_space_description, - Toast.LENGTH_LONG) - .show(); + ScheduledRecording scheduledRecording = mDataManager + .getScheduledRecording(mScheduledRecording.getId()); + if (scheduledRecording != null) { + Toast.makeText(mContext.getApplicationContext(), + mContext.getString(R.string + .dvr_error_insufficient_space_description_one_recording, + scheduledRecording.getProgramDisplayTitle(mContext)), + Toast.LENGTH_LONG) + .show(); + } } else { Utils.setRecordingFailedReason(mContext.getApplicationContext(), TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + Utils.addFailedScheduledRecordingInfo(mContext.getApplicationContext(), + mScheduledRecording.getProgramDisplayTitle(mContext)); } } }); diff --git a/src/com/android/tv/dvr/ScheduledProgramReaper.java b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java index cd79a631..d958c4a1 100644 --- a/src/com/android/tv/dvr/ScheduledProgramReaper.java +++ b/src/com/android/tv/dvr/recorder/ScheduledProgramReaper.java @@ -14,11 +14,14 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.support.annotation.MainThread; import android.support.annotation.VisibleForTesting; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.util.Clock; import java.util.ArrayList; diff --git a/src/com/android/tv/dvr/SeriesRecordingScheduler.java b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java index 5ed12ce8..15508c24 100644 --- a/src/com/android/tv/dvr/SeriesRecordingScheduler.java +++ b/src/com/android/tv/dvr/recorder/SeriesRecordingScheduler.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.recorder; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -23,7 +23,6 @@ import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.MainThread; -import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; @@ -36,11 +35,19 @@ import com.android.tv.common.SharedPreferencesUtils; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.data.epg.EpgFetcher; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.EpisodicProgramLoadTask.ScheduledEpisode; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.WritableDvrDataManager; +import com.android.tv.dvr.data.SeasonEpisodeNumber; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesInfo; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; import com.android.tv.experiments.Experiments; +import com.android.tv.util.LocationUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -52,11 +59,11 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.concurrent.CopyOnWriteArraySet; import java.util.Set; /** - * Creates the {@link ScheduledRecording}s for the {@link SeriesRecording}. + * Creates the {@link com.android.tv.dvr.data.ScheduledRecording}s for + * the {@link com.android.tv.dvr.data.SeriesRecording}. * <p> * The current implementation assumes that the series recordings are scheduled only for one channel. */ @@ -85,15 +92,13 @@ public class SeriesRecordingScheduler { private final DvrManager mDvrManager; private final WritableDvrDataManager mDataManager; private final List<SeriesRecordingUpdateTask> mScheduleTasks = new ArrayList<>(); - private final List<FetchSeriesInfoTask> mFetchSeriesInfoTasks = new ArrayList<>(); + private final LongSparseArray<FetchSeriesInfoTask> mFetchSeriesInfoTasks = + new LongSparseArray<>(); private final Set<String> mFetchedSeriesIds = new ArraySet<>(); private final SharedPreferences mSharedPreferences; private boolean mStarted; private boolean mPaused; private final Set<Long> mPendingSeriesRecordings = new ArraySet<>(); - private final Set<OnSeriesRecordingUpdatedListener> mOnSeriesRecordingUpdatedListeners = - new CopyOnWriteArraySet<>(); - private final SeriesRecordingListener mSeriesRecordingListener = new SeriesRecordingListener() { @Override @@ -107,7 +112,7 @@ public class SeriesRecordingScheduler { public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { // Cancel the update. for (Iterator<SeriesRecordingUpdateTask> iter = mScheduleTasks.iterator(); - iter.hasNext(); ) { + iter.hasNext(); ) { SeriesRecordingUpdateTask task = iter.next(); if (CollectionUtils.subtract(task.getSeriesRecordings(), seriesRecordings, SeriesRecording.ID_COMPARATOR).isEmpty()) { @@ -115,6 +120,13 @@ public class SeriesRecordingScheduler { iter.remove(); } } + for (SeriesRecording seriesRecording : seriesRecordings) { + FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(seriesRecording.getId()); + if (task != null) { + task.cancel(true); + mFetchSeriesInfoTasks.remove(seriesRecording.getId()); + } + } } @Override @@ -226,7 +238,8 @@ public class SeriesRecordingScheduler { } if (DEBUG) Log.d(TAG, "stop"); mStarted = false; - for (FetchSeriesInfoTask task : mFetchSeriesInfoTasks) { + for (int i = 0; i < mFetchSeriesInfoTasks.size(); i++) { + FetchSeriesInfoTask task = mFetchSeriesInfoTasks.get(mFetchSeriesInfoTasks.keyAt(i)); task.cancel(true); } mFetchSeriesInfoTasks.clear(); @@ -250,7 +263,7 @@ public class SeriesRecordingScheduler { if (Experiments.CLOUD_EPG.get()) { FetchSeriesInfoTask task = new FetchSeriesInfoTask(seriesRecording); task.execute(); - mFetchSeriesInfoTasks.add(task); + mFetchSeriesInfoTasks.put(seriesRecording.getId(), task); } } @@ -363,20 +376,6 @@ public class SeriesRecordingScheduler { } } - /** - * Adds {@link OnSeriesRecordingUpdatedListener}. - */ - public void addOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) { - mOnSeriesRecordingUpdatedListeners.add(listener); - } - - /** - * Removes {@link OnSeriesRecordingUpdatedListener}. - */ - public void removeOnSeriesRecordingUpdatedListener(OnSeriesRecordingUpdatedListener listener) { - mOnSeriesRecordingUpdatedListeners.remove(listener); - } - private boolean needToReadAllChannels(List<SeriesRecording> seriesRecordingsToUpdate) { for (SeriesRecording seriesRecording : seriesRecordingsToUpdate) { if (seriesRecording.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ALL) { @@ -403,8 +402,7 @@ public class SeriesRecordingScheduler { /** * @see #pickOneProgramPerEpisode(List, List) */ - @VisibleForTesting - static LongSparseArray<List<Program>> pickOneProgramPerEpisode( + public static LongSparseArray<List<Program>> pickOneProgramPerEpisode( DvrDataManager dataManager, List<SeriesRecording> seriesRecordings, List<Program> programs) { // Initialize. @@ -415,7 +413,7 @@ public class SeriesRecordingScheduler { seriesRecordingIds.put(seriesRecording.getSeriesId(), seriesRecording.getId()); } // Group programs by the episode. - Map<ScheduledEpisode, List<Program>> programsForEpisodeMap = new HashMap<>(); + Map<SeasonEpisodeNumber, List<Program>> programsForEpisodeMap = new HashMap<>(); for (Program program : programs) { long seriesRecordingId = seriesRecordingIds.get(program.getSeriesId()); if (TextUtils.isEmpty(program.getSeasonNumber()) @@ -424,17 +422,17 @@ public class SeriesRecordingScheduler { result.get(seriesRecordingId).add(program); continue; } - ScheduledEpisode episode = new ScheduledEpisode(seriesRecordingId, + SeasonEpisodeNumber seasonEpisodeNumber = new SeasonEpisodeNumber(seriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()); - List<Program> programsForEpisode = programsForEpisodeMap.get(episode); + List<Program> programsForEpisode = programsForEpisodeMap.get(seasonEpisodeNumber); if (programsForEpisode == null) { programsForEpisode = new ArrayList<>(); - programsForEpisodeMap.put(episode, programsForEpisode); + programsForEpisodeMap.put(seasonEpisodeNumber, programsForEpisode); } programsForEpisode.add(program); } // Pick one program. - for (Entry<ScheduledEpisode, List<Program>> entry : programsForEpisodeMap.entrySet()) { + for (Entry<SeasonEpisodeNumber, List<Program>> entry : programsForEpisodeMap.entrySet()) { List<Program> programsForEpisode = entry.getValue(); Collections.sort(programsForEpisode, new Comparator<Program>() { @Override @@ -512,13 +510,6 @@ public class SeriesRecordingScheduler { mDvrManager.addScheduleToSeriesRecording(seriesRecording, programsToSchedule); } } - if (!mOnSeriesRecordingUpdatedListeners.isEmpty()) { - for (OnSeriesRecordingUpdatedListener listener - : mOnSeriesRecordingUpdatedListeners) { - listener.onSeriesRecordingUpdated( - SeriesRecording.toArray(getSeriesRecordings())); - } - } } @Override @@ -543,7 +534,7 @@ public class SeriesRecordingScheduler { @Override protected SeriesInfo doInBackground(Void... voids) { - return EpgFetcher.createEpgReader(mContext) + return EpgFetcher.createEpgReader(mContext, LocationUtils.getCurrentCountry(mContext)) .getSeriesInfo(mSeriesRecording.getSeriesId()); } @@ -561,19 +552,12 @@ public class SeriesRecordingScheduler { mFetchedSeriesIds.add(seriesInfo.getId()); updateFetchedSeries(); } - mFetchSeriesInfoTasks.remove(this); + mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); } @Override protected void onCancelled(SeriesInfo seriesInfo) { - mFetchSeriesInfoTasks.remove(this); + mFetchSeriesInfoTasks.remove(mSeriesRecording.getId()); } } - - /** - * A listener to notify when series recording are updated. - */ - public interface OnSeriesRecordingUpdatedListener { - void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings); - } } diff --git a/src/com/android/tv/dvr/ui/BigArguments.java b/src/com/android/tv/dvr/ui/BigArguments.java new file mode 100644 index 00000000..ec3b5065 --- /dev/null +++ b/src/com/android/tv/dvr/ui/BigArguments.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.support.annotation.NonNull; + +import com.android.tv.common.SoftPreconditions; + +import java.util.HashMap; +import java.util.Map; + +/** + * Stores the object to pass through activities/fragments. + */ +public class BigArguments { + private final static String TAG = "BigArguments"; + private static Map<String, Object> sBigArgumentMap = new HashMap<>(); + + /** + * Sets the argument. + */ + public static void setArgument(String name, @NonNull Object value) { + SoftPreconditions.checkState(value != null, TAG, "Set argument, but value is null"); + sBigArgumentMap.put(name, value); + } + + /** + * Returns the argument which is associated to the name. + */ + public static Object getArgument(String name) { + return sBigArgumentMap.get(name); + } + + /** + * Resets the arguments. + */ + public static void reset() { + sBigArgumentMap.clear(); + } +} diff --git a/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java new file mode 100644 index 00000000..cddece73 --- /dev/null +++ b/src/com/android/tv/dvr/ui/ChangeImageTransformWithScaledParent.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.drawable.BitmapDrawable; +import android.transition.ChangeImageTransform; +import android.transition.TransitionValues; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; + +import com.android.tv.R; + +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}. + */ +public class ChangeImageTransformWithScaledParent extends ChangeImageTransform { + private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix"; + + public ChangeImageTransformWithScaledParent(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + applyParentScale(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + super.captureEndValues(transitionValues); + applyParentScale(transitionValues); + } + + private void applyParentScale(TransitionValues transitionValues) { + View view = transitionValues.view; + Map<String, Object> values = transitionValues.values; + Matrix matrix = (Matrix) values.get(PROPNAME_MATRIX); + if (matrix != null && view.getId() == R.id.details_overview_image + && view instanceof ImageView) { + ImageView imageView = (ImageView) view; + if (imageView.getScaleType() == ScaleType.CENTER_INSIDE + && imageView.getDrawable() instanceof BitmapDrawable) { + Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap(); + if (bitmap.getWidth() < imageView.getWidth() + && bitmap.getHeight() < imageView.getHeight()) { + float scale = imageView.getContext().getResources().getFraction( + R.fraction.lb_focus_zoom_factor_medium, 1, 1); + matrix.postScale(scale, scale, imageView.getWidth() / 2, + imageView.getHeight() / 2); + } + } + } + } +} diff --git a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java deleted file mode 100644 index 5d8e20ff..00000000 --- a/src/com/android/tv/dvr/ui/CurrentRecordingDetailsFragment.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.res.Resources; -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.TvApplication; -import com.android.tv.dvr.DvrManager; - -/** - * {@link RecordingDetailsFragment} for current recording in DVR. - */ -public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { - private static final int ACTION_STOP_RECORDING = 1; - - @Override - protected SparseArrayObjectAdapter onCreateActionsAdapter() { - SparseArrayObjectAdapter adapter = - new SparseArrayObjectAdapter(new ActionPresenterSelector()); - Resources res = getResources(); - adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING, - res.getString(R.string.epg_dvr_dialog_message_stop_recording), null, - res.getDrawable(R.drawable.lb_ic_stop))); - return adapter; - } - - @Override - protected OnActionClickedListener onCreateOnActionClickedListener() { - return new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (action.getId() == ACTION_STOP_RECORDING) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()) - .getDvrManager(); - dvrManager.stopRecording(getRecording()); - } - getActivity().finish(); - } - }; - } -} diff --git a/src/com/android/tv/dvr/ui/DetailsContent.java b/src/com/android/tv/dvr/ui/DetailsContent.java deleted file mode 100644 index 19521fca..00000000 --- a/src/com/android/tv/dvr/ui/DetailsContent.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.media.tv.TvContract; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import com.android.tv.data.BaseProgram; -import com.android.tv.data.Channel; - -/** - * A class for details content. - */ -public class DetailsContent { - /** Constant for invalid time. */ - public static final long INVALID_TIME = -1; - - private CharSequence mTitle; - private long mStartTimeUtcMillis; - private long mEndTimeUtcMillis; - private String mDescription; - private String mLogoImageUri; - private String mBackgroundImageUri; - - private DetailsContent() { } - - /** - * Returns title. - */ - public CharSequence getTitle() { - return mTitle; - } - - /** - * Returns start time. - */ - public long getStartTimeUtcMillis() { - return mStartTimeUtcMillis; - } - - /** - * Returns end time. - */ - public long getEndTimeUtcMillis() { - return mEndTimeUtcMillis; - } - - /** - * Returns description. - */ - public String getDescription() { - return mDescription; - } - - /** - * Returns Logo image URI as a String. - */ - public String getLogoImageUri() { - return mLogoImageUri; - } - - /** - * Returns background image URI as a String. - */ - public String getBackgroundImageUri() { - return mBackgroundImageUri; - } - - /** - * Copies other details content. - */ - public void copyFrom(DetailsContent other) { - if (this == other) { - return; - } - mTitle = other.mTitle; - mStartTimeUtcMillis = other.mStartTimeUtcMillis; - mEndTimeUtcMillis = other.mEndTimeUtcMillis; - mDescription = other.mDescription; - mLogoImageUri = other.mLogoImageUri; - mBackgroundImageUri = other.mBackgroundImageUri; - } - - /** - * A class for building details content. - */ - public static final class Builder { - private final DetailsContent mDetailsContent; - - public Builder() { - mDetailsContent = new DetailsContent(); - mDetailsContent.mStartTimeUtcMillis = INVALID_TIME; - mDetailsContent.mEndTimeUtcMillis = INVALID_TIME; - } - - /** - * Sets title. - */ - public Builder setTitle(CharSequence title) { - mDetailsContent.mTitle = title; - return this; - } - - /** - * Sets start time. - */ - public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { - mDetailsContent.mStartTimeUtcMillis = startTimeUtcMillis; - return this; - } - - /** - * Sets end time. - */ - public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { - mDetailsContent.mEndTimeUtcMillis = endTimeUtcMillis; - return this; - } - - /** - * Sets description. - */ - public Builder setDescription(String description) { - mDetailsContent.mDescription = description; - return this; - } - - /** - * Sets logo image URI as a String. - */ - public Builder setLogoImageUri(String logoImageUri) { - mDetailsContent.mLogoImageUri = logoImageUri; - return this; - } - - /** - * Sets background image URI as a String. - */ - public Builder setBackgroundImageUri(String backgroundImageUri) { - mDetailsContent.mBackgroundImageUri = backgroundImageUri; - return this; - } - - /** - * Sets background image and logo image URI from program and channel. - */ - public Builder setImageUris(@Nullable BaseProgram program, @Nullable Channel channel) { - if (program != null) { - return setImageUris(program.getPosterArtUri(), program.getThumbnailUri(), channel); - } else { - return setImageUris(null, null, channel); - } - } - - /** - * Sets background image and logo image URI and channel is used for fallback images. - */ - public Builder setImageUris(@Nullable String posterArtUri, - @Nullable String thumbnailUri, @Nullable Channel channel) { - mDetailsContent.mLogoImageUri = null; - mDetailsContent.mBackgroundImageUri = null; - if (!TextUtils.isEmpty(posterArtUri) && !TextUtils.isEmpty(thumbnailUri)) { - mDetailsContent.mLogoImageUri = posterArtUri; - mDetailsContent.mBackgroundImageUri = thumbnailUri; - } else if (!TextUtils.isEmpty(posterArtUri)) { - // thumbnailUri is empty - mDetailsContent.mLogoImageUri = posterArtUri; - mDetailsContent.mBackgroundImageUri = posterArtUri; - } else if (!TextUtils.isEmpty(thumbnailUri)) { - // posterArtUri is empty - mDetailsContent.mLogoImageUri = thumbnailUri; - mDetailsContent.mBackgroundImageUri = thumbnailUri; - } - if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) { - String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId()) - .toString(); - mDetailsContent.mLogoImageUri = channelLogoUri; - mDetailsContent.mBackgroundImageUri = channelLogoUri; - } - return this; - } - - /** - * Builds details content. - */ - public DetailsContent build() { - DetailsContent detailsContent = new DetailsContent(); - detailsContent.copyFrom(mDetailsContent); - return detailsContent; - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java index 9df228d1..62327870 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyRecordedFragment.java @@ -24,15 +24,12 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; -import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.RecordedProgram; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.util.Utils; +import com.android.tv.dvr.data.RecordedProgram; import java.util.List; @@ -92,7 +89,7 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment { } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_RECORD_ANYWAY) { getDvrManager().addSchedule(mProgram); } else if (action.getId() == ACTION_WATCH) { @@ -100,4 +97,23 @@ public class DvrAlreadyRecordedFragment extends DvrGuidedStepFragment { } dismissDialog(); } + + @Override + public String getTrackerPrefix() { + return "onTrackedGuidedActionClicked"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_RECORD_ANYWAY) { + return "record-anyway"; + } else if (actionId == ACTION_WATCH) { + return "watch-now"; + } else if (actionId == ACTION_CANCEL) { + return "cancel-recording"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } } diff --git a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java index 78f21784..6da75e55 100644 --- a/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrAlreadyScheduledFragment.java @@ -25,15 +25,12 @@ import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.text.format.DateUtils; -import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.util.Utils; +import com.android.tv.dvr.data.ScheduledRecording; import java.util.List; @@ -95,7 +92,7 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment { } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_RECORD_ANYWAY) { getDvrManager().addSchedule(mProgram); } else if (action.getId() == ACTION_RECORD_INSTEAD) { @@ -104,4 +101,23 @@ public class DvrAlreadyScheduledFragment extends DvrGuidedStepFragment { } dismissDialog(); } + + @Override + public String getTrackerPrefix() { + return "DvrAlreadyScheduledFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_RECORD_ANYWAY) { + return "record-anyway"; + } else if (actionId == ACTION_RECORD_INSTEAD) { + return "record-instead"; + } else if (actionId == ACTION_CANCEL) { + return "cancel-recording"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } } diff --git a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java index 837d8ab2..36659412 100644 --- a/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrChannelRecordDurationOptionFragment.java @@ -27,7 +27,7 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrChannelRecordConflictFragment; import java.util.ArrayList; @@ -85,7 +85,7 @@ public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragmen } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); long duration = mDurations.get((int) action.getId()); long startTimeMs = System.currentTimeMillis(); @@ -106,4 +106,25 @@ public class DvrChannelRecordDurationOptionFragment extends DvrGuidedStepFragmen R.id.halfsized_dialog_host); } } + + @Override + public String getTrackerPrefix() { + return "DvrChannelRecordDurationOptionFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == 0) { + return "record-10-minutes"; + } else if (actionId == 1) { + return "record-30-minutes"; + } else if (actionId == 2) { + return "record-1-hour"; + } else if (actionId == 3) { + return "record-3-hour"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } } diff --git a/src/com/android/tv/dvr/ui/DvrConflictFragment.java b/src/com/android/tv/dvr/ui/DvrConflictFragment.java index e7be4d0a..6f362e68 100644 --- a/src/com/android/tv/dvr/ui/DvrConflictFragment.java +++ b/src/com/android/tv/dvr/ui/DvrConflictFragment.java @@ -34,10 +34,9 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.ConflictChecker; -import com.android.tv.dvr.ConflictChecker.OnUpcomingConflictChangeListener; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.recorder.ConflictChecker; +import com.android.tv.dvr.recorder.ConflictChecker.OnUpcomingConflictChangeListener; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -85,7 +84,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_VIEW_SCHEDULES) { DvrUiHelper.startSchedulesActivityForOneTimeRecordingConflict( getContext(), getConflicts()); @@ -93,6 +92,16 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { dismissDialog(); } + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = getId(); + if (actionId == ACTION_VIEW_SCHEDULES) { + return "view-schedules"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } + String getConflictDescription() { List<String> titles = new ArrayList<>(); HashSet<String> titleSet = new HashSet<>(); @@ -185,6 +194,11 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); return new Guidance(title, descriptionPrefix + " " + description, null, icon); } + + @Override + public String getTrackerPrefix() { + return "DvrProgramConflictFragment"; + } } /** @@ -236,6 +250,11 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { Drawable icon = getResources().getDrawable(R.drawable.ic_error_white_48dp, null); return new Guidance(title, descriptionPrefix + " " + description, null, icon); } + + @Override + public String getTrackerPrefix() { + return "DvrChannelRecordConflictFragment"; + } } /** @@ -300,7 +319,7 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_CANCEL) { ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker(); if (checker != null) { @@ -319,6 +338,23 @@ public abstract class DvrConflictFragment extends DvrGuidedStepFragment { } @Override + public String getTrackerPrefix() { + return "DvrChannelWatchConflictFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_CANCEL) { + return "cancel"; + } else if (actionId == ACTION_DELETE_CONFLICT) { + return "delete"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } + + @Override public void onDetach() { ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker(); if (checker != null) { diff --git a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java deleted file mode 100644 index 73ddcdd0..00000000 --- a/src/com/android/tv/dvr/ui/DvrForgetStorageErrorFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -import android.support.v17.leanback.widget.GuidedAction; -import android.text.TextUtils; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; - -import java.util.List; - -public class DvrForgetStorageErrorFragment extends DvrGuidedStepFragment { - private static final int ACTION_CANCEL = 1; - private static final int ACTION_FORGET_STORAGE = 2; - private String mInputId; - - @Override - public void onCreate(Bundle savedInstanceState) { - Bundle args = getArguments(); - if (args != null) { - mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID); - } - SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId)); - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString(R.string.dvr_error_forget_storage_title); - String description = getResources().getString( - R.string.dvr_error_forget_storage_description); - return new Guidance(title, description, null, null); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { - Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL) - .title(getResources().getString(R.string.dvr_action_error_cancel)) - .build()); - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_FORGET_STORAGE) - .title(getResources().getString(R.string.dvr_action_error_forget_storage)) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() != ACTION_FORGET_STORAGE) { - dismissDialog(); - return; - } - DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); - dvrManager.forgetStorage(mInputId); - Activity activity = getActivity(); - if (activity instanceof DvrDetailsActivity) { - // Since we removed everything, just finish the activity. - activity.finish(); - } else { - dismissDialog(); - } - } -} diff --git a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java index d26e6836..ab852e10 100644 --- a/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java +++ b/src/com/android/tv/dvr/ui/DvrGuidedStepFragment.java @@ -16,24 +16,43 @@ package com.android.tv.dvr.ui; +import android.app.Activity; import android.app.DialogFragment; import android.content.Context; import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidanceStylist; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.VerticalGridView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.android.tv.ApplicationSingletons; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.dialog.HalfSizedDialogFragment.OnActionClickListener; import com.android.tv.dialog.SafeDismissDialogFragment; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ui.HalfSizedDialogFragment.OnActionClickListener; +import com.android.tv.dvr.DvrStorageStatusManager; + +import java.util.List; + +public abstract class DvrGuidedStepFragment extends TrackedGuidedStepFragment { + /** + * Action ID for "recording/scheduling the program anyway". + */ + public static final int ACTION_RECORD_ANYWAY = 1; + /** + * Action ID for "deleting existed recordings". + */ + public static final int ACTION_DELETE_RECORDINGS = 2; + /** + * Action ID for "cancelling current recording request". + */ + public static final int ACTION_CANCEL_RECORDING = 3; + public static final String UNKNOWN_DVR_ACTION = "Unknown DVR Action"; -public class DvrGuidedStepFragment extends GuidedStepFragment { private DvrManager mDvrManager; private OnActionClickListener mOnActionClickListener; @@ -44,7 +63,8 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { @Override public void onAttach(Context context) { super.onAttach(context); - mDvrManager = TvApplication.getSingletons(context).getDvrManager(); + ApplicationSingletons singletons = TvApplication.getSingletons(context); + mDvrManager = singletons.getDvrManager(); } @Override @@ -64,13 +84,27 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { if (mOnActionClickListener != null) { mOnActionClickListener.onActionClick(action.getId()); } dismissDialog(); } + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_RECORD_ANYWAY) { + return "record-anyway"; + } else if (actionId == ACTION_DELETE_RECORDINGS) { + return "delete-recordings"; + } else if (actionId == ACTION_CANCEL_RECORDING) { + return "cancel-recording"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } + protected void dismissDialog() { if (getActivity() instanceof MainActivity) { SafeDismissDialogFragment currentDialog = @@ -86,4 +120,76 @@ public class DvrGuidedStepFragment extends GuidedStepFragment { protected void setOnActionClickListener(OnActionClickListener listener) { mOnActionClickListener = listener; } + + /** + * The inner guided step fragment for + * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment + * .DvrNoFreeSpaceErrorDialogFragment}. + */ + public static class DvrNoFreeSpaceErrorFragment extends DvrGuidedStepFragment { + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + return new GuidanceStylist.Guidance(getString(R.string.dvr_error_no_free_space_title), + getString(R.string.dvr_error_no_free_space_description), null, null); + } + + @Override + public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { + Activity activity = getActivity(); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_RECORD_ANYWAY) + .title(R.string.dvr_action_record_anyway) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_DELETE_RECORDINGS) + .title(R.string.dvr_action_delete_recordings) + .build()); + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_CANCEL_RECORDING) + .title(R.string.dvr_action_record_cancel) + .build()); + } + + @Override + public String getTrackerPrefix() { + return "DvrNoFreeSpaceErrorFragment"; + } + } + + /** + * The inner guided step fragment for + * {@link com.android.tv.dvr.ui.DvrHalfSizedDialogFragment + * .DvrSmallSizedStorageErrorDialogFragment}. + */ + public static class DvrSmallSizedStorageErrorFragment extends DvrGuidedStepFragment { + @Override + public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { + String title = getResources().getString( + R.string.dvr_error_small_sized_storage_title); + String description = getResources().getString( + R.string.dvr_error_small_sized_storage_description, + DvrStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES / 1024 + / 1024 / 1024); + return new 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 "DvrSmallSizedStorageErrorFragment"; + } + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java index 2b132db8..f8ef3850 100644 --- a/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java +++ b/src/com/android/tv/dvr/ui/DvrHalfSizedDialogFragment.java @@ -29,6 +29,7 @@ import android.view.ViewGroup; import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.dvr.DvrStorageStatusManager; +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; @@ -166,6 +167,17 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { } /** + * A dialog fragment to show error message when there is no enough free space to record. + */ + public static class DvrNoFreeSpaceErrorDialogFragment + extends DvrGuidedStepDialogFragment { + @Override + protected DvrGuidedStepFragment onCreateGuidedStepFragment() { + return new DvrGuidedStepFragment.DvrNoFreeSpaceErrorFragment(); + } + } + + /** * A dialog fragment to show error message when the current storage is too small to * support DVR */ @@ -173,32 +185,7 @@ public class DvrHalfSizedDialogFragment extends HalfSizedDialogFragment { extends DvrGuidedStepDialogFragment { @Override protected DvrGuidedStepFragment onCreateGuidedStepFragment() { - return new DvrGuidedStepFragment() { - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString( - R.string.dvr_error_small_sized_storage_title); - String description = getResources().getString( - R.string.dvr_error_small_sized_storage_description, - DvrStorageStatusManager.MIN_STORAGE_SIZE_FOR_DVR_IN_BYTES / 1024 - / 1024 / 1024); - return new Guidance(title, description, null, null); - } - - @Override - public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { - Activity activity = getActivity(); - actions.add(new GuidedAction.Builder(activity) - .id(GuidedAction.ACTION_ID_OK) - .title(android.R.string.ok) - .build()); - } - - @Override - public void onGuidedActionClicked(GuidedAction action) { - dismissDialog(); - } - }; + return new DvrGuidedStepFragment.DvrSmallSizedStorageErrorFragment(); } } diff --git a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java index 3b1dbfa0..182416b6 100644 --- a/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrInsufficientSpaceErrorFragment.java @@ -17,6 +17,7 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; @@ -24,19 +25,67 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrDataManager; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.ui.browse.DvrBrowseActivity; +import java.util.ArrayList; import java.util.List; public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { - private static final int ACTION_DONE = 1; - private static final int ACTION_OPEN_DVR = 2; + /** + * Key for the failed scheduled recordings information. + */ + public static final String FAILED_SCHEDULED_RECORDING_INFOS = + "failed_scheduled_recording_infos"; + + private static final String TAG = "DvrInsufficientSpaceErrorFragment"; + + private static final int ACTION_VIEW_RECENT_RECORDINGS = 1; + + private ArrayList<String> mFailedScheduledRecordingInfos; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + Bundle args = getArguments(); + if (args != null) { + mFailedScheduledRecordingInfos = + args.getStringArrayList(FAILED_SCHEDULED_RECORDING_INFOS); + } + SoftPreconditions.checkState( + mFailedScheduledRecordingInfos != null && !mFailedScheduledRecordingInfos.isEmpty(), + TAG, "failed scheduled recording is null"); + } @Override public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title = getResources().getString(R.string.dvr_error_insufficient_space_title); - String description = getResources() - .getString(R.string.dvr_error_insufficient_space_description); + String title; + String description; + int failedScheduledRecordingSize = mFailedScheduledRecordingInfos.size(); + if (failedScheduledRecordingSize == 1) { + title = getString( + R.string.dvr_error_insufficient_space_title_one_recording, + mFailedScheduledRecordingInfos.get(0)); + description = getString( + R.string.dvr_error_insufficient_space_description_one_recording, + mFailedScheduledRecordingInfos.get(0)); + } else if (failedScheduledRecordingSize == 2) { + title = getString( + R.string.dvr_error_insufficient_space_title_two_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); + description = getString( + R.string.dvr_error_insufficient_space_description_two_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1)); + } else { + title = getString( + R.string.dvr_error_insufficient_space_title_three_or_more_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), + mFailedScheduledRecordingInfos.get(2)); + description = getString( + R.string.dvr_error_insufficient_space_description_three_or_more_recordings, + mFailedScheduledRecordingInfos.get(0), mFailedScheduledRecordingInfos.get(1), + mFailedScheduledRecordingInfos.get(2)); + } return new Guidance(title, description, null, null); } @@ -44,28 +93,38 @@ public class DvrInsufficientSpaceErrorFragment extends DvrGuidedStepFragment { public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_DONE) - .title(getResources().getString(R.string.dvr_action_error_done)) + .clickAction(GuidedAction.ACTION_ID_OK) .build()); - DvrDataManager dvrDataManager = TvApplication.getSingletons(getContext()) - .getDvrDataManager(); - if (!(dvrDataManager.getRecordedPrograms().isEmpty() - && dvrDataManager.getStartedRecordings().isEmpty() - && dvrDataManager.getNonStartedScheduledRecordings().isEmpty() - && dvrDataManager.getSeriesRecordings().isEmpty())) { - actions.add(new GuidedAction.Builder(activity) - .id(ACTION_OPEN_DVR) - .title(getResources().getString(R.string.dvr_action_error_open_dvr)) - .build()); + if (TvApplication.getSingletons(getContext()).getDvrManager().hasValidItems()) { + actions.add(new GuidedAction.Builder(activity) + .id(ACTION_VIEW_RECENT_RECORDINGS) + .title(getResources().getString( + R.string.dvr_error_insufficient_space_action_view_recent_recordings)) + .build()); } } @Override - public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_OPEN_DVR) { - Intent intent = new Intent(getActivity(), DvrActivity.class); + public void onTrackedGuidedActionClicked(GuidedAction action) { + if (action.getId() == ACTION_VIEW_RECENT_RECORDINGS) { + Intent intent = new Intent(getActivity(), DvrBrowseActivity.class); getActivity().startActivity(intent); } dismissDialog(); } + + @Override + public String getTrackerPrefix() { + return "DvrInsufficientSpaceErrorFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_VIEW_RECENT_RECORDINGS) { + return "view-recent"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } } diff --git a/src/com/android/tv/dvr/ui/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/DvrItemPresenter.java deleted file mode 100644 index 339e5d2f..00000000 --- a/src/com/android/tv/dvr/ui/DvrItemPresenter.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.support.annotation.CallSuper; -import android.support.v17.leanback.widget.Presenter; -import android.view.View; -import android.view.View.OnClickListener; - -import com.android.tv.dvr.DvrUiHelper; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -/** - * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in - * {@link DvrBrowseFragment}. DVR items might include: {@link ScheduledRecording}, - * {@link RecordedProgram}, and {@link SeriesRecording}. - */ -public abstract class DvrItemPresenter extends Presenter { - private final Set<ViewHolder> mBoundViewHolders = new HashSet<>(); - private final OnClickListener mOnClickListener = onCreateOnClickListener(); - - @Override - @CallSuper - public void onBindViewHolder(ViewHolder viewHolder, Object o) { - viewHolder.view.setTag(o); - viewHolder.view.setOnClickListener(mOnClickListener); - mBoundViewHolders.add(viewHolder); - } - - @Override - @CallSuper - public void onUnbindViewHolder(ViewHolder viewHolder) { - mBoundViewHolders.remove(viewHolder); - } - - /** - * Unbinds all bound view holders. - */ - public void unbindAllViewHolders() { - // When browse fragments are destroyed, RecyclerView would not call presenters' - // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks. - for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) { - onUnbindViewHolder(viewHolder); - } - } - - /** - * Creates {@link OnClickListener} for DVR library's card views. - */ - protected OnClickListener onCreateOnClickListener() { - return new OnClickListener() { - @Override - public void onClick(View view) { - if (view instanceof RecordingCardView) { - RecordingCardView v = (RecordingCardView) view; - DvrUiHelper.startDetailsActivity((Activity) v.getContext(), - v.getTag(), v.getImageView(), false); - } - } - }; - } -}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java index 2e2c2849..e726995f 100644 --- a/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java +++ b/src/com/android/tv/dvr/ui/DvrMissingStorageErrorFragment.java @@ -17,29 +17,27 @@ package com.android.tv.dvr.ui; import android.app.Activity; +import android.content.ActivityNotFoundException; +import android.content.Intent; import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; +import android.provider.Settings; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; -import android.text.TextUtils; +import android.util.Log; import com.android.tv.R; -import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.ui.browse.DvrDetailsActivity; import java.util.List; public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { - private static final int ACTION_CANCEL = 1; - private static final int ACTION_FORGET_STORAGE = 2; - private String mInputId; + private static final String TAG = "DvrMissingStorageError"; + + private static final int ACTION_OK = 1; + private static final int ACTION_OPEN_STORAGE_SETTINGS = 2; @Override public void onCreate(Bundle savedInstanceState) { - Bundle args = getArguments(); - if (args != null) { - mInputId = args.getString(DvrHalfSizedDialogFragment.KEY_INPUT_ID); - } - SoftPreconditions.checkArgument(!TextUtils.isEmpty(mInputId)); super.onCreate(savedInstanceState); } @@ -55,25 +53,46 @@ public class DvrMissingStorageErrorFragment extends DvrGuidedStepFragment { public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { Activity activity = getActivity(); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_CANCEL) - .title(getResources().getString(R.string.dvr_action_error_cancel)) + .id(ACTION_OK) + .title(android.R.string.ok) .build()); actions.add(new GuidedAction.Builder(activity) - .id(ACTION_FORGET_STORAGE) - .title(getResources().getString(R.string.dvr_action_error_forget_storage)) + .id(ACTION_OPEN_STORAGE_SETTINGS) + .title(getResources().getString(R.string.dvr_action_error_storage_settings)) .build()); } @Override - public void onGuidedActionClicked(GuidedAction action) { - if (action.getId() == ACTION_FORGET_STORAGE) { - DvrForgetStorageErrorFragment fragment = new DvrForgetStorageErrorFragment(); - Bundle args = new Bundle(); - args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, mInputId); - fragment.setArguments(args); - GuidedStepFragment.add(getFragmentManager(), fragment, R.id.halfsized_dialog_host); + public void onTrackedGuidedActionClicked(GuidedAction action) { + Activity activity = getActivity(); + if (activity instanceof DvrDetailsActivity) { + activity.finish(); + } else { + dismissDialog(); + } + if (action.getId() != ACTION_OPEN_STORAGE_SETTINGS) { return; } - dismissDialog(); + final Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS); + try { + getContext().startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Can't start internal storage settings activity", e); + } + } + + @Override + public String getTrackerPrefix() { + return "DvrMissingStorageErrorFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_OPEN_STORAGE_SETTINGS) { + return "open-storage-settings"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java deleted file mode 100644 index 8c4c856c..00000000 --- a/src/com/android/tv/dvr/ui/DvrPlaybackCardPresenter.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; -import android.text.TextUtils; -import android.util.Log; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.DvrPlaybackActivity; -import com.android.tv.util.Utils; - -/** - * This class is used to generate Views and bind Objects for related recordings in DVR playback. - */ -public class DvrPlaybackCardPresenter extends RecordedProgramPresenter { - private static final String TAG = "DvrPlaybackCardPresenter"; - private static final boolean DEBUG = false; - - private final int mRelatedRecordingCardWidth; - private final int mRelatedRecordingCardHeight; - - DvrPlaybackCardPresenter(Context context) { - super(context); - mRelatedRecordingCardWidth = - context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width); - mRelatedRecordingCardHeight = - context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_height); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Resources res = parent.getResources(); - RecordingCardView view = new RecordingCardView( - getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight); - return new ViewHolder(view); - } - - @Override - protected OnClickListener onCreateOnClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - long programId = ((RecordedProgram) v.getTag()).getId(); - if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); - Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); - getContext().startActivity(intent); - } - }; - } - - @Override - protected String getDescription(RecordedProgram program) { - String description = program.getDescription(); - if (TextUtils.isEmpty(description)) { - description = - getContext().getResources().getString(R.string.dvr_msg_no_program_description); - } - return description; - } -}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java deleted file mode 100644 index 51ec93b8..00000000 --- a/src/com/android/tv/dvr/ui/DvrPlaybackOverlayFragment.java +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Point; -import android.hardware.display.DisplayManager; -import android.media.tv.TvContentRating; -import android.os.Bundle; -import android.media.session.PlaybackState; -import android.media.tv.TvInputManager; -import android.media.tv.TvView; -import android.support.v17.leanback.app.PlaybackOverlayFragment; -import android.support.v17.leanback.widget.ArrayObjectAdapter; -import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.support.v17.leanback.widget.HeaderItem; -import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; -import android.support.v17.leanback.widget.PlaybackControlsRow; -import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; -import android.support.v17.leanback.widget.SinglePresenterSelector; -import android.view.Display; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import android.text.TextUtils; -import android.util.Log; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.BaseProgram; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dialog.PinDialogFragment; -import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrPlayer; -import com.android.tv.dvr.DvrPlaybackMediaSessionHelper; -import com.android.tv.parental.ContentRatingsManager; -import com.android.tv.util.Utils; - -public class DvrPlaybackOverlayFragment extends PlaybackOverlayFragment { - // TODO: Handles audio focus. Deals with block and ratings. - private static final String TAG = "DvrPlaybackOverlayFragment"; - private static final boolean DEBUG = false; - - private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; - private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; - - // mProgram is only used to store program from intent. Don't use it elsewhere. - private RecordedProgram mProgram; - private DvrPlaybackMediaSessionHelper mMediaSessionHelper; - private DvrPlaybackControlHelper mPlaybackControlHelper; - private ArrayObjectAdapter mRowsAdapter; - private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter; - private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; - private DvrDataManager mDvrDataManager; - private ContentRatingsManager mContentRatingsManager; - private TvView mTvView; - private View mBlockScreenView; - private ListRow mRelatedRecordingsRow; - private int mExtraPaddingNoRelatedRow; - private int mWindowWidth; - private int mWindowHeight; - private float mAppliedAspectRatio; - private float mWindowAspectRatio; - private boolean mPinChecked; - - @Override - public void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate"); - super.onCreate(savedInstanceState); - mExtraPaddingNoRelatedRow = getActivity().getResources() - .getDimensionPixelOffset(R.dimen.dvr_playback_fragment_extra_padding_top); - mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); - mContentRatingsManager = TvApplication.getSingletons(getContext()) - .getTvInputManagerHelper().getContentRatingsManager(); - mProgram = getProgramFromIntent(getActivity().getIntent()); - if (mProgram == null) { - Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), - Toast.LENGTH_SHORT).show(); - getActivity().finish(); - return; - } - Point size = new Point(); - ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) - .getDisplay(Display.DEFAULT_DISPLAY).getSize(size); - mWindowWidth = size.x; - mWindowHeight = size.y; - mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight; - setBackgroundType(PlaybackOverlayFragment.BG_LIGHT); - setFadingEnabled(true); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); - mBlockScreenView = getActivity().findViewById(R.id.block_screen); - mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( - getActivity(), MEDIA_SESSION_TAG, new DvrPlayer(mTvView), this); - mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); - setUpRows(); - preparePlayback(getActivity().getIntent()); - DvrPlayer dvrPlayer = mMediaSessionHelper.getDvrPlayer(); - dvrPlayer.setAspectRatioChangedListener(new DvrPlayer.AspectRatioChangedListener() { - @Override - public void onAspectRatioChanged(float videoAspectRatio) { - updateAspectRatio(videoAspectRatio); - } - }); - mPinChecked = getActivity().getIntent() - .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); - dvrPlayer.setContentBlockedListener(new DvrPlayer.ContentBlockedListener() { - @Override - public void onContentBlocked(TvContentRating rating) { - if (mPinChecked) { - mTvView.unblockContent(rating); - return; - } - mBlockScreenView.setVisibility(View.VISIBLE); - getActivity().getMediaController().getTransportControls().pause(); - new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR, - new PinDialogFragment.ResultListener() { - @Override - public void done(boolean success) { - if (success) { - mPinChecked = true; - mTvView.unblockContent(rating); - mBlockScreenView.setVisibility(View.GONE); - getActivity().getMediaController() - .getTransportControls().play(); - } - } - }, mContentRatingsManager.getDisplayNameForRating(rating)) - .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); - } - }); - } - - @Override - public void onPause() { - if (DEBUG) Log.d(TAG, "onPause"); - super.onPause(); - if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING - || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) { - getActivity().getMediaController().getTransportControls().pause(); - } - if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) { - getActivity().requestVisibleBehind(false); - } else { - getActivity().requestVisibleBehind(true); - } - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - mPlaybackControlHelper.unregisterCallback(); - mMediaSessionHelper.release(); - mRelatedRecordingCardPresenter.unbindAllViewHolders(); - super.onDestroy(); - } - - /** - * Passes the intent to the fragment. - */ - public void onNewIntent(Intent intent) { - mProgram = getProgramFromIntent(intent); - if (mProgram == null) { - Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), - Toast.LENGTH_SHORT).show(); - // Continue playing the original program - return; - } - preparePlayback(intent); - } - - /** - * Should be called when windows' size is changed in order to notify DVR player - * to update it's view width/height and position. - */ - public void onWindowSizeChanged(final int windowWidth, final int windowHeight) { - mWindowWidth = windowWidth; - mWindowHeight = windowHeight; - mWindowAspectRatio = (float) mWindowWidth / mWindowHeight; - updateAspectRatio(mAppliedAspectRatio); - } - - public RecordedProgram getNextEpisode(RecordedProgram program) { - int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); - if (position == mRelatedRecordingsRowAdapter.size()) { - return null; - } else { - return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); - } - } - - void onMediaControllerUpdated() { - mRowsAdapter.notifyArrayItemRangeChanged(0, 1); - } - - private void updateAspectRatio(float videoAspectRatio) { - if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { - // No need to change - return; - } - if (videoAspectRatio < mWindowAspectRatio) { - int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2; - ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0); - } else { - int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2; - ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding); - } - mAppliedAspectRatio = videoAspectRatio; - } - - private void preparePlayback(Intent intent) { - mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); - getActivity().getMediaController().getTransportControls().prepare(); - updateRelatedRecordingsRow(); - } - - private void updateRelatedRecordingsRow() { - boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0); - mRelatedRecordingsRowAdapter.clear(); - long programId = mProgram.getId(); - String seriesId = mProgram.getSeriesId(); - if (!TextUtils.isEmpty(seriesId)) { - if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); - for (RecordedProgram program : mDvrDataManager.getRecordedPrograms()) { - if (seriesId.equals(program.getSeriesId()) && programId != program.getId()) { - mRelatedRecordingsRowAdapter.add(program); - } - } - } - View view = getView(); - if (mRelatedRecordingsRowAdapter.size() == 0) { - mRowsAdapter.remove(mRelatedRecordingsRow); - view.setPadding(view.getPaddingLeft(), mExtraPaddingNoRelatedRow, - view.getPaddingRight(), view.getPaddingBottom()); - } else if (wasEmpty){ - mRowsAdapter.add(mRelatedRecordingsRow); - view.setPadding(view.getPaddingLeft(), 0, - view.getPaddingRight(), view.getPaddingBottom()); - } - } - - private void setUpRows() { - PlaybackControlsRowPresenter controlsRowPresenter = - mPlaybackControlHelper.createControlsRowAndPresenter(); - - ClassPresenterSelector selector = new ClassPresenterSelector(); - selector.addClassPresenter(PlaybackControlsRow.class, controlsRowPresenter); - selector.addClassPresenter(ListRow.class, new ListRowPresenter()); - - mRowsAdapter = new ArrayObjectAdapter(selector); - mRowsAdapter.add(mPlaybackControlHelper.getControlsRow()); - mRelatedRecordingsRow = getRelatedRecordingsRow(); - setAdapter(mRowsAdapter); - } - - private ListRow getRelatedRecordingsRow() { - mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); - mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); - HeaderItem header = new HeaderItem(0, - getActivity().getString(R.string.dvr_playback_related_recordings)); - return new ListRow(header, mRelatedRecordingsRowAdapter); - } - - private RecordedProgram getProgramFromIntent(Intent intent) { - long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1); - return mDvrDataManager.getRecordedProgram(programId); - } - - private long getSeekTimeFromIntent(Intent intent) { - return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, - TvInputManager.TIME_SHIFT_INVALID_TIME); - } - - private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> { - RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { - super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); - } - - @Override - long getId(BaseProgram item) { - return item.getId(); - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java index 158bd824..e4cb7243 100644 --- a/src/com/android/tv/dvr/ui/PrioritySettingsFragment.java +++ b/src/com/android/tv/dvr/ui/DvrPrioritySettingsFragment.java @@ -20,7 +20,6 @@ import android.app.FragmentManager; import android.content.Context; import android.graphics.Typeface; import android.os.Bundle; -import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.GuidedActionsStylist; @@ -33,15 +32,13 @@ import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.SeriesRecording; import java.util.ArrayList; import java.util.List; -/** - * Fragment for DVR series recording settings. - */ -public class PrioritySettingsFragment extends GuidedStepFragment { +/** Fragment for DVR series recording settings. */ +public class DvrPrioritySettingsFragment extends TrackedGuidedStepFragment { /** * Name of series recording id starting the fragment. * Type: Long @@ -124,7 +121,7 @@ public class PrioritySettingsFragment extends GuidedStepFragment { } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == ACTION_ID_SAVE) { DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); @@ -156,13 +153,27 @@ public class PrioritySettingsFragment extends GuidedStepFragment { } @Override + public String getTrackerPrefix() { + return "DvrPrioritySettingsFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_ID_SAVE) { + return "save"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } + + @Override public void onGuidedActionFocused(GuidedAction action) { super.onGuidedActionFocused(action); if (mSelectedRecording == null) { return; } if (action.getId() < 0) { - int selectedPosition = mSeriesRecordings.indexOf(mSelectedRecording); mSelectedRecording = null; for (int i = 0; i < mSeriesRecordings.size(); ++i) { updateItem(i); @@ -248,4 +259,4 @@ public class PrioritySettingsFragment extends GuidedStepFragment { titleView.setTypeface(titleView.getTypeface(), Typeface.NORMAL); } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java index da6d1637..390e0928 100644 --- a/src/com/android/tv/dvr/ui/DvrScheduleFragment.java +++ b/src/com/android/tv/dvr/ui/DvrScheduleFragment.java @@ -32,9 +32,8 @@ import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.DvrConflictFragment.DvrProgramConflictFragment; import com.android.tv.util.Utils; @@ -48,18 +47,26 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrScheduleFragment extends DvrGuidedStepFragment { + /** + * Key for the whether to add the current program to series. + * Type: boolean + */ + public static final String KEY_ADD_CURRENT_PROGRAM_TO_SERIES = "add_current_program_to_series"; + private static final String TAG = "DvrScheduleFragment"; private static final int ACTION_RECORD_EPISODE = 1; private static final int ACTION_RECORD_SERIES = 2; private Program mProgram; + private boolean mAddCurrentProgramToSeries; @Override public void onCreate(Bundle savedInstanceState) { Bundle args = getArguments(); if (args != null) { mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); + mAddCurrentProgramToSeries = args.getBoolean(KEY_ADD_CURRENT_PROGRAM_TO_SERIES, false); } DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); SoftPreconditions.checkArgument(mProgram != null && mProgram.isEpisodic(), TAG, @@ -109,7 +116,7 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_RECORD_EPISODE) { getDvrManager().addSchedule(mProgram); List<ScheduledRecording> conflicts = getDvrManager().getConflictingSchedules(mProgram); @@ -139,9 +146,28 @@ public class DvrScheduleFragment extends DvrGuidedStepFragment { .build(); getDvrManager().updateSeriesRecording(seriesRecording); } + DvrUiHelper.startSeriesSettingsActivity(getContext(), - seriesRecording.getId(), null, true, true, true); + seriesRecording.getId(), null, true, true, true, + mAddCurrentProgramToSeries ? mProgram : null); dismissDialog(); } } + + @Override + public String getTrackerPrefix() { + return "DvrSmallSizedStorageErrorFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_RECORD_EPISODE) { + return "record-episode"; + } else if (actionId == ACTION_RECORD_SERIES) { + return "record-series"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java index f57e4b05..667af34a 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionActivity.java @@ -22,9 +22,6 @@ import android.support.v17.leanback.app.GuidedStepFragment; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.ui.SeriesDeletionFragment; -import com.android.tv.ui.sidepanel.SettingsFragment; /** * Activity to show details view in DVR. @@ -42,7 +39,7 @@ 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) { - SeriesDeletionFragment deletionFragment = new SeriesDeletionFragment(); + DvrSeriesDeletionFragment deletionFragment = new DvrSeriesDeletionFragment(); deletionFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, deletionFragment, R.id.dvr_settings_view_frame); } diff --git a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java index 36e3cfc1..8bf8560f 100644 --- a/src/com/android/tv/dvr/ui/SeriesDeletionFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesDeletionFragment.java @@ -33,9 +33,10 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.ui.GuidedActionsStylistWithDivider; +import com.android.tv.util.Utils; import java.util.ArrayList; import java.util.Collections; @@ -47,7 +48,7 @@ import java.util.concurrent.TimeUnit; /** * Fragment for DVR series recording settings. */ -public class SeriesDeletionFragment extends GuidedStepFragment { +public class DvrSeriesDeletionFragment extends GuidedStepFragment { private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2); // Since recordings' IDs are used as its check actions' IDs, which are random positive numbers, @@ -218,8 +219,8 @@ public class SeriesDeletionFragment extends GuidedStepFragment { private String getWatchedString(long watchedPositionMs, long durationMs) { if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) { return getResources().getString(R.string.dvr_series_watched_info_minutes, - Math.max(1, TimeUnit.MILLISECONDS.toMinutes(watchedPositionMs)), - TimeUnit.MILLISECONDS.toMinutes(durationMs)); + Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)), + Utils.getRoundOffMinsFromMs(durationMs)); } else { return getResources().getString(R.string.dvr_series_watched_info_seconds, Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)), diff --git a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java index 1173df46..2c4bb3ea 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesScheduledFragment.java @@ -25,22 +25,29 @@ import android.support.v17.leanback.widget.GuidedAction; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrDataManager; +import com.android.tv.data.Program; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.list.DvrSchedulesActivity; import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; import java.util.List; public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { + /** + * The key for program list which will be passed to {@link DvrSeriesSchedulesFragment}. + * Type: List<{@link Program}> + */ + public static final String SERIES_SCHEDULED_KEY_PROGRAMS = "series_scheduled_key_programs"; + private final static long SERIES_RECORDING_ID_NOT_SET = -1; private final static int ACTION_VIEW_SCHEDULES = 1; - private DvrScheduleManager mDvrScheduleManager; private SeriesRecording mSeriesRecording; private boolean mShowViewScheduleOption; + private List<Program> mPrograms; private int mSchedulesAddedCount = 0; private boolean mHasConflict = false; @@ -58,22 +65,25 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { } mShowViewScheduleOption = getArguments().getBoolean( DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION); - mDvrScheduleManager = TvApplication.getSingletons(context).getDvrScheduleManager(); mSeriesRecording = TvApplication.getSingletons(context).getDvrDataManager() .getSeriesRecording(seriesRecordingId); if (mSeriesRecording == null) { getActivity().finish(); return; } + mPrograms = (List<Program>) BigArguments.getArgument(SERIES_SCHEDULED_KEY_PROGRAMS); + BigArguments.reset(); mSchedulesAddedCount = TvApplication.getSingletons(getContext()).getDvrManager() .getAvailableScheduledRecording(mSeriesRecording.getId()).size(); + DvrScheduleManager dvrScheduleManager = + TvApplication.getSingletons(context).getDvrScheduleManager(); List<ScheduledRecording> conflictingRecordings = - mDvrScheduleManager.getConflictingSchedules(mSeriesRecording); + dvrScheduleManager.getConflictingSchedules(mSeriesRecording); mHasConflict = !conflictingRecordings.isEmpty(); for (ScheduledRecording recording : conflictingRecordings) { if (recording.getSeriesRecordingId() == mSeriesRecording.getId()) { ++mInThisSeriesConflictCount; - } else { + } else if (recording.getPriority() < mSeriesRecording.getPriority()) { ++mOutThisSeriesConflictCount; } } @@ -106,45 +116,63 @@ public class DvrSeriesScheduledFragment extends DvrGuidedStepFragment { } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_VIEW_SCHEDULES) { Intent intent = new Intent(getActivity(), DvrSchedulesActivity.class); intent.putExtra(DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity .TYPE_SERIES_SCHEDULE); intent.putExtra(DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, mSeriesRecording); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, mPrograms); startActivity(intent); } getActivity().finish(); } + @Override + public String getTrackerPrefix() { + return "DvrMissingStorageErrorFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_VIEW_SCHEDULES) { + return "view-schedules"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } + private String getDescription() { if (!mHasConflict) { return getResources().getQuantityString( - R.plurals.dvr_series_recording_scheduled_no_conflict, mSchedulesAddedCount, + R.plurals.dvr_series_scheduled_no_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle()); } else { // mInThisSeriesConflictCount equals 0 and mOutThisSeriesConflictCount equals 0 means // mHasConflict is false. So we don't need to check that case. if (mInThisSeriesConflictCount != 0 && mOutThisSeriesConflictCount != 0) { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_this_and_other_series_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_scheduled_this_and_other_series_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mInThisSeriesConflictCount + mOutThisSeriesConflictCount); } else if (mInThisSeriesConflictCount != 0) { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_only_this_series_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_recording_scheduled_only_this_series_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mInThisSeriesConflictCount); } else { if (mOutThisSeriesConflictCount == 1) { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_only_other_series_one_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_scheduled_only_other_series_one_conflict, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle()); } else { - return getResources().getQuantityString(R.plurals - .dvr_series_recording_scheduled_only_other_series_conflict, + return getResources().getQuantityString( + R.plurals.dvr_series_scheduled_only_other_series_many_conflicts, mSchedulesAddedCount, mSchedulesAddedCount, mSeriesRecording.getTitle(), mOutThisSeriesConflictCount); } diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java index 3f7671b3..6dd20b3a 100644 --- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsActivity.java @@ -17,7 +17,6 @@ package com.android.tv.dvr.ui; import android.app.Activity; -import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; @@ -38,25 +37,34 @@ public class DvrSeriesSettingsActivity extends Activity { /** * Name of the boolean flag to decide if the series recording with empty schedule and recording * will be removed. + * Type: boolean */ public static final String REMOVE_EMPTY_SERIES_RECORDING = "remove_empty_series_recording"; /** * Name of the boolean flag to decide if the setting fragment should be translucent. + * Type: boolean */ public static final String IS_WINDOW_TRANSLUCENT = "windows_translucent"; /** - * Name of the channel id list. If the channel list is given, we show the channels - * from the values in channel option. - * Type: Long array + * Name of the program list. The list contains the programs which belong to the series. + * Type: List<{@link com.android.tv.data.Program}> */ - public static final String CHANNEL_ID_LIST = "channel_id_list"; + public static final String PROGRAM_LIST = "program_list"; /** * Name of the boolean flag to check if the confirm dialog should show view schedule option. + * Type: boolean */ public static final String SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG = "show_view_schedule_option_in_dialog"; + /** + * Name of the current program added to series. The current program will be recorded only when + * the series recording is initialized from media controller. But for other case, the current + * program won't be recorded. + */ + public static final String CURRENT_PROGRAM = "current_program"; + @Override public void onCreate(Bundle savedInstanceState) { TvApplication.setCurrentRunningProcess(this, true); @@ -66,7 +74,7 @@ public class DvrSeriesSettingsActivity extends Activity { SoftPreconditions.checkArgument(seriesRecordingId != -1); if (savedInstanceState == null) { - SeriesSettingsFragment settingFragment = new SeriesSettingsFragment(); + DvrSeriesSettingsFragment settingFragment = new DvrSeriesSettingsFragment(); settingFragment.setArguments(getIntent().getExtras()); GuidedStepFragment.addAsRoot(this, settingFragment, R.id.dvr_settings_view_frame); } diff --git a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java index 6c05c9c6..f28382da 100644 --- a/src/com/android/tv/dvr/ui/SeriesSettingsFragment.java +++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java @@ -17,19 +17,13 @@ package com.android.tv.dvr.ui; import android.app.FragmentManager; -import android.app.ProgressDialog; import android.content.Context; import android.os.Bundle; import android.support.v17.leanback.app.GuidedStepFragment; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; import android.support.v17.leanback.widget.GuidedActionsStylist; -import android.util.Log; import android.util.LongSparseArray; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ProgressBar; import com.android.tv.R; import com.android.tv.TvApplication; @@ -38,14 +32,14 @@ import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.SeriesRecording.ChannelOption; -import com.android.tv.dvr.SeriesRecordingScheduler; -import com.android.tv.dvr.SeriesRecordingScheduler.OnSeriesRecordingUpdatedListener; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeasonEpisodeNumber; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.data.SeriesRecording.ChannelOption; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; + import java.util.ArrayList; -import java.util.Comparator; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -53,7 +47,7 @@ import java.util.Set; /** * Fragment for DVR series recording settings. */ -public class SeriesSettingsFragment extends GuidedStepFragment +public class DvrSeriesSettingsFragment extends GuidedStepFragment implements DvrDataManager.SeriesRecordingListener { private static final String TAG = "SeriesSettingsFragment"; private static final boolean DEBUG = false; @@ -66,15 +60,13 @@ public class SeriesSettingsFragment extends GuidedStepFragment private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; private DvrDataManager mDvrDataManager; - private ChannelDataManager mChannelDataManager; - private DvrManager mDvrManager; private SeriesRecording mSeriesRecording; private long mSeriesRecordingId; @ChannelOption int mChannelOption; - private Comparator<Channel> mChannelComparator; private long mSelectedChannelId; private int mBackStackCount; private boolean mShowViewScheduleOptionInDialog; + private Program mCurrentProgram; private String mFragmentTitle; private String mProrityActionTitle; @@ -84,7 +76,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment private String mChannelsActionAllText; private LongSparseArray<Channel> mId2Channel = new LongSparseArray<>(); private List<Channel> mChannels = new ArrayList<>(); - private EpisodicProgramLoadTask mEpisodicProgramLoadTask; + private List<Program> mPrograms; private GuidedAction mPriorityGuidedAction; private GuidedAction mChannelsGuidedAction; @@ -100,22 +92,24 @@ public class SeriesSettingsFragment extends GuidedStepFragment getActivity().finish(); return; } - mDvrManager = TvApplication.getSingletons(context).getDvrManager(); mShowViewScheduleOptionInDialog = getArguments().getBoolean( DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); + mCurrentProgram = getArguments().getParcelable(DvrSeriesSettingsActivity.CURRENT_PROGRAM); mDvrDataManager.addSeriesRecordingListener(this); - long[] channelIds = getArguments().getLongArray(DvrSeriesSettingsActivity.CHANNEL_ID_LIST); - mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); - if (channelIds == null) { - Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); - if (channel != null) { - mId2Channel.put(channel.getId(), channel); - mChannels.add(channel); - } - collectChannelsInBackground(); - } else { - for (long channelId : channelIds) { - Channel channel = mChannelDataManager.getChannel(channelId); + mPrograms = (List<Program>) BigArguments.getArgument( + DvrSeriesSettingsActivity.PROGRAM_LIST); + BigArguments.reset(); + if (mPrograms == null) { + getActivity().finish(); + return; + } + Set<Long> channelIds = new HashSet<>(); + ChannelDataManager channelDataManager = + TvApplication.getSingletons(context).getChannelDataManager(); + for (Program program : mPrograms) { + long channelId = program.getChannelId(); + if (channelIds.add(channelId)) { + Channel channel = channelDataManager.getChannel(channelId); if (channel != null) { mId2Channel.put(channel.getId(), channel); mChannels.add(channel); @@ -125,16 +119,14 @@ public class SeriesSettingsFragment extends GuidedStepFragment mChannelOption = mSeriesRecording.getChannelOption(); mSelectedChannelId = Channel.INVALID_ID; if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { - Channel channel = mChannelDataManager.getChannel(mSeriesRecording.getChannelId()); + Channel channel = channelDataManager.getChannel(mSeriesRecording.getChannelId()); if (channel != null) { mSelectedChannelId = channel.getId(); } else { mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; } } - mChannelComparator = new Channel.DefaultComparator(context, - TvApplication.getSingletons(context).getTvInputManagerHelper()); - mChannels.sort(mChannelComparator); + mChannels.sort(Channel.CHANNEL_NUMBER_COMPARATOR); mFragmentTitle = getString(R.string.dvr_series_settings_title); mProrityActionTitle = getString(R.string.dvr_series_settings_priority); mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest); @@ -144,23 +136,23 @@ public class SeriesSettingsFragment extends GuidedStepFragment } @Override + public void onResume() { + super.onResume(); + // To avoid the order of series's priority has changed, but series doesn't get update. + updatePriorityGuidedAction(); + } + + @Override public void onDetach() { super.onDetach(); mDvrDataManager.removeSeriesRecordingListener(this); - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - mEpisodicProgramLoadTask = null; - } } @Override public void onDestroy() { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - if (getFragmentManager().getBackStackEntryCount() == mBackStackCount - && getArguments() - .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING) - && dvrManager.canRemoveSeriesRecording(mSeriesRecordingId)) { - dvrManager.removeSeriesRecording(mSeriesRecordingId); + if (getFragmentManager().getBackStackEntryCount() == mBackStackCount && getArguments() + .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) { + mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeriesRecordingId); } super.onDestroy(); } @@ -178,7 +170,6 @@ public class SeriesSettingsFragment extends GuidedStepFragment .id(ACTION_ID_PRIORITY) .title(mProrityActionTitle) .build(); - updatePriorityGuidedAction(false); actions.add(mPriorityGuidedAction); mChannelsGuidedAction = new GuidedAction.Builder(getActivity()) @@ -204,10 +195,6 @@ public class SeriesSettingsFragment extends GuidedStepFragment public void onGuidedActionClicked(GuidedAction action) { long actionId = action.getId(); if (actionId == GuidedAction.ACTION_ID_OK) { - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); - mEpisodicProgramLoadTask = null; - } if (mChannelOption != mSeriesRecording.getChannelOption() || mSeriesRecording.isStopped() || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE @@ -218,28 +205,14 @@ public class SeriesSettingsFragment extends GuidedStepFragment if (mSelectedChannelId != Channel.INVALID_ID) { builder.setChannelId(mSelectedChannelId); } - TvApplication.getSingletons(getContext()).getDvrManager() - .updateSeriesRecording(builder.build()); - SeriesRecordingScheduler scheduler = - SeriesRecordingScheduler.getInstance(getContext()); - // Since dialog is used even after the fragment is closed, we should - // use application context. - ProgressDialog dialog = ProgressDialog.show(getContext(), null, getString( - R.string.dvr_series_schedules_progress_message_updating_programs)); - scheduler.addOnSeriesRecordingUpdatedListener( - new OnSeriesRecordingUpdatedListener() { - @Override - public void onSeriesRecordingUpdated(SeriesRecording... seriesRecordings) { - for (SeriesRecording seriesRecording : seriesRecordings) { - if (seriesRecording.getId() == mSeriesRecordingId) { - dialog.dismiss(); - scheduler.removeOnSeriesRecordingUpdatedListener(this); - showConfirmDialog(); - return; - } - } - } - }); + DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + dvrManager.updateSeriesRecording(builder.build()); + if (mCurrentProgram != null && (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL + || mSelectedChannelId == mCurrentProgram.getChannelId())) { + dvrManager.addSchedule(mCurrentProgram); + } + updateSchedulesToSeries(); + showConfirmDialog(); } else { showConfirmDialog(); } @@ -247,9 +220,9 @@ public class SeriesSettingsFragment extends GuidedStepFragment finishGuidedStepFragments(); } else if (actionId == ACTION_ID_PRIORITY) { FragmentManager fragmentManager = getFragmentManager(); - PrioritySettingsFragment fragment = new PrioritySettingsFragment(); + DvrPrioritySettingsFragment fragment = new DvrPrioritySettingsFragment(); Bundle args = new Bundle(); - args.putLong(PrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, + args.putLong(DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, mSeriesRecording.getId()); fragment.setArguments(args); GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame); @@ -281,7 +254,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment private void updateChannelsGuidedAction(boolean notifyActionChanged) { if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { mChannelsGuidedAction.setDescription(mChannelsActionAllText); - } else { + } else if (mId2Channel.get(mSelectedChannelId) != null){ mChannelsGuidedAction.setDescription(mId2Channel.get(mSelectedChannelId) .getDisplayText()); } @@ -290,7 +263,7 @@ public class SeriesSettingsFragment extends GuidedStepFragment } } - private void updatePriorityGuidedAction(boolean notifyActionChanged) { + private void updatePriorityGuidedAction() { int totalSeriesCount = 0; int priorityOrder = 0; for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) { @@ -312,49 +285,38 @@ public class SeriesSettingsFragment extends GuidedStepFragment mPriorityGuidedAction.setDescription(getString( R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); } - if (notifyActionChanged) { - notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); - } + notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); } - private void collectChannelsInBackground() { - if (mEpisodicProgramLoadTask != null) { - mEpisodicProgramLoadTask.cancel(true); + private void updateSchedulesToSeries() { + List<Program> recordingCandidates = new ArrayList<>(); + Set<SeasonEpisodeNumber> scheduledEpisodes = new HashSet<>(); + for (ScheduledRecording r : mDvrDataManager.getScheduledRecordings(mSeriesRecordingId)) { + if (r.getState() != ScheduledRecording.STATE_RECORDING_FAILED + && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { + scheduledEpisodes.add(new SeasonEpisodeNumber( + r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber())); + } } - mEpisodicProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { - @Override - protected void onPostExecute(List<Program> programs) { - mEpisodicProgramLoadTask = null; - Set<Long> channelIds = new HashSet<>(); - for (Program program : programs) { - channelIds.add(program.getChannelId()); - } - boolean channelAdded = false; - for (Long channelId : channelIds) { - if (mId2Channel.get(channelId) != null) { - continue; - } - Channel channel = mChannelDataManager.getChannel(channelId); - if (channel != null) { - channelAdded = true; - mId2Channel.put(channelId, channel); - mChannels.add(channel); - if (DEBUG) Log.d(TAG, "Added channel: " + channel); - } - } - if (!channelAdded) { - return; - } - mChannels.sort(mChannelComparator); - mChannelsGuidedAction.setSubActions(buildChannelSubAction()); - notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); - if (DEBUG) Log.d(TAG, "Complete EpisodicProgramLoadTask"); + for (Program program : mPrograms) { + // Removes current programs and scheduled episodes out, matches the channel option. + if (program.getStartTimeUtcMillis() >= System.currentTimeMillis() + && mSeriesRecording.matchProgram(program) + && !scheduledEpisodes.contains(new SeasonEpisodeNumber( + mSeriesRecordingId, program.getSeasonNumber(), program.getEpisodeNumber()))) { + recordingCandidates.add(program); } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true); - mEpisodicProgramLoadTask.execute(); + } + if (recordingCandidates.isEmpty()) { + return; + } + List<Program> programsToSchedule = SeriesRecordingScheduler.pickOneProgramPerEpisode( + mDvrDataManager, Collections.singletonList(mSeriesRecording), recordingCandidates) + .get(mSeriesRecordingId); + if (!programsToSchedule.isEmpty()) { + TvApplication.getSingletons(getContext()).getDvrManager() + .addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule); + } } private List<GuidedAction> buildChannelSubAction() { @@ -373,8 +335,8 @@ public class SeriesSettingsFragment extends GuidedStepFragment } private void showConfirmDialog() { - DvrUiHelper.StartSeriesScheduledDialogActivity( - getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog); + DvrUiHelper.StartSeriesScheduledDialogActivity(getContext(), mSeriesRecording, + mShowViewScheduleOptionInDialog, mPrograms); finishGuidedStepFragments(); } @@ -382,16 +344,23 @@ public class SeriesSettingsFragment extends GuidedStepFragment public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { } @Override - public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { } + public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { + for (SeriesRecording series : seriesRecordings) { + if (series.getId() == mSeriesRecording.getId()) { + finishGuidedStepFragments(); + return; + } + } + } @Override public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { for (SeriesRecording seriesRecording : seriesRecordings) { if (seriesRecording.getId() == mSeriesRecordingId) { mSeriesRecording = seriesRecording; - updatePriorityGuidedAction(true); + updatePriorityGuidedAction(); return; } } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java index c3867886..baa45793 100644 --- a/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopRecordingFragment.java @@ -25,15 +25,12 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.v17.leanback.widget.GuidanceStylist.Guidance; import android.support.v17.leanback.widget.GuidedAction; -import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -131,15 +128,8 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { String title = getString(R.string.dvr_stop_recording_dialog_title); String description; if (mStopReason == REASON_ON_CONFLICT) { - String programTitle = mSchedule.getProgramTitle(); - if (TextUtils.isEmpty(programTitle)) { - ChannelDataManager channelDataManager = - TvApplication.getSingletons(getActivity()).getChannelDataManager(); - Channel channel = channelDataManager.getChannel(mSchedule.getChannelId()); - programTitle = channel.getDisplayName(); - } description = getString(R.string.dvr_stop_recording_dialog_description_on_conflict, - mSchedule.getProgramTitle()); + mSchedule.getProgramDisplayTitle(getContext())); } else { description = getString(R.string.dvr_stop_recording_dialog_description); } @@ -158,4 +148,19 @@ public class DvrStopRecordingFragment extends DvrGuidedStepFragment { .clickAction(GuidedAction.ACTION_ID_CANCEL) .build()); } + + @Override + public String getTrackerPrefix() { + return "DvrStopRecordingFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == ACTION_STOP) { + return "stop"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java index feaa2357..7b56cfc1 100644 --- a/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java +++ b/src/com/android/tv/dvr/ui/DvrStopSeriesRecordingFragment.java @@ -31,8 +31,8 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import java.util.ArrayList; import java.util.List; @@ -78,7 +78,7 @@ public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment { } @Override - public void onGuidedActionClicked(GuidedAction action) { + public void onTrackedGuidedActionClicked(GuidedAction action) { if (action.getId() == ACTION_STOP_SERIES_RECORDING) { ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); DvrManager dvrManager = singletons.getDvrManager(); @@ -101,4 +101,18 @@ public class DvrStopSeriesRecordingFragment extends DvrGuidedStepFragment { } dismissDialog(); } + + @Override + public String getTrackerPrefix() { + return "DvrStopSeriesRecordingFragment"; + } + + @Override + public String getTrackerLabelForGuidedAction(GuidedAction action) { + if (action.getId() == ACTION_STOP_SERIES_RECORDING) { + return "stop"; + } else { + return super.getTrackerLabelForGuidedAction(action); + } + } } diff --git a/src/com/android/tv/dvr/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java index c0d3b0c5..302fd6cd 100644 --- a/src/com/android/tv/dvr/DvrUiHelper.java +++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java @@ -14,19 +14,27 @@ * limitations under the License. */ -package com.android.tv.dvr; +package com.android.tv.dvr.ui; import android.annotation.TargetApi; import android.app.Activity; +import android.app.ProgressDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.media.tv.TvInputManager; import android.os.Build; import android.os.Bundle; import android.support.annotation.MainThread; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityOptionsCompat; +import android.text.Html; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.text.style.TextAppearanceSpan; import android.widget.ImageView; import android.widget.Toast; @@ -34,34 +42,40 @@ import com.android.tv.MainActivity; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; +import com.android.tv.data.BaseProgram; import com.android.tv.data.Channel; import com.android.tv.data.Program; -import com.android.tv.dvr.ui.DvrDetailsActivity; -import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment; +import com.android.tv.dialog.HalfSizedDialogFragment; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.DvrStorageStatusManager; +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.provider.EpisodicProgramLoadTask; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment; 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.DvrInsufficientSpaceErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment; +import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment; import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment; -import com.android.tv.dvr.ui.DvrSchedulesActivity; -import com.android.tv.dvr.ui.DvrSeriesDeletionActivity; -import com.android.tv.dvr.ui.DvrSeriesScheduledDialogActivity; -import com.android.tv.dvr.ui.DvrSeriesSettingsActivity; -import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.DvrStopSeriesRecordingDialogFragment; -import com.android.tv.dvr.ui.DvrStopSeriesRecordingFragment; -import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.dvr.ui.browse.DvrBrowseActivity; +import com.android.tv.dvr.ui.browse.DvrDetailsActivity; +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.util.ToastUtils; import com.android.tv.util.Utils; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; /** * A helper class for DVR UI. @@ -69,94 +83,54 @@ import java.util.List; @MainThread @TargetApi(Build.VERSION_CODES.N) public class DvrUiHelper { - /** - * Handles the action to create the new schedule. It returns {@code true} if the schedule is - * added and there's no additional UI, otherwise {@code false}. - */ - public static boolean handleCreateSchedule(MainActivity activity, Program program) { - if (program == null) { - return false; - } - DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager(); - if (!program.isEpisodic()) { - // One time recording. - dvrManager.addSchedule(program); - if (!dvrManager.getConflictingSchedules(program).isEmpty()) { - DvrUiHelper.showScheduleConflictDialog(activity, program); - return false; - } - } else { - SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); - if (seriesRecording == null || seriesRecording.isStopped()) { - DvrUiHelper.showScheduleDialog(activity, program); - return false; - } else { - // Show recorded program rather than the schedule. - RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(), - program.getSeasonNumber(), program.getEpisodeNumber()); - if (recordedProgram != null) { - DvrUiHelper.showAlreadyRecordedDialog(activity, program); - return false; - } - ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(), - program.getSeasonNumber(), program.getEpisodeNumber()); - if (duplicate != null - && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED - || duplicate.getState() - == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { - DvrUiHelper.showAlreadyScheduleDialog(activity, program); - return false; - } - // Just add the schedule. - dvrManager.addSchedule(program); - } - } - return true; + private static final String TAG = "DvrUiHelper"; - } + private static ProgressDialog sProgressDialog = null; /** * Checks if the storage status is good for recording and shows error messages if needed. * - * @return true if the storage status is fine to be recorded for {@code inputId}. + * @param recordingRequestRunnable if the storage status is OK to record or users choose to + * perform the operation anyway, this Runnable will run. */ - public static boolean checkStorageStatusAndShowErrorMessage(Activity activity, String inputId) { - if (!Utils.isBundledInput(inputId)) { - return true; - } - DvrStorageStatusManager dvrStorageStatusManager = - TvApplication.getSingletons(activity).getDvrStorageStatusManager(); - int status = dvrStorageStatusManager.getDvrStorageStatus(); - if (status == DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL) { - showDvrSmallSizedStorageErrorDialog(activity); - return false; - } else if (status == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { - showDvrMissingStorageErrorDialog(activity, inputId); - return false; - } else if (status == DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT) { - // TODO: handle insufficient storage case. - return true; - } else { - return true; + public static void checkStorageStatusAndShowErrorMessage(Activity activity, String inputId, + Runnable recordingRequestRunnable) { + if (Utils.isBundledInput(inputId)) { + switch (TvApplication.getSingletons(activity).getDvrStorageStatusManager() + .getDvrStorageStatus()) { + case DvrStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL: + showDvrSmallSizedStorageErrorDialog(activity); + return; + case DvrStorageStatusManager.STORAGE_STATUS_MISSING: + showDvrMissingStorageErrorDialog(activity); + return; + case DvrStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT: + showDvrNoFreeSpaceErrorDialog(activity, recordingRequestRunnable); + return; + } } + recordingRequestRunnable.run(); } /** * Shows the schedule dialog. */ - public static void showScheduleDialog(MainActivity activity, Program program) { + public static void showScheduleDialog(Activity activity, Program program, + boolean addCurrentProgramToSeries) { if (SoftPreconditions.checkNotNull(program) == null) { return; } Bundle args = new Bundle(); args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program); + args.putBoolean(DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES, + addCurrentProgramToSeries); showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true); } /** * Shows the recording duration options dialog. */ - public static void showChannelRecordDurationOptions(MainActivity activity, Channel channel) { + public static void showChannelRecordDurationOptions(Activity activity, Channel channel) { if (SoftPreconditions.checkNotNull(channel) == null) { return; } @@ -168,7 +142,7 @@ public class DvrUiHelper { /** * Shows the dialog which says that the new schedule conflicts with others. */ - public static void showScheduleConflictDialog(MainActivity activity, Program program) { + public static void showScheduleConflictDialog(Activity activity, Program program) { if (program == null) { return; } @@ -192,20 +166,47 @@ public class DvrUiHelper { /** * Shows DVR insufficient space error dialog. */ - public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity) { - showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), null); + public static void showDvrInsufficientSpaceErrorDialog(MainActivity activity, + Set<String> failedScheduledRecordingInfoSet) { + Bundle args = new Bundle(); + ArrayList<String> failedScheduledRecordingInfoArray = + new ArrayList<>(failedScheduledRecordingInfoSet); + args.putStringArrayList(DvrInsufficientSpaceErrorFragment.FAILED_SCHEDULED_RECORDING_INFOS, + failedScheduledRecordingInfoArray); + showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), args); Utils.clearRecordingFailedReason(activity, TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); + Utils.clearFailedScheduledRecordingInfoSet(activity); + } + + /** + * Shows DVR no free space error dialog. + * + * @param recordingRequestRunnable the recording request to be executed when users choose + * {@link DvrGuidedStepFragment#ACTION_RECORD_ANYWAY}. + */ + public static void showDvrNoFreeSpaceErrorDialog(Activity activity, + Runnable recordingRequestRunnable) { + DvrHalfSizedDialogFragment fragment = new DvrNoFreeSpaceErrorDialogFragment(); + fragment.setOnActionClickListener(new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrGuidedStepFragment.ACTION_RECORD_ANYWAY) { + recordingRequestRunnable.run(); + } else if (actionId == DvrGuidedStepFragment.ACTION_DELETE_RECORDINGS) { + Intent intent = new Intent(activity, DvrBrowseActivity.class); + activity.startActivity(intent); + } + } + }); + showDialogFragment(activity, fragment, null); } /** * Shows DVR missing storage error dialog. */ - private static void showDvrMissingStorageErrorDialog(Activity activity, String inputId) { - SoftPreconditions.checkArgument(!TextUtils.isEmpty(inputId)); - Bundle args = new Bundle(); - args.putString(DvrHalfSizedDialogFragment.KEY_INPUT_ID, inputId); - showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), args); + private static void showDvrMissingStorageErrorDialog(Activity activity) { + showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), null); } /** @@ -231,7 +232,7 @@ public class DvrUiHelper { /** * Shows "already scheduled" dialog. */ - public static void showAlreadyScheduleDialog(MainActivity activity, Program program) { + public static void showAlreadyScheduleDialog(Activity activity, Program program) { if (program == null) { return; } @@ -243,7 +244,7 @@ public class DvrUiHelper { /** * Shows "already recorded" dialog. */ - public static void showAlreadyRecordedDialog(MainActivity activity, Program program) { + public static void showAlreadyRecordedDialog(Activity activity, Program program) { if (program == null) { return; } @@ -252,6 +253,87 @@ public class DvrUiHelper { showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true); } + /** + * Handle the request of recording a current program. It will handle creating schedules and + * shows the proper dialog and toast message respectively for timed-recording and program + * recording cases. + * + * @param addProgramToSeries denotes whether the program to be recorded should be added into + * the series recording when users choose to record the entire series. + */ + public static void requestRecordingCurrentProgram(Activity activity, + Channel channel, Program program, boolean addProgramToSeries) { + if (program == null) { + DvrUiHelper.showChannelRecordDurationOptions(activity, channel); + } else if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { + String msg = activity.getString(R.string.dvr_msg_current_program_scheduled, + program.getTitle(), Utils.toTimeString(program.getEndTimeUtcMillis(), false)); + Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show(); + } + } + + /** + * Handle the request of recording a future program. It will handle creating schedules and + * shows the proper toast message. + * + * @param addProgramToSeries denotes whether the program to be recorded should be added into + * the series recording when users choose to record the entire series. + */ + public static void requestRecordingFutureProgram(Activity activity, + Program program, boolean addProgramToSeries) { + if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) { + String msg = activity.getString( + R.string.dvr_msg_program_scheduled, program.getTitle()); + ToastUtils.show(activity, msg, Toast.LENGTH_SHORT); + } + } + + /** + * Handles the action to create the new schedule. It returns {@code true} if the schedule is + * added and there's no additional UI, otherwise {@code false}. + */ + private static boolean handleCreateSchedule(Activity activity, Program program, + boolean addProgramToSeries) { + if (program == null) { + return false; + } + DvrManager dvrManager = TvApplication.getSingletons(activity).getDvrManager(); + if (!program.isEpisodic()) { + // One time recording. + dvrManager.addSchedule(program); + if (!dvrManager.getConflictingSchedules(program).isEmpty()) { + DvrUiHelper.showScheduleConflictDialog(activity, program); + return false; + } + } else { + // Show recorded program rather than the schedule. + RecordedProgram recordedProgram = dvrManager.getRecordedProgram(program.getTitle(), + program.getSeasonNumber(), program.getEpisodeNumber()); + if (recordedProgram != null) { + DvrUiHelper.showAlreadyRecordedDialog(activity, program); + return false; + } + ScheduledRecording duplicate = dvrManager.getScheduledRecording(program.getTitle(), + program.getSeasonNumber(), program.getEpisodeNumber()); + if (duplicate != null + && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || duplicate.getState() + == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) { + DvrUiHelper.showAlreadyScheduleDialog(activity, program); + return false; + } + SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program); + if (seriesRecording == null || seriesRecording.isStopped()) { + DvrUiHelper.showScheduleDialog(activity, program, addProgramToSeries); + return false; + } else { + // Just add the schedule. + dvrManager.addSchedule(program); + } + } + return true; + } + private static void showDialogFragment(Activity activity, DvrHalfSizedDialogFragment dialogFragment, Bundle args) { showDialogFragment(activity, dialogFragment, args, false, false); @@ -291,6 +373,25 @@ public class DvrUiHelper { } /** + * Launches DVR playback activity for the give recorded program. + * + * @param programId the ID of the recorded program going to be played. + * @param seekTimeMs the seek position to initial playback. + * @param pinChecked {@code true} if the pin code for parental controls has already been + * verified, otherwise {@code false}. + */ + public static void startPlaybackActivity(Context context, long programId, + long seekTimeMs, boolean pinChecked) { + Intent intent = new Intent(context, DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); + if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); + } + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); + context.startActivity(intent); + } + + /** * Shows the schedules activity to resolve the tune conflict. */ public static void startSchedulesActivityForTuneConflict(Context context, Channel channel) { @@ -341,19 +442,66 @@ public class DvrUiHelper { /** * Shows the series settings activity. * - * @param channelIds Channel ID list which has programs belonging to the series. + * @param programs list of programs which belong to the series. */ public static void startSeriesSettingsActivity(Context context, long seriesRecordingId, - @Nullable long[] channelIds, boolean removeEmptySeriesSchedule, - boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog) { + @Nullable List<Program> programs, boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, + Program currentProgram) { + SeriesRecording series = TvApplication.getSingletons(context).getDvrDataManager() + .getSeriesRecording(seriesRecordingId); + if (series == null) { + return; + } + if (programs != null) { + startSeriesSettingsActivityInternal(context, seriesRecordingId, programs, + removeEmptySeriesSchedule, isWindowTranslucent, + showViewScheduleOptionInDialog, currentProgram); + } else { + EpisodicProgramLoadTask episodicProgramLoadTask = + new EpisodicProgramLoadTask(context, series) { + @Override + protected void onPostExecute(List<Program> loadedPrograms) { + sProgressDialog.dismiss(); + sProgressDialog = null; + startSeriesSettingsActivityInternal(context, seriesRecordingId, + loadedPrograms == null ? Collections.EMPTY_LIST : loadedPrograms, + removeEmptySeriesSchedule, isWindowTranslucent, + showViewScheduleOptionInDialog, currentProgram); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true); + sProgressDialog = ProgressDialog.show(context, null, context.getString( + R.string.dvr_series_progress_message_reading_programs), true, true, + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + episodicProgramLoadTask.cancel(true); + sProgressDialog = null; + } + }); + episodicProgramLoadTask.execute(); + } + } + + private static void startSeriesSettingsActivityInternal(Context context, long seriesRecordingId, + @NonNull List<Program> programs, boolean removeEmptySeriesSchedule, + boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, + Program currentProgram) { + SoftPreconditions.checkState(programs != null, + TAG, "Start series settings activity but programs is null"); Intent intent = new Intent(context, DvrSeriesSettingsActivity.class); intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId); - intent.putExtra(DvrSeriesSettingsActivity.CHANNEL_ID_LIST, channelIds); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesSettingsActivity.PROGRAM_LIST, programs); intent.putExtra(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING, removeEmptySeriesSchedule); intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent); intent.putExtra(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG, showViewScheduleOptionInDialog); + intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram); context.startActivity(intent); } @@ -361,7 +509,8 @@ public class DvrUiHelper { * Shows "series recording scheduled" dialog activity. */ public static void StartSeriesScheduledDialogActivity(Context context, - SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog) { + SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog, + List<Program> programs) { if (seriesRecording == null) { return; } @@ -370,6 +519,9 @@ public class DvrUiHelper { seriesRecording.getId()); intent.putExtra(DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION, showViewScheduleOptionInDialog); + BigArguments.reset(); + BigArguments.setArgument(DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS, + programs); context.startActivity(intent); } @@ -447,4 +599,51 @@ public class DvrUiHelper { Utils.toTimeString(endTimeMs, false)); Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); } -} + + /** + * Returns the styled schedule's title with its season and episode number. + */ + public static CharSequence getStyledTitleWithEpisodeNumber(Context context, + ScheduledRecording schedule, int episodeNumberStyleResId) { + return getStyledTitleWithEpisodeNumber(context, schedule.getProgramTitle(), + schedule.getSeasonNumber(), schedule.getEpisodeNumber(), episodeNumberStyleResId); + } + + /** + * Returns the styled program's title with its season and episode number. + */ + public static CharSequence getStyledTitleWithEpisodeNumber(Context context, + BaseProgram program, int episodeNumberStyleResId) { + return getStyledTitleWithEpisodeNumber(context, program.getTitle(), + program.getSeasonNumber(), program.getEpisodeNumber(), episodeNumberStyleResId); + } + + @NonNull + public static CharSequence getStyledTitleWithEpisodeNumber(Context context, String title, + String seasonNumber, String episodeNumber, int episodeNumberStyleResId) { + if (TextUtils.isEmpty(title)) { + return ""; + } + SpannableStringBuilder builder; + if (TextUtils.isEmpty(seasonNumber) || seasonNumber.equals("0")) { + builder = TextUtils.isEmpty(episodeNumber) ? new SpannableStringBuilder(title) : + new SpannableStringBuilder(Html.fromHtml( + context.getString(R.string.program_title_with_episode_number_no_season, + title, episodeNumber))); + } else { + builder = new SpannableStringBuilder(Html.fromHtml( + context.getString(R.string.program_title_with_episode_number, + title, seasonNumber, episodeNumber))); + } + Object[] spans = builder.getSpans(0, builder.length(), Object.class); + if (spans.length > 0) { + if (episodeNumberStyleResId != 0) { + builder.setSpan(new TextAppearanceSpan(context, episodeNumberStyleResId), + builder.getSpanStart(spans[0]), builder.getSpanEnd(spans[0]), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + builder.removeSpan(spans[0]); + } + return new SpannableString(builder); + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/FadeBackground.java b/src/com/android/tv/dvr/ui/FadeBackground.java new file mode 100644 index 00000000..4f06ebcf --- /dev/null +++ b/src/com/android/tv/dvr/ui/FadeBackground.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.transition.Transition; +import android.transition.TransitionValues; +import android.transition.Visibility; +import android.util.AttributeSet; +import android.view.ViewGroup; + +import com.android.tv.R; + +/** + * This transition fades in/out of the background of the view by changing the background color. + */ +public class FadeBackground extends Transition { + private final int mMode; + + public FadeBackground(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadeBackground); + mMode = a.getInt(R.styleable.FadeBackground_fadingMode, Visibility.MODE_IN); + a.recycle(); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { } + + @Override + public void captureEndValues(TransitionValues transitionValues) { } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (startValues == null || endValues == null) { + return null; + } + Drawable background = endValues.view.getBackground(); + if (background instanceof ColorDrawable) { + int color = ((ColorDrawable) background).getColor(); + int transparentColor = Color.argb(0, Color.red(color), Color.green(color), + Color.blue(color)); + return mMode == Visibility.MODE_OUT + ? ObjectAnimator.ofArgb(background, "color", transparentColor) + : ObjectAnimator.ofArgb(background, "color", transparentColor, color); + } + return null; + } +} diff --git a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java b/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java deleted file mode 100644 index d320816e..00000000 --- a/src/com/android/tv/dvr/ui/HalfSizedDialogFragment.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.content.DialogInterface; -import android.os.Bundle; -import android.os.Handler; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.dialog.SafeDismissDialogFragment; - -import java.util.concurrent.TimeUnit; - -public class HalfSizedDialogFragment extends SafeDismissDialogFragment { - public static final String DIALOG_TAG = HalfSizedDialogFragment.class.getSimpleName(); - public static final String TRACKER_LABEL = "Half sized dialog"; - - private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(30); - - private OnActionClickListener mOnActionClickListener; - - private Handler mHandler = new Handler(); - private Runnable mAutoDismisser = new Runnable() { - @Override - public void run() { - dismiss(); - } - }; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.halfsized_dialog, container, false); - } - - @Override - public void onStart() { - super.onStart(); - getDialog().setOnKeyListener(new DialogInterface.OnKeyListener() { - public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent keyEvent) { - mHandler.removeCallbacks(mAutoDismisser); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - return false; - } - }); - mHandler.postDelayed(mAutoDismisser, AUTO_DISMISS_TIME_THRESHOLD_MS); - } - - @Override - public void onPause() { - super.onPause(); - if (mOnActionClickListener != null) { - // Dismisses the dialog to prevent the callback being forgotten during - // fragment re-creating. - dismiss(); - } - } - - @Override - public void onStop() { - super.onStop(); - mHandler.removeCallbacks(mAutoDismisser); - } - - @Override - public int getTheme() { - return R.style.Theme_TV_dialog_HalfSizedDialog; - } - - @Override - public String getTrackerLabel() { - return TRACKER_LABEL; - } - - /** - * Sets {@link OnActionClickListener} for the dialog fragment. If listener is set, the dialog - * will be automatically closed when it's paused to prevent the fragment being re-created by - * the framework, which will result the listener being forgotten. - */ - public void setOnActionClickListener(OnActionClickListener listener) { - mOnActionClickListener = listener; - } - - /** - * Returns {@link OnActionClickListener} for sub-classes or any inner fragments. - */ - protected OnActionClickListener getOnActionClickListener() { - return mOnActionClickListener; - } - - /** - * An interface to provide callbacks for half-sized dialogs. Subclasses or inner fragments - * should invoke {@link OnActionClickListener#onActionClick(long)} and provide the identifier - * of the action user clicked. - */ - public interface OnActionClickListener { - void onActionClick(long actionId); - } -}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java deleted file mode 100644 index 1bf34310..00000000 --- a/src/com/android/tv/dvr/ui/RecordedProgramPresenter.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.content.Context; -import android.media.tv.TvContract; -import android.media.tv.TvInputManager; -import android.net.Uri; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; -import com.android.tv.util.Utils; - -import java.util.concurrent.TimeUnit; - -/** - * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. - */ -public class RecordedProgramPresenter extends DvrItemPresenter { - private final ChannelDataManager mChannelDataManager; - private final DvrWatchedPositionManager mDvrWatchedPositionManager; - private final Context mContext; - private String mTodayString; - private String mYesterdayString; - private final int mProgressBarColor; - private final boolean mShowEpisodeTitle; - - private static final class RecordedProgramViewHolder extends ViewHolder - implements WatchedPositionChangedListener { - private RecordedProgram mProgram; - - RecordedProgramViewHolder(RecordingCardView view, int progressColor) { - super(view); - view.setProgressBarColor(progressColor); - } - - private void setProgram(RecordedProgram program) { - mProgram = program; - } - - private void setProgressBar(long watchedPositionMs) { - ((RecordingCardView) view).setProgressBar( - (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) ? null - : Math.min(100, (int) (100.0f * watchedPositionMs - / mProgram.getDurationMillis()))); - } - - @Override - public void onWatchedPositionChanged(long programId, long positionMs) { - if (programId == mProgram.getId()) { - setProgressBar(positionMs); - } - } - } - - public RecordedProgramPresenter(Context context, boolean showEpisodeTitle) { - mContext = context; - mChannelDataManager = TvApplication.getSingletons(context).getChannelDataManager(); - mTodayString = context.getString(R.string.dvr_date_today); - mYesterdayString = context.getString(R.string.dvr_date_yesterday); - mDvrWatchedPositionManager = - TvApplication.getSingletons(context).getDvrWatchedPositionManager(); - mProgressBarColor = context.getResources() - .getColor(R.color.play_controls_progress_bar_watched); - mShowEpisodeTitle = showEpisodeTitle; - } - - public RecordedProgramPresenter(Context context) { - this(context, false); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - RecordingCardView view = new RecordingCardView(mContext); - return new RecordedProgramViewHolder(view, mProgressBarColor); - } - - @Override - public void onBindViewHolder(ViewHolder viewHolder, Object o) { - final RecordedProgram program = (RecordedProgram) o; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - Channel channel = mChannelDataManager.getChannel(program.getChannelId()); - String titleString = mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext) - : program.getTitleWithEpisodeNumber(mContext); - SpannableString title = titleString == null ? null : new SpannableString(titleString); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : mContext.getResources().getString(R.string.no_program_information)); - } else if (!mShowEpisodeTitle) { - // TODO: Some translation may add delimiters in-between program titles, we should use - // a more robust way to get the span range. - String programTitle = program.getTitle(); - title.setSpan(new TextAppearanceSpan(mContext, - R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 - : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - cardView.setTitle(title); - String imageUri = null; - boolean isChannelLogo = false; - if (program.getPosterArtUri() != null) { - imageUri = program.getPosterArtUri(); - } else if (program.getThumbnailUri() != null) { - imageUri = program.getThumbnailUri(); - } else if (channel != null) { - imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); - isChannelLogo = true; - } - cardView.setImageUri(imageUri, isChannelLogo); - int durationMinutes = - Math.max(1, (int) TimeUnit.MILLISECONDS.toMinutes(program.getDurationMillis())); - String durationString = getContext().getResources().getQuantityString( - R.plurals.dvr_program_duration, durationMinutes, durationMinutes); - cardView.setContent(getDescription(program), durationString); - if (viewHolder instanceof RecordedProgramViewHolder) { - RecordedProgramViewHolder cardViewHolder = (RecordedProgramViewHolder) viewHolder; - cardViewHolder.setProgram(program); - mDvrWatchedPositionManager.addListener(cardViewHolder, program.getId()); - cardViewHolder - .setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId())); - } - super.onBindViewHolder(viewHolder, o); - } - - @Override - public void onUnbindViewHolder(ViewHolder viewHolder) { - if (viewHolder instanceof RecordedProgramViewHolder) { - mDvrWatchedPositionManager.removeListener((RecordedProgramViewHolder) viewHolder, - ((RecordedProgramViewHolder) viewHolder).mProgram.getId()); - } - ((RecordingCardView) viewHolder.view).reset(); - super.onUnbindViewHolder(viewHolder); - } - - /** - * Returns description would be used in its card view. - */ - protected String getDescription(RecordedProgram recording) { - int dateDifference = Utils.computeDateDifference(recording.getStartTimeUtcMillis(), - System.currentTimeMillis()); - if (dateDifference == 0) { - return mTodayString; - } else if (dateDifference == 1) { - return mYesterdayString; - } else { - return Utils.getDurationString(mContext, recording.getStartTimeUtcMillis(), - recording.getStartTimeUtcMillis(), false, true, false, 0); - } - } - - /** - * Returns context. - */ - protected Context getContext() { - return mContext; - } -} diff --git a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java deleted file mode 100644 index 4e19ec3f..00000000 --- a/src/com/android/tv/dvr/ui/RecordingDetailsFragment.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.dvr.ui; - -import android.os.Bundle; -import android.support.v17.leanback.app.DetailsFragment; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; - -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.dvr.ScheduledRecording; - -/** - * {@link DetailsFragment} for recordings in DVR. - */ -abstract class RecordingDetailsFragment extends DvrDetailsFragment { - private ScheduledRecording mRecording; - - @Override - protected void onCreateInternal() { - setDetailsOverviewRow(createDetailsContent()); - } - - @Override - protected boolean onLoadRecordingDetails(Bundle args) { - long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); - mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager() - .getScheduledRecording(scheduledRecordingId); - return mRecording != null; - } - - /** - * Returns {@link ScheduledRecording} for the current fragment. - */ - public ScheduledRecording getRecording() { - return mRecording; - } - - private DetailsContent createDetailsContent() { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(mRecording.getChannelId()); - SpannableString title = mRecording.getProgramTitleWithEpisodeNumber(getContext()) == null ? - null : new SpannableString(mRecording - .getProgramTitleWithEpisodeNumber(getContext())); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : getContext().getResources().getString( - R.string.no_program_information)); - } else { - String programTitle = mRecording.getProgramTitle(); - title.setSpan(new TextAppearanceSpan(getContext(), - R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 - : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - String description = !TextUtils.isEmpty(mRecording.getProgramDescription()) ? - mRecording.getProgramDescription() : mRecording.getProgramLongDescription(); - if (TextUtils.isEmpty(description)) { - description = channel != null ? channel.getDescription() : null; - } - return new DetailsContent.Builder() - .setTitle(title) - .setStartTimeUtcMillis(mRecording.getStartTimeMs()) - .setEndTimeUtcMillis(mRecording.getEndTimeMs()) - .setDescription(description) - .setImageUris(mRecording.getProgramPosterArtUri(), - mRecording.getProgramThumbnailUri(), channel) - .build(); - } -} diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java deleted file mode 100644 index 5f447f13..00000000 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingPresenter.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.dvr.ui; - -import android.app.Activity; -import android.content.Context; -import android.media.tv.TvContract; -import android.os.Handler; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; -import android.view.ViewGroup; - -import com.android.tv.ApplicationSingletons; -import com.android.tv.R; -import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.util.Utils; - -import java.util.concurrent.TimeUnit; - -/** - * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. - */ -public class ScheduledRecordingPresenter extends DvrItemPresenter { - private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); - - private final ChannelDataManager mChannelDataManager; - private final DvrManager mDvrManager; - private final Context mContext; - private final int mProgressBarColor; - - private static final class ScheduledRecordingViewHolder extends ViewHolder { - private final Handler mHandler = new Handler(); - private ScheduledRecording mScheduledRecording; - private final Runnable mProgressBarUpdater = new Runnable() { - @Override - public void run() { - updateProgressBar(); - mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS); - } - }; - - ScheduledRecordingViewHolder(RecordingCardView view, int progressBarColor) { - super(view); - view.setProgressBarColor(progressBarColor); - } - - private void updateProgressBar() { - if (mScheduledRecording == null) { - return; - } - int recordingState = mScheduledRecording.getState(); - RecordingCardView cardView = (RecordingCardView) view; - if (recordingState == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { - cardView.setProgressBar(Math.max(0, Math.min((int) (100 * - (System.currentTimeMillis() - mScheduledRecording.getStartTimeMs()) - / mScheduledRecording.getDuration()), 100))); - } else if (recordingState == ScheduledRecording.STATE_RECORDING_FINISHED) { - cardView.setProgressBar(100); - } else { - // Hides progress bar. - cardView.setProgressBar(null); - } - } - - private void startUpdateProgressBar() { - mHandler.post(mProgressBarUpdater); - } - - private void stopUpdateProgressBar() { - mHandler.removeCallbacks(mProgressBarUpdater); - } - } - - public ScheduledRecordingPresenter(Context context) { - ApplicationSingletons singletons = TvApplication.getSingletons(context); - mChannelDataManager = singletons.getChannelDataManager(); - mDvrManager = singletons.getDvrManager(); - mContext = context; - mProgressBarColor = context.getResources() - .getColor(R.color.play_controls_recording_icon_color_on_focus); - } - - @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); - return new ScheduledRecordingViewHolder(view, mProgressBarColor); - } - - @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { - final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final ScheduledRecording recording = (ScheduledRecording) o; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - final Context context = viewHolder.view.getContext(); - - setTitleAndImage(cardView, recording); - int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(), - recording.getStartTimeMs()); - if (dateDifference <= 0) { - cardView.setContent(mContext.getString(R.string.dvr_date_today_time, - Utils.getDurationString(context, recording.getStartTimeMs(), - recording.getEndTimeMs(), false, false, true, 0)), null); - } else if (dateDifference == 1) { - cardView.setContent(mContext.getString(R.string.dvr_date_tomorrow_time, - Utils.getDurationString(context, recording.getStartTimeMs(), - recording.getEndTimeMs(), false, false, true, 0)), null); - } else { - cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(), - recording.getStartTimeMs(), false, true, false, 0), null); - } - if (mDvrManager.isConflicting(recording)) { - cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp); - } else { - cardView.setAffiliatedIcon(0); - } - viewHolder.updateProgressBar(); - viewHolder.mScheduledRecording = recording; - viewHolder.startUpdateProgressBar(); - super.onBindViewHolder(viewHolder, o); - } - - @Override - public void onUnbindViewHolder(ViewHolder baseHolder) { - ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - viewHolder.stopUpdateProgressBar(); - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - viewHolder.mScheduledRecording = null; - cardView.reset(); - super.onUnbindViewHolder(viewHolder); - } - - private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) { - Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); - SpannableString title = recording.getProgramTitleWithEpisodeNumber(mContext) == null ? - null : new SpannableString(recording.getProgramTitleWithEpisodeNumber(mContext)); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : mContext.getResources().getString(R.string.no_program_information)); - } else { - String programTitle = recording.getProgramTitle(); - title.setSpan(new TextAppearanceSpan(mContext, - R.style.text_appearance_card_view_episode_number), - programTitle == null ? 0 : programTitle.length(), title.length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - String imageUri = recording.getProgramPosterArtUri(); - boolean isChannelLogo = false; - if (TextUtils.isEmpty(imageUri)) { - imageUri = channel != null ? - TvContract.buildChannelLogoUri(channel.getId()).toString() : null; - isChannelLogo = true; - } - cardView.setTitle(title); - cardView.setImageUri(imageUri, isChannelLogo); - } -} diff --git a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java index 393a5ff3..8c0af9ed 100644 --- a/src/com/android/tv/dvr/ui/SortedArrayAdapter.java +++ b/src/com/android/tv/dvr/ui/SortedArrayAdapter.java @@ -20,11 +20,15 @@ import android.support.annotation.VisibleForTesting; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.PresenterSelector; +import com.android.tv.common.SoftPreconditions; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Keeps a set of items sorted @@ -35,16 +39,18 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { private final Comparator<T> mComparator; private final int mMaxItemCount; private int mExtraItemCount; + private final Set<Long> mIds = new HashSet<>(); - SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) { + public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator) { this(presenterSelector, comparator, Integer.MAX_VALUE); } - SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator, + public SortedArrayAdapter(PresenterSelector presenterSelector, Comparator<T> comparator, int maxItemCount) { super(presenterSelector); mComparator = comparator; mMaxItemCount = maxItemCount; + setHasStableIds(true); } /** @@ -56,7 +62,12 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { final void setInitialItems(List<T> items) { List<T> itemsCopy = new ArrayList<>(items); Collections.sort(itemsCopy, mComparator); - addAll(0, itemsCopy.subList(0, Math.min(mMaxItemCount, itemsCopy.size()))); + for (T item : itemsCopy) { + add(item, true); + if (size() == mMaxItemCount) { + break; + } + } } /** @@ -82,6 +93,9 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { * the end to save search time. */ public final void add(T item, boolean insertToEnd) { + long newItemId = getId(item); + SoftPreconditions.checkState(!mIds.contains(newItemId)); + mIds.add(newItemId); int i; if (insertToEnd) { i = findInsertPosition(item); @@ -89,8 +103,9 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { i = findInsertPositionBinary(item); } super.add(i, item); - if (size() > mMaxItemCount + mExtraItemCount) { - removeItems(mMaxItemCount, size() - mMaxItemCount - mExtraItemCount); + if (mMaxItemCount < Integer.MAX_VALUE && size() > mMaxItemCount + mExtraItemCount) { + Object removedItem = get(mMaxItemCount); + remove(removedItem); } } @@ -100,48 +115,97 @@ public abstract class SortedArrayAdapter<T> extends ArrayObjectAdapter { * They will be presented in their insertion order. */ public int addExtraItem(T item) { + long newItemId = getId(item); + SoftPreconditions.checkState(!mIds.contains(newItemId)); + mIds.add(newItemId); super.add(item); return ++mExtraItemCount; } + @Override + public boolean remove(Object item) { + return removeWithId((T) item); + } + /** * Removes an item which has the same ID as {@code item}. */ public boolean removeWithId(T item) { - int index = indexWithTypeAndId(item); - return index >= 0 && index < size() && remove(get(index)); + int index = indexWithId(item); + return index >= 0 && index < size() && removeItems(index, 1) == 1; + } + + @Override + public int removeItems(int position, int count) { + int upperBound = Math.min(position + count, size()); + for (int i = position; i < upperBound; i++) { + mIds.remove(getId((T) get(i))); + } + if (upperBound > size() - mExtraItemCount) { + mExtraItemCount -= upperBound - Math.max(size() - mExtraItemCount, position); + } + return super.removeItems(position, count); + } + + @Override + public void replace(int position, Object item) { + boolean wasExtra = position >= size() - mExtraItemCount; + removeItems(position, 1); + if (!wasExtra) { + add(item); + } else { + addExtraItem((T) item); + } + } + + @Override + public void clear() { + mIds.clear(); + super.clear(); } /** - * Change an item in the list. + * Changes an item in the list. * @param item The item to change. */ public final void change(T item) { - int oldIndex = indexWithTypeAndId(item); + int oldIndex = indexWithId(item); if (oldIndex != -1) { T old = (T) get(oldIndex); if (mComparator.compare(old, item) == 0) { replace(oldIndex, item); return; } - removeItems(oldIndex, 1); + remove(old); } add(item); } /** + * Checks whether the item is in the list. + */ + public final boolean contains(T item) { + return indexWithId(item) != -1; + } + + @Override + public long getId(int position) { + return getId((T) get(position)); + } + + /** * Returns the id of the the given {@code item}, which will be used in {@link #change} to * decide if the given item is already existed in the adapter. * * The id must be stable. */ - abstract long getId(T item); + protected abstract long getId(T item); - private int indexWithTypeAndId(T item) { + private int indexWithId(T item) { long id = getId(item); for (int i = 0; i < size() - mExtraItemCount; i++) { T r = (T) get(i); - if (r.getClass() == item.getClass() && getId(r) == id) { + if (getId(r) == id) { return i; } } diff --git a/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java new file mode 100644 index 00000000..5fe9c478 --- /dev/null +++ b/src/com/android/tv/dvr/ui/TrackedGuidedStepFragment.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.tv.dvr.ui; + +import android.content.Context; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidedAction; + +import com.android.tv.TvApplication; +import com.android.tv.analytics.Tracker; + +/** A {@link GuidedStepFragment} with {@link Tracker} for analytics. */ +public abstract class TrackedGuidedStepFragment extends GuidedStepFragment { + private Tracker mTracker; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mTracker = TvApplication.getSingletons(context).getAnalytics().getDefaultTracker(); + } + + @Override + public void onDetach() { + mTracker = null; + super.onDetach(); + } + + @Override + public final void onGuidedActionClicked(GuidedAction action) { + super.onGuidedActionClicked(action); + if (mTracker != null) { + mTracker.sendMenuClicked( + getTrackerPrefix() + "-action-" + getTrackerLabelForGuidedAction(action)); + } + onTrackedGuidedActionClicked(action); + } + + public String getTrackerLabelForGuidedAction(GuidedAction action) { + long actionId = action.getId(); + if (actionId == GuidedAction.ACTION_ID_CANCEL) { + return "cancel"; + } else if (actionId == GuidedAction.ACTION_ID_NEXT) { + return "next"; + } else if (actionId == GuidedAction.ACTION_ID_CURRENT) { + return "current"; + } else if (actionId == GuidedAction.ACTION_ID_OK) { + return "ok"; + } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { + return "cancel"; + } else if (actionId == GuidedAction.ACTION_ID_FINISH) { + return "finish"; + } else if (actionId == GuidedAction.ACTION_ID_CONTINUE) { + return "continue"; + } else if (actionId == GuidedAction.ACTION_ID_YES) { + return "yes"; + } else if (actionId == GuidedAction.ACTION_ID_NO) { + return "no"; + } else { + return "unknown-" + actionId; + } + } + + /** Delegated from {@link #onGuidedActionClicked(GuidedAction)} */ + public abstract void onTrackedGuidedActionClicked(GuidedAction action); + + /** The prefix used for analytics tracking, Usually the class name. */ + public abstract String getTrackerPrefix(); +} diff --git a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java index 8b8cd5c5..38a78f5d 100644 --- a/src/com/android/tv/dvr/ui/ActionPresenterSelector.java +++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.graphics.drawable.Drawable; import android.support.v17.leanback.R; @@ -110,11 +110,7 @@ class ActionPresenterSelector extends PresenterSelector { .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal); vh.view.setPaddingRelative(padding, 0, padding, 0); } - if (vh.mLayoutDirection == View.LAYOUT_DIRECTION_RTL) { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, icon, null); - } else { - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); - } + vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null); CharSequence line1 = action.getLabel1(); CharSequence line2 = action.getLabel2(); @@ -130,7 +126,7 @@ class ActionPresenterSelector extends PresenterSelector { @Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { ActionViewHolder vh = (ActionViewHolder) viewHolder; - vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); + vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null); vh.view.setPadding(0, 0, 0, 0); vh.mAction = null; } diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java new file mode 100644 index 00000000..bf18ddc0 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java @@ -0,0 +1,120 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +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.TvApplication; +import com.android.tv.dialog.HalfSizedDialogFragment; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrStopRecordingFragment; +import com.android.tv.dvr.ui.DvrUiHelper; + +/** + * {@link RecordingDetailsFragment} for current recording in DVR. + */ +public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment { + private static final int ACTION_STOP_RECORDING = 1; + + private DvrDataManager mDvrDataManger; + private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = + new DvrDataManager.ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording... schedules) { } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + if (schedule.getId() == getRecording().getId()) { + getActivity().finish(); + return; + } + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + if (schedule.getId() == getRecording().getId() + && schedule.getState() + != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + getActivity().finish(); + return; + } + } + } + }; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mDvrDataManger = TvApplication.getSingletons(context).getDvrDataManager(); + mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener); + } + + @Override + protected SparseArrayObjectAdapter onCreateActionsAdapter() { + SparseArrayObjectAdapter adapter = + new SparseArrayObjectAdapter(new ActionPresenterSelector()); + Resources res = getResources(); + adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING, + res.getString(R.string.dvr_detail_stop_recording), null, + res.getDrawable(R.drawable.lb_ic_stop))); + return adapter; + } + + @Override + protected OnActionClickedListener onCreateOnActionClickedListener() { + return new OnActionClickedListener() { + @Override + public void onActionClicked(Action action) { + if (action.getId() == ACTION_STOP_RECORDING) { + DvrUiHelper.showStopRecordingDialog(getActivity(), + getRecording().getChannelId(), + DvrStopRecordingFragment.REASON_USER_STOP, + new HalfSizedDialogFragment.OnActionClickListener() { + @Override + public void onActionClick(long actionId) { + if (actionId == DvrStopRecordingFragment.ACTION_STOP) { + DvrManager dvrManager = + TvApplication.getSingletons(getContext()) + .getDvrManager(); + dvrManager.stopRecording(getRecording()); + getActivity().finish(); + } + } + }); + } + } + }; + } + + @Override + public void onDetach() { + if (mDvrDataManger != null) { + mDvrDataManger.removeScheduledRecordingListener(mScheduledRecordingListener); + } + super.onDetach(); + } +} diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java new file mode 100644 index 00000000..c1fa05d7 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java @@ -0,0 +1,317 @@ +/* + * 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.content.Context; +import android.media.tv.TvContract; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.Channel; +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.DvrUiHelper; + +/** + * A class for details content. + */ +class DetailsContent { + /** Constant for invalid time. */ + public static final long INVALID_TIME = -1; + + private CharSequence mTitle; + private long mStartTimeUtcMillis; + private long mEndTimeUtcMillis; + private String mDescription; + private String mLogoImageUri; + private String mBackgroundImageUri; + private boolean mUsingChannelLogo; + + static DetailsContent createFromRecordedProgram(Context context, + RecordedProgram recordedProgram) { + return new DetailsContent.Builder() + .setChannelId(recordedProgram.getChannelId()) + .setProgramTitle(recordedProgram.getTitle()) + .setSeasonNumber(recordedProgram.getSeasonNumber()) + .setEpisodeNumber(recordedProgram.getEpisodeNumber()) + .setStartTimeUtcMillis(recordedProgram.getStartTimeUtcMillis()) + .setEndTimeUtcMillis(recordedProgram.getEndTimeUtcMillis()) + .setDescription(TextUtils.isEmpty(recordedProgram.getLongDescription()) + ? recordedProgram.getDescription() : recordedProgram.getLongDescription()) + .setPosterArtUri(recordedProgram.getPosterArtUri()) + .setThumbnailUri(recordedProgram.getThumbnailUri()) + .build(context); + } + + static DetailsContent createFromSeriesRecording(Context context, + SeriesRecording seriesRecording) { + return new DetailsContent.Builder() + .setChannelId(seriesRecording.getChannelId()) + .setTitle(seriesRecording.getTitle()) + .setDescription(TextUtils.isEmpty(seriesRecording.getLongDescription()) + ? seriesRecording.getDescription() : seriesRecording.getLongDescription()) + .setPosterArtUri(seriesRecording.getPosterUri()) + .setThumbnailUri(seriesRecording.getPhotoUri()) + .build(context); + } + + static DetailsContent createFromScheduledRecording(Context context, + ScheduledRecording scheduledRecording) { + Channel channel = TvApplication.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); + } + + private DetailsContent() { } + + /** + * Returns title. + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns start time. + */ + public long getStartTimeUtcMillis() { + return mStartTimeUtcMillis; + } + + /** + * Returns end time. + */ + public long getEndTimeUtcMillis() { + return mEndTimeUtcMillis; + } + + /** + * Returns description. + */ + public String getDescription() { + return mDescription; + } + + /** + * Returns Logo image URI as a String. + */ + public String getLogoImageUri() { + return mLogoImageUri; + } + + /** + * Returns background image URI as a String. + */ + public String getBackgroundImageUri() { + return mBackgroundImageUri; + } + + /** + * Returns if image URIs are from its channels' logo. + */ + public boolean isUsingChannelLogo() { + return mUsingChannelLogo; + } + + /** + * Copies other details content. + */ + public void copyFrom(DetailsContent other) { + if (this == other) { + return; + } + mTitle = other.mTitle; + mStartTimeUtcMillis = other.mStartTimeUtcMillis; + mEndTimeUtcMillis = other.mEndTimeUtcMillis; + mDescription = other.mDescription; + mLogoImageUri = other.mLogoImageUri; + mBackgroundImageUri = other.mBackgroundImageUri; + mUsingChannelLogo = other.mUsingChannelLogo; + } + + /** + * A class for building details content. + */ + public static final class Builder { + private final DetailsContent mDetailsContent; + + private long mChannelId; + private String mProgramTitle; + private String mSeasonNumber; + private String mEpisodeNumber; + private String mPosterArtUri; + private String mThumbnailUri; + + public Builder() { + mDetailsContent = new DetailsContent(); + mDetailsContent.mStartTimeUtcMillis = INVALID_TIME; + mDetailsContent.mEndTimeUtcMillis = INVALID_TIME; + } + + /** + * Sets title. + */ + public Builder setTitle(CharSequence title) { + mDetailsContent.mTitle = title; + return this; + } + + /** + * Sets start time. + */ + public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { + mDetailsContent.mStartTimeUtcMillis = startTimeUtcMillis; + return this; + } + + /** + * Sets end time. + */ + public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { + mDetailsContent.mEndTimeUtcMillis = endTimeUtcMillis; + return this; + } + + /** + * Sets description. + */ + public Builder setDescription(String description) { + mDetailsContent.mDescription = description; + return this; + } + + /** + * Sets logo image URI as a String. + */ + public Builder setLogoImageUri(String logoImageUri) { + mDetailsContent.mLogoImageUri = logoImageUri; + return this; + } + + /** + * Sets background image URI as a String. + */ + public Builder setBackgroundImageUri(String backgroundImageUri) { + mDetailsContent.mBackgroundImageUri = backgroundImageUri; + return this; + } + + private Builder setProgramTitle(String programTitle) { + mProgramTitle = programTitle; + return this; + } + + private Builder setSeasonNumber(String seasonNumber) { + mSeasonNumber = seasonNumber; + return this; + } + + private Builder setEpisodeNumber(String episodeNumber) { + mEpisodeNumber = episodeNumber; + return this; + } + + private Builder setChannelId(long channelId) { + mChannelId = channelId; + return this; + } + + private Builder setPosterArtUri(String posterArtUri) { + mPosterArtUri = posterArtUri; + return this; + } + + private Builder setThumbnailUri(String thumbnailUri) { + mThumbnailUri = thumbnailUri; + return this; + } + + private void createStyledTitle(Context context, Channel channel) { + CharSequence title = DvrUiHelper.getStyledTitleWithEpisodeNumber(context, + mProgramTitle, mSeasonNumber, mEpisodeNumber, + R.style.text_appearance_card_view_episode_number); + if (TextUtils.isEmpty(title)) { + mDetailsContent.mTitle = channel != null ? channel.getDisplayName() + : context.getResources().getString(R.string.no_program_information); + } else { + mDetailsContent.mTitle = title; + } + } + + private void createImageUris(@Nullable Channel channel) { + mDetailsContent.mLogoImageUri = null; + mDetailsContent.mBackgroundImageUri = null; + mDetailsContent.mUsingChannelLogo = false; + if (!TextUtils.isEmpty(mPosterArtUri) && !TextUtils.isEmpty(mThumbnailUri)) { + mDetailsContent.mLogoImageUri = mPosterArtUri; + mDetailsContent.mBackgroundImageUri = mThumbnailUri; + } else if (!TextUtils.isEmpty(mPosterArtUri)) { + // thumbnailUri is empty + mDetailsContent.mLogoImageUri = mPosterArtUri; + mDetailsContent.mBackgroundImageUri = mPosterArtUri; + } else if (!TextUtils.isEmpty(mThumbnailUri)) { + // posterArtUri is empty + mDetailsContent.mLogoImageUri = mThumbnailUri; + mDetailsContent.mBackgroundImageUri = mThumbnailUri; + } + if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) { + String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId()) + .toString(); + mDetailsContent.mLogoImageUri = channelLogoUri; + mDetailsContent.mBackgroundImageUri = channelLogoUri; + mDetailsContent.mUsingChannelLogo = true; + } + } + + /** + * Builds details content. + */ + public DetailsContent build(Context context) { + Channel channel = TvApplication.getSingletons(context).getChannelDataManager() + .getChannel(mChannelId); + if (mDetailsContent.mTitle == null) { + createStyledTitle(context, channel); + } + if (mDetailsContent.mBackgroundImageUri == null + && mDetailsContent.mLogoImageUri == null) { + createImageUris(channel); + } + DetailsContent detailsContent = new DetailsContent(); + detailsContent.copyFrom(mDetailsContent); + return detailsContent; + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java index 175f05bc..09b57887 100644 --- a/src/com/android/tv/dvr/ui/DetailsContentPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java @@ -14,13 +14,14 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.app.Activity; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; +import android.content.Context; import android.graphics.Paint; import android.graphics.Paint.FontMetricsInt; import android.support.v17.leanback.widget.Presenter; @@ -29,6 +30,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityManager; import android.widget.LinearLayout; import android.widget.TextView; @@ -38,13 +40,14 @@ import com.android.tv.util.Utils; /** * An {@link Presenter} for rendering a detailed description of an DVR item. - * Typically this Presenter will be used in a {@link DetailsOverviewRowPresenter}. + * Typically this Presenter will be used in a + * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter}. * Most codes of this class is originated from * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}. * The latter class are re-used to provide a customized version of * {@link android.support.v17.leanback.widget.DetailsOverviewRow}. */ -public class DetailsContentPresenter extends Presenter { +class DetailsContentPresenter extends Presenter { /** * The ViewHolder for the {@link DetailsContentPresenter}. */ @@ -82,25 +85,38 @@ public class DetailsContentPresenter extends Presenter { return false; } final int bodyLines = mBody.getLineCount(); - final int maxLines = mFullTextMode ? bodyLines : + int maxLines = mFullTextMode ? bodyLines : (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines); if (bodyLines > maxLines) { mReadMoreView.setVisibility(View.VISIBLE); mDescriptionContainer.setFocusable(true); + mDescriptionContainer.setClickable(true); mDescriptionContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { mFullTextMode = true; mReadMoreView.setVisibility(View.GONE); - mDescriptionContainer.setFocusable(false); + mDescriptionContainer.setFocusable(( + (AccessibilityManager) view.getContext() + .getSystemService( + Context.ACCESSIBILITY_SERVICE)) + .isEnabled()); + mDescriptionContainer.setClickable(false); mDescriptionContainer.setOnClickListener(null); + int oldMaxLines = mBody.getMaxLines(); mBody.setMaxLines(bodyLines); // Minus 1 from line difference to eliminate the space // originally occupied by "READ MORE" - showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing); + showFullText((bodyLines - oldMaxLines - 1) * mBodyLineSpacing); } }); } + if (mReadMoreView.getVisibility() == View.VISIBLE + && mSubtitle.getVisibility() == View.VISIBLE) { + // If both "READ MORE" and subtitle is shown, the capable maximum lines + // will be one line less. + maxLines -= 1; + } if (mBody.getMaxLines() != maxLines) { mBody.setMaxLines(maxLines); return false; @@ -113,11 +129,30 @@ public class DetailsContentPresenter extends Presenter { public ViewHolder(final View view) { super(view); + view.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + // In case predraw listener was removed in detach, make sure + // we have the proper layout. + addPreDrawListener(); + } + + @Override + public void onViewDetachedFromWindow(View v) { + removePreDrawListener(); + } + }); mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title); mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle); mBody = (TextView) view.findViewById(R.id.dvr_details_description_body); mDescriptionContainer = (LinearLayout) view.findViewById(R.id.dvr_details_description_container); + // We have to explicitly set focusable to true here for accessibility, since we might + // set the view's focusable state when we need to show "READ MORE", which would remove + // the default focusable state for accessibility. + mDescriptionContainer.setFocusable(((AccessibilityManager) view.getContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE)).isEnabled()); mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more); FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle); @@ -129,7 +164,7 @@ public class DetailsContentPresenter extends Presenter { mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize( R.dimen.lb_details_description_under_title_baseline_margin); mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize( - R.dimen.lb_details_description_under_subtitle_baseline_margin); + R.dimen.dvr_details_description_under_subtitle_baseline_margin); mTitleLineSpacing = view.getResources().getDimensionPixelSize( R.dimen.lb_details_description_title_line_spacing); @@ -276,22 +311,6 @@ public class DetailsContentPresenter extends Presenter { @Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { } - @Override - public void onViewAttachedToWindow(Presenter.ViewHolder holder) { - // In case predraw listener was removed in detach, make sure - // we have the proper layout. - ViewHolder vh = (ViewHolder) holder; - vh.addPreDrawListener(); - super.onViewAttachedToWindow(holder); - } - - @Override - public void onViewDetachedFromWindow(Presenter.ViewHolder holder) { - ViewHolder vh = (ViewHolder) holder; - vh.removePreDrawListener(); - super.onViewDetachedFromWindow(holder); - } - private void setTopMargin(View view, int topMargin) { ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); lp.topMargin = topMargin; diff --git a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java index 6714ecd3..82fe9ce3 100644 --- a/src/com/android/tv/dvr/ui/DetailsViewBackgroundHelper.java +++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.app.Activity; import android.graphics.drawable.BitmapDrawable; @@ -26,7 +26,7 @@ import android.support.v17.leanback.app.BackgroundManager; /** * The Background Helper. */ -public class DetailsViewBackgroundHelper { +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/DvrActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java index 45fb1cf1..07eec107 100644 --- a/src/com/android/tv/dvr/ui/DvrActivity.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java @@ -14,9 +14,11 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.app.Activity; +import android.content.Intent; +import android.media.tv.TvInputManager; import android.os.Bundle; import com.android.tv.R; @@ -25,11 +27,26 @@ import com.android.tv.TvApplication; /** * {@link android.app.Activity} for DVR UI. */ -public class DvrActivity extends Activity { +public class DvrBrowseActivity extends Activity { + private DvrBrowseFragment mFragment; + @Override public void onCreate(Bundle savedInstanceState) { TvApplication.setCurrentRunningProcess(this, true); super.onCreate(savedInstanceState); setContentView(R.layout.dvr_main); + mFragment = (DvrBrowseFragment) getFragmentManager().findFragmentById(R.id.dvr_frame); + handleIntent(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + handleIntent(intent); + } + + private void handleIntent(Intent intent) { + if (TvInputManager.ACTION_VIEW_RECORDING_SCHEDULES.equals(intent.getAction())) { + mFragment.showScheduledRow(); + } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java index a6dd31d1..cb3a5745 100644 --- a/src/com/android/tv/dvr/ui/DvrBrowseFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java @@ -14,10 +14,9 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.Context; -import android.media.tv.TvInputManager.TvInputCallback; import android.os.Bundle; import android.os.Handler; import android.support.v17.leanback.app.BrowseFragment; @@ -25,11 +24,11 @@ import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.TitleViewAdapter; -import android.text.TextUtils; import android.util.Log; +import android.view.View; +import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; import com.android.tv.ApplicationSingletons; import com.android.tv.R; @@ -42,9 +41,10 @@ import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.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 java.util.ArrayList; import java.util.Arrays; @@ -64,12 +64,16 @@ public class DvrBrowseFragment extends BrowseFragment implements private static final int MAX_RECENT_ITEM_COUNT = 10; private static final int MAX_SCHEDULED_ITEM_COUNT = 4; + private boolean mShouldShowScheduleRow; + private boolean mEntranceTransitionEnded; + private RecordedProgramAdapter mRecentAdapter; private ScheduleAdapter mScheduleAdapter; private SeriesAdapter mSeriesAdapter; private RecordedProgramAdapter[] mGenreAdapters = new RecordedProgramAdapter[GenreItems.getGenreCount() + 1]; private ListRow mRecentRow; + private ListRow mScheduledRow; private ListRow mSeriesRow; private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1]; private List<String> mGenreLabels; @@ -79,6 +83,20 @@ public class DvrBrowseFragment extends BrowseFragment implements private ClassPresenterSelector mPresenterSelector; private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>(); private final Handler mHandler = new Handler(); + private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener = + new OnGlobalFocusChangeListener() { + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + if (oldFocus instanceof RecordingCardView) { + ((RecordingCardView) oldFocus).expandTitle(false, true); + } + if (newFocus instanceof RecordingCardView) { + // If the header transition is ongoing, expand cards immediately without + // animation to make a smooth transition. + ((RecordingCardView) newFocus).expandTitle(true, !isInHeadersTransition()); + } + } + }; private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = new Comparator<Object>() { @Override @@ -104,7 +122,7 @@ public class DvrBrowseFragment extends BrowseFragment implements } }; - private final Comparator<Object> SCHEDULE_COMPARATOR = new Comparator<Object>() { + private static final Comparator<Object> SCHEDULE_COMPARATOR = new Comparator<Object>() { @Override public int compare(Object lhs, Object rhs) { if (lhs instanceof ScheduledRecording) { @@ -128,7 +146,7 @@ public class DvrBrowseFragment extends BrowseFragment implements public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) { if (mScheduleAdapter != null) { for (ScheduledRecording schedule : schedules) { - onScheduledRecordingStatusChanged(schedule); + onScheduledRecordingConflictStatusChanged(schedule); } } } @@ -154,16 +172,12 @@ public class DvrBrowseFragment extends BrowseFragment implements new ScheduledRecordingPresenter(context)) .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context)) .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context)) - .addClassPresenter(FullScheduleCardHolder.class, new FullSchedulesCardPresenter()); + .addClassPresenter(FullScheduleCardHolder.class, + new FullSchedulesCardPresenter(context)); mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context))); mGenreLabels.add(getString(R.string.dvr_main_others)); - setupUiElements(); - setupAdapters(); - mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); - prepareEntranceTransition(); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } else { + prepareUiElements(); + if (!startBrowseIfDvrInitialized()) { if (!mDvrDataManager.isDvrScheduleLoadFinished()) { mDvrDataManager.addDvrScheduleLoadFinishedListener(this); } @@ -174,6 +188,19 @@ public class DvrBrowseFragment extends BrowseFragment implements } @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); + } + + @Override + public void onDestroyView() { + getView().getViewTreeObserver() + .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener); + super.onDestroyView(); + } + + @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy"); mHandler.removeCallbacks(mUpdateRowsRunnable); @@ -195,25 +222,13 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public void onDvrScheduleLoadFinished() { - List<ScheduledRecording> scheduledRecordings = mDvrDataManager.getAllScheduledRecordings(); - onScheduledRecordingAdded(ScheduledRecording.toArray(scheduledRecordings)); - List<SeriesRecording> seriesRecordings = mDvrDataManager.getSeriesRecordings(); - onSeriesRecordingAdded(SeriesRecording.toArray(seriesRecordings)); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } + startBrowseIfDvrInitialized(); mDvrDataManager.removeDvrScheduleLoadFinishedListener(this); } @Override public void onRecordedProgramLoadFinished() { - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, true); - } - updateRows(); - if (mDvrDataManager.isInitialized()) { - startEntranceTransition(); - } + startBrowseIfDvrInitialized(); mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); } @@ -270,6 +285,18 @@ public class DvrBrowseFragment extends BrowseFragment implements } } + private void onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules) { + for (ScheduledRecording schedule : schedules) { + if (needToShowScheduledRecording(schedule)) { + if (mScheduleAdapter.contains(schedule)) { + mScheduleAdapter.change(schedule); + } + } else { + mScheduleAdapter.removeWithId(schedule); + } + } + } + @Override public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings)); @@ -295,44 +322,80 @@ public class DvrBrowseFragment extends BrowseFragment implements super.showTitle(flags); } - private void setupUiElements() { + @Override + protected void onEntranceTransitionEnd() { + super.onEntranceTransitionEnd(); + if (mShouldShowScheduleRow) { + showScheduledRowInternal(); + } + mEntranceTransitionEnded = true; + } + + void showScheduledRow() { + if (!mEntranceTransitionEnded) { + setHeadersState(HEADERS_HIDDEN); + mShouldShowScheduleRow = true; + } else { + showScheduledRowInternal(); + } + } + + private void showScheduledRowInternal() { + setSelectedPosition(mRowsAdapter.indexOf(mScheduledRow), true, null); + if (getHeadersState() == HEADERS_ENABLED) { + startHeadersTransition(false); + } + mShouldShowScheduleRow = false; + } + + private void prepareUiElements() { setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge)); setHeadersState(HEADERS_ENABLED); setHeadersTransitionOnBackEnabled(false); setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null)); + mRowsAdapter = new ArrayObjectAdapter(new DvrListRowPresenter(getContext())); + setAdapter(mRowsAdapter); + prepareEntranceTransition(); } - private void setupAdapters() { - mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); - mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); - mSeriesAdapter = new SeriesAdapter(); - for (int i = 0; i < mGenreAdapters.length; i++) { - mGenreAdapters[i] = new RecordedProgramAdapter(); - } - // Schedule Recordings. - List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings(); - onScheduledRecordingAdded(ScheduledRecording.toArray(schedules)); - mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); - // Recorded Programs. - for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { - handleRecordedProgramAdded(recordedProgram, false); - } - // Series Recordings. Series recordings should be added after recorded programs, because - // we build series recordings' latest program information while adding recorded programs. - List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings(); - handleSeriesRecordingsAdded(recordings); - mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); - mRecentRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_recent)), mRecentAdapter); - mRowsAdapter.add(new ListRow(new HeaderItem( - getString(R.string.dvr_main_scheduled)), mScheduleAdapter)); - mSeriesRow = new ListRow(new HeaderItem( - getString(R.string.dvr_main_series)), mSeriesAdapter); - updateRows(); - mDvrDataManager.addRecordedProgramListener(this); - mDvrDataManager.addScheduledRecordingListener(this); - mDvrDataManager.addSeriesRecordingListener(this); - setAdapter(mRowsAdapter); + private boolean startBrowseIfDvrInitialized() { + if (mDvrDataManager.isInitialized()) { + // Setup rows + mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT); + mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT); + mSeriesAdapter = new SeriesAdapter(); + for (int i = 0; i < mGenreAdapters.length; i++) { + mGenreAdapters[i] = new RecordedProgramAdapter(); + } + // Schedule Recordings. + List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings(); + onScheduledRecordingAdded(ScheduledRecording.toArray(schedules)); + mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER); + // Recorded Programs. + for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) { + handleRecordedProgramAdded(recordedProgram, false); + } + // Series Recordings. Series recordings should be added after recorded programs, because + // we build series recordings' latest program information while adding recorded programs. + List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings(); + handleSeriesRecordingsAdded(recordings); + mRecentRow = new ListRow(new HeaderItem( + getString(R.string.dvr_main_recent)), mRecentAdapter); + mScheduledRow = new ListRow(new HeaderItem( + getString(R.string.dvr_main_scheduled)), mScheduleAdapter); + mSeriesRow = new ListRow(new HeaderItem( + getString(R.string.dvr_main_series)), mSeriesAdapter); + mRowsAdapter.add(mScheduledRow); + updateRows(); + // Initialize listeners + mDvrDataManager.addRecordedProgramListener(this); + mDvrDataManager.addScheduledRecordingListener(this); + mDvrDataManager.addSeriesRecordingListener(this); + mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener); + startEntranceTransition(); + return true; + } + return false; } private void handleRecordedProgramAdded(RecordedProgram recordedProgram, @@ -589,10 +652,11 @@ public class DvrBrowseFragment extends BrowseFragment implements @Override public long getId(Object item) { + // We takes the inverse number for the ID of recorded programs to make the ID stable. if (item instanceof SeriesRecording) { return ((SeriesRecording) item).getId(); } else if (item instanceof RecordedProgram) { - return ((RecordedProgram) item).getId(); + return -((RecordedProgram) item).getId() - 1; } else { return -1; } diff --git a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java index 806c775c..35d21db8 100644 --- a/src/com/android/tv/dvr/ui/DvrDetailsActivity.java +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java @@ -14,19 +14,23 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +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.TvApplication; +import com.android.tv.dialog.PinDialogFragment; /** * Activity to show details view in DVR. */ -public class DvrDetailsActivity extends Activity { +public class DvrDetailsActivity extends Activity implements PinDialogFragment.OnPinCheckedListener { /** * Name of record id added to the Intent. */ @@ -68,6 +72,8 @@ public class DvrDetailsActivity extends Activity { */ public static final int SERIES_RECORDING_VIEW = 4; + private PinDialogFragment.OnPinCheckedListener mOnPinCheckedListener; + @Override public void onCreate(Bundle savedInstanceState) { TvApplication.setCurrentRunningProcess(this, true); @@ -94,5 +100,55 @@ public class DvrDetailsActivity extends Activity { 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/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java index 21f9c4b4..19fb7117 100644 --- a/src/com/android/tv/dvr/ui/DvrDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java @@ -14,16 +14,14 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.Context; -import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.tv.TvContentRating; -import android.media.tv.TvInputManager; import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; @@ -36,20 +34,18 @@ import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import android.support.v17.leanback.widget.VerticalGridView; -import android.text.Spannable; -import android.text.SpannableString; import android.text.TextUtils; -import android.text.style.TextAppearanceSpan; import android.widget.Toast; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.data.BaseProgram; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.dialog.PinDialogFragment; -import com.android.tv.dvr.DvrPlaybackActivity; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.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.util.ImageLoader; import com.android.tv.util.ToastUtils; @@ -163,26 +159,6 @@ abstract class DvrDetailsFragment extends DetailsFragment { abstract OnActionClickedListener onCreateOnActionClickedListener(); /** - * Returns program title with episode number. If the program is null, returns channel name. - */ - protected CharSequence getTitleFromProgram(BaseProgram program, Channel channel) { - String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(getContext()); - SpannableString title = titleWithEpisodeNumber == null ? null - : new SpannableString(titleWithEpisodeNumber); - if (TextUtils.isEmpty(title)) { - title = new SpannableString(channel != null ? channel.getDisplayName() - : getContext().getResources().getString( - R.string.no_program_information)); - } else { - String programTitle = program.getTitle(); - title.setSpan(new TextAppearanceSpan(getContext(), - R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 - : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - return title; - } - - /** * Loads logo and background images for detail fragments. */ protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) { @@ -233,10 +209,11 @@ abstract class DvrDetailsFragment extends DetailsFragment { Toast.LENGTH_SHORT); return; } + long programId = recordedProgram.getId(); ParentalControlSettings parental = TvApplication.getSingletons(getActivity()) .getTvInputManagerHelper().getParentalControlSettings(); if (!parental.isParentalControlsEnabled()) { - launchPlaybackActivity(recordedProgram, seekTimeMs, false); + DvrUiHelper.startPlaybackActivity(getContext(), programId, seekTimeMs, false); return; } ChannelDataManager channelDataManager = @@ -246,21 +223,12 @@ abstract class DvrDetailsFragment extends DetailsFragment { checkPinToPlay(recordedProgram, seekTimeMs); return; } - String ratingString = recordedProgram.getContentRating(); - if (TextUtils.isEmpty(ratingString)) { - launchPlaybackActivity(recordedProgram, seekTimeMs, false); - return; - } - String[] ratingList = ratingString.split(","); - TvContentRating[] programRatings = new TvContentRating[ratingList.length]; - for (int i = 0; i < ratingList.length; i++) { - programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); - } - TvContentRating blockRatings = parental.getBlockedRating(programRatings); + TvContentRating[] ratings = recordedProgram.getContentRatings(); + TvContentRating blockRatings = parental.getBlockedRating(ratings); if (blockRatings != null) { checkPinToPlay(recordedProgram, seekTimeMs); } else { - launchPlaybackActivity(recordedProgram, seekTimeMs, false); + DvrUiHelper.startPlaybackActivity(getContext(), programId, seekTimeMs, false); } } @@ -279,26 +247,21 @@ abstract class DvrDetailsFragment extends DetailsFragment { } private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { - new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, - new PinDialogFragment.ResultListener() { - @Override - public void done(boolean success) { - if (success) { - launchPlaybackActivity(recordedProgram, seekTimeMs, true); - } + SoftPreconditions.checkState(getActivity() instanceof DvrDetailsActivity); + if (getActivity() instanceof DvrDetailsActivity) { + ((DvrDetailsActivity) getActivity()).setOnPinCheckListener(new OnPinCheckedListener() { + @Override + public void onPinChecked(boolean checked, int type, String rating) { + ((DvrDetailsActivity) getActivity()).setOnPinCheckListener(null); + if (checked && type == PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM) { + DvrUiHelper.startPlaybackActivity(getContext(), recordedProgram.getId(), + seekTimeMs, true); } - }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); - } - - private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs, - boolean pinChecked) { - Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); - if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); + } + }); + PinDialogFragment.create(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM) + .show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); } - intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); - getActivity().startActivity(intent); } private static class MyImageLoaderCallback extends diff --git a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java new file mode 100644 index 00000000..df0e61c1 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java @@ -0,0 +1,140 @@ +/* + * 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.content.Context; +import android.support.annotation.CallSuper; +import android.support.v17.leanback.widget.Presenter; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; + +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.ui.DvrUiHelper; + +import java.util.HashSet; +import java.util.Set; + +/** + * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in + * {@link DvrBrowseFragment}. DVR items might include: + * {@link com.android.tv.dvr.data.ScheduledRecording}, + * {@link com.android.tv.dvr.data.RecordedProgram}, and + * {@link com.android.tv.dvr.data.SeriesRecording}. + */ +public abstract class DvrItemPresenter<T> extends Presenter { + protected final Context mContext; + private final Set<DvrItemViewHolder> mBoundViewHolders = new HashSet<>(); + private final OnClickListener mOnClickListener = onCreateOnClickListener(); + + protected class DvrItemViewHolder extends ViewHolder { + DvrItemViewHolder(RecordingCardView view) { + super(view); + } + + protected RecordingCardView getView() { + return (RecordingCardView) view; + } + + protected void onBound(T item) { } + + protected void onUnbound() { } + } + + DvrItemPresenter(Context context) { + mContext = context; + } + + @Override + public final ViewHolder onCreateViewHolder(ViewGroup parent) { + return onCreateDvrItemViewHolder(); + } + + @Override + public final void onBindViewHolder(ViewHolder baseHolder, Object item) { + DvrItemViewHolder viewHolder; + T dvrItem; + try { + viewHolder = (DvrItemViewHolder) baseHolder; + Class<T> itemType = (Class<T>) item.getClass(); + dvrItem = itemType.cast(item); + } catch (ClassCastException e) { + SoftPreconditions.checkState(false); + return; + } + viewHolder.view.setTag(item); + viewHolder.view.setOnClickListener(mOnClickListener); + onBindDvrItemViewHolder(viewHolder, dvrItem); + viewHolder.onBound(dvrItem); + mBoundViewHolders.add(viewHolder); + } + + @Override + @CallSuper + public void onUnbindViewHolder(ViewHolder baseHolder) { + DvrItemViewHolder viewHolder = (DvrItemViewHolder) baseHolder; + mBoundViewHolders.remove(viewHolder); + viewHolder.onUnbound(); + viewHolder.view.setTag(null); + viewHolder.view.setOnClickListener(null); + } + + /** + * Unbinds all bound view holders. + */ + public void unbindAllViewHolders() { + // When browse fragments are destroyed, RecyclerView would not call presenters' + // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks. + for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) { + onUnbindViewHolder(viewHolder); + } + } + + /** + * This method will be called when a {@link DvrItemViewHolder} is needed to be created. + */ + abstract protected DvrItemViewHolder onCreateDvrItemViewHolder(); + + /** + * This method will be called when a {@link DvrItemViewHolder} is bound to a DVR item. + */ + abstract protected void onBindDvrItemViewHolder(DvrItemViewHolder viewHolder, T item); + + /** + * Returns context. + */ + protected Context getContext() { + return mContext; + } + + /** + * Creates {@link OnClickListener} for DVR library's card views. + */ + protected OnClickListener onCreateOnClickListener() { + return new OnClickListener() { + @Override + public void onClick(View view) { + if (view instanceof RecordingCardView) { + RecordingCardView v = (RecordingCardView) view; + DvrUiHelper.startDetailsActivity((Activity) v.getContext(), + v.getTag(), v.getImageView(), false); + } + } + }; + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java new file mode 100644 index 00000000..37a72eaf --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2017 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.content.Context; +import android.support.v17.leanback.widget.ListRowPresenter; +import android.view.ViewGroup; + +import com.android.tv.R; + +/** A list row presenter to display expand/fold card views list. */ +public class DvrListRowPresenter extends ListRowPresenter { + public DvrListRowPresenter(Context context) { + super(); + setRowHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + setExpandedRowHeight( + context.getResources() + .getDimensionPixelSize(R.dimen.dvr_library_expanded_row_height)); + } +} diff --git a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java index d4d4d8ab..311137a9 100644 --- a/src/com/android/tv/dvr/ui/FullScheduleCardHolder.java +++ b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; /** * Special object for schedule preview; diff --git a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java index 7dd85f45..94c67eec 100644 --- a/src/com/android/tv/dvr/ui/FullSchedulesCardPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java @@ -14,17 +14,17 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.Context; -import android.support.v17.leanback.widget.Presenter; +import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.Utils; import java.util.Collections; @@ -33,23 +33,28 @@ import java.util.List; /** * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. */ -public class FullSchedulesCardPresenter extends Presenter { +class FullSchedulesCardPresenter extends DvrItemPresenter<Object> { + private final Drawable mIconDrawable; + private final String mCardTitleText; + + FullSchedulesCardPresenter(Context context) { + super(context); + mIconDrawable = mContext.getDrawable(R.drawable.dvr_full_schedule); + mCardTitleText = mContext.getString(R.string.dvr_full_schedule_card_view_title); + } + @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); - return new ScheduledRecordingViewHolder(view); + public DvrItemViewHolder onCreateDvrItemViewHolder() { + return new DvrItemViewHolder(new RecordingCardView(mContext)); } @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { - final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - final Context context = viewHolder.view.getContext(); + public void onBindDvrItemViewHolder(DvrItemViewHolder vh, Object o) { + final RecordingCardView cardView = (RecordingCardView) vh.view; - cardView.setImage(context.getDrawable(R.drawable.dvr_full_schedule)); - cardView.setTitle(context.getString(R.string.dvr_full_schedule_card_view_title)); - List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(context) + cardView.setTitle(mCardTitleText); + cardView.setImage(mIconDrawable); + List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(mContext) .getDvrDataManager().getAvailableScheduledRecordings(); int fullDays = 0; if (!scheduledRecordings.isEmpty()) { @@ -57,28 +62,23 @@ public class FullSchedulesCardPresenter extends Presenter { Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR) .getStartTimeMs()) + 1; } - cardView.setContent(context.getResources().getQuantityString( + cardView.setContent(mContext.getResources().getQuantityString( R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null); - - View.OnClickListener clickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - DvrUiHelper.startSchedulesActivity(context, null); - } - }; - baseHolder.view.setOnClickListener(clickListener); } @Override - public void onUnbindViewHolder(ViewHolder baseHolder) { - ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - cardView.reset(); + public void onUnbindViewHolder(ViewHolder vh) { + ((RecordingCardView) vh.view).reset(); + super.onUnbindViewHolder(vh); } - private static final class ScheduledRecordingViewHolder extends ViewHolder { - ScheduledRecordingViewHolder(RecordingCardView view) { - super(view); - } + @Override + protected View.OnClickListener onCreateOnClickListener() { + return new View.OnClickListener() { + @Override + public void onClick(View view) { + DvrUiHelper.startSchedulesActivity(mContext, null); + } + }; } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java index e698b8a2..eb9cb26c 100644 --- a/src/com/android/tv/dvr/ui/RecordedProgramDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.res.Resources; import android.media.tv.TvInputManager; @@ -22,18 +22,16 @@ 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 android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.data.Channel; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; /** - * {@link DetailsFragment} for recorded program in DVR. + * {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR. */ public class RecordedProgramDetailsFragment extends DvrDetailsFragment implements DvrDataManager.RecordedProgramListener { @@ -44,7 +42,6 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment private DvrWatchedPositionManager mDvrWatchedPositionManager; private RecordedProgram mRecordedProgram; - private DetailsContent mDetailsContent; private boolean mPaused; private DvrDataManager mDvrDataManager; @@ -59,7 +56,8 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment public void onCreateInternal() { mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) .getDvrWatchedPositionManager(); - setDetailsOverviewRow(mDetailsContent); + setDetailsOverviewRow(DetailsContent + .createFromRecordedProgram(getContext(), mRecordedProgram)); } @Override @@ -87,26 +85,7 @@ public class RecordedProgramDetailsFragment extends DvrDetailsFragment protected boolean onLoadRecordingDetails(Bundle args) { long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID); mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId); - if (mRecordedProgram == null) { - // notify super class to end activity before initializing anything - return false; - } - mDetailsContent = createDetailsContent(); - return true; - } - - private DetailsContent createDetailsContent() { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(mRecordedProgram.getChannelId()); - String description = TextUtils.isEmpty(mRecordedProgram.getLongDescription()) - ? mRecordedProgram.getDescription() : mRecordedProgram.getLongDescription(); - return new DetailsContent.Builder() - .setTitle(getTitleFromProgram(mRecordedProgram, channel)) - .setStartTimeUtcMillis(mRecordedProgram.getStartTimeUtcMillis()) - .setEndTimeUtcMillis(mRecordedProgram.getEndTimeUtcMillis()) - .setDescription(description) - .setImageUris(mRecordedProgram, channel) - .build(); + return mRecordedProgram != null; } @Override diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java new file mode 100644 index 00000000..5fe162b6 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java @@ -0,0 +1,142 @@ +/* + * 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.content.Context; +import android.media.tv.TvInputManager; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.util.Utils; + +/** + * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}. + */ +public class RecordedProgramPresenter extends DvrItemPresenter<RecordedProgram> { + private final DvrWatchedPositionManager mDvrWatchedPositionManager; + private String mTodayString; + private String mYesterdayString; + private final int mProgressBarColor; + private final boolean mShowEpisodeTitle; + private final boolean mExpandTitleWhenFocused; + + protected final class RecordedProgramViewHolder extends DvrItemViewHolder + implements WatchedPositionChangedListener { + private RecordedProgram mProgram; + private boolean mShowProgress; + + public RecordedProgramViewHolder(RecordingCardView view, Integer progressColor) { + super(view); + if (progressColor == null) { + mShowProgress = false; + } else { + mShowProgress = true; + view.setProgressBarColor(progressColor); + } + } + + private void setProgressBar(long watchedPositionMs) { + ((RecordingCardView) view).setProgressBar( + (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) ? null + : Math.min(100, (int) (100.0f * watchedPositionMs + / mProgram.getDurationMillis()))); + } + + @Override + public void onWatchedPositionChanged(long programId, long positionMs) { + if (programId == mProgram.getId()) { + setProgressBar(positionMs); + } + } + + @Override + protected void onBound(RecordedProgram program) { + mProgram = program; + if (mShowProgress) { + mDvrWatchedPositionManager.addListener(this, program.getId()); + setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId())); + } else { + getView().setProgressBar(null); + } + } + + @Override + protected void onUnbound() { + if (mShowProgress) { + mDvrWatchedPositionManager.removeListener(this, mProgram.getId()); + } + getView().reset(); + } + } + + RecordedProgramPresenter(Context context, boolean showEpisodeTitle, + boolean expandTitleWhenFocused) { + super(context); + mTodayString = mContext.getString(R.string.dvr_date_today); + mYesterdayString = mContext.getString(R.string.dvr_date_yesterday); + mDvrWatchedPositionManager = + TvApplication.getSingletons(mContext).getDvrWatchedPositionManager(); + mProgressBarColor = mContext.getResources() + .getColor(R.color.play_controls_progress_bar_watched); + mShowEpisodeTitle = showEpisodeTitle; + mExpandTitleWhenFocused = expandTitleWhenFocused; + } + + public RecordedProgramPresenter(Context context) { + this(context, false, false); + } + + @Override + public DvrItemViewHolder onCreateDvrItemViewHolder() { + return new RecordedProgramViewHolder( + new RecordingCardView(mContext, mExpandTitleWhenFocused), mProgressBarColor); + } + + @Override + public void onBindDvrItemViewHolder(DvrItemViewHolder baseHolder, RecordedProgram program) { + final RecordedProgramViewHolder viewHolder = (RecordedProgramViewHolder) baseHolder; + final RecordingCardView cardView = viewHolder.getView(); + DetailsContent details = DetailsContent.createFromRecordedProgram(mContext, program); + cardView.setTitle(mShowEpisodeTitle ? + program.getEpisodeDisplayTitle(mContext) : details.getTitle()); + cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo()); + cardView.setContent(generateMajorContent(program), generateMinorContent(program)); + cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri()); + } + + private String generateMajorContent(RecordedProgram program) { + int dateDifference = Utils.computeDateDifference(program.getStartTimeUtcMillis(), + System.currentTimeMillis()); + if (dateDifference == 0) { + return mTodayString; + } else if (dateDifference == 1) { + return mYesterdayString; + } else { + return Utils.getDurationString(mContext, program.getStartTimeUtcMillis(), + program.getStartTimeUtcMillis(), false, true, false, 0); + } + } + + private String generateMinorContent(RecordedProgram program) { + int durationMinutes = Math.max(1, Utils.getRoundOffMinsFromMs(program.getDurationMillis())); + return mContext.getResources().getQuantityString( + R.plurals.dvr_program_duration, durationMinutes, durationMinutes); + } +} diff --git a/src/com/android/tv/dvr/ui/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java index 51c3b03b..767addc8 100644 --- a/src/com/android/tv/dvr/ui/RecordingCardView.java +++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java @@ -14,51 +14,70 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; +import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.support.annotation.Nullable; import android.support.v17.leanback.widget.BaseCardView; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import com.android.tv.R; -import com.android.tv.dvr.RecordedProgram; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.ui.ViewUtils; import com.android.tv.util.ImageLoader; /** - * A CardView for displaying info about a {@link com.android.tv.dvr.ScheduledRecording} or - * {@link RecordedProgram} or - * {@link com.android.tv.dvr.SeriesRecording}. + * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording} + * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}. */ -class RecordingCardView extends BaseCardView { +public class RecordingCardView extends BaseCardView { + // This value should be the same with + // android.support.v17.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS + private final static int ANIMATION_DURATION = 150; private final ImageView mImageView; private final int mImageWidth; private final int mImageHeight; private String mImageUri; - private final TextView mTitleView; 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; + private final TextView mExpandedTitleView; + private final ValueAnimator mExpandTitleAnimator; + private final int mFoldedTitleHeight; + private final int mExpandedTitleHeight; + private final boolean mExpandTitleWhenFocused; + private boolean mExpanded; + private String mDetailBackgroundImageUri; - RecordingCardView(Context context) { - this(context, - context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_width), - context.getResources().getDimensionPixelSize(R.dimen.dvr_card_image_layout_height)); + public RecordingCardView(Context context) { + this(context, false); } - RecordingCardView(Context context, int imageWidth, int imageHeight) { + public RecordingCardView(Context context, boolean expandTitleWhenFocused) { + this(context, context.getResources().getDimensionPixelSize( + R.dimen.dvr_library_card_image_layout_width), context.getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height), + expandTitleWhenFocused); + } + + public RecordingCardView(Context context, int imageWidth, int imageHeight, + boolean expandTitleWhenFocused) { super(context); //TODO(dvr): move these to the layout XML. setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA); @@ -75,13 +94,81 @@ class RecordingCardView extends BaseCardView { mProgressBar = (ProgressBar) findViewById(R.id.recording_progress); mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container); mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon); - mTitleView = (TextView) findViewById(R.id.title); mMajorContentView = (TextView) findViewById(R.id.content_major); mMinorContentView = (TextView) findViewById(R.id.content_minor); + mTitleArea = (FrameLayout) findViewById(R.id.title_area); + mFoldedTitleView = (TextView) findViewById(R.id.title_one_line); + mExpandedTitleView = (TextView) findViewById(R.id.title_two_lines); + mFoldedTitleHeight = getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height); + mExpandedTitleHeight = getResources() + .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height); + mExpandTitleAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(ANIMATION_DURATION); + mExpandTitleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + float value = (Float) valueAnimator.getAnimatedValue(); + mExpandedTitleView.setAlpha(value); + mFoldedTitleView.setAlpha(1.0f - value); + ViewUtils.setLayoutHeight(mTitleArea, (int) (mFoldedTitleHeight + + (mExpandedTitleHeight - mFoldedTitleHeight) * value)); + } + }); + mExpandTitleWhenFocused = expandTitleWhenFocused; + } + + @Override + protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); + // Preload the background image going to be used in detail fragments here to prevent + // loading and drawing background images during activity transitions. + if (gainFocus) { + if (!TextUtils.isEmpty(mDetailBackgroundImageUri)) { + ImageLoader.loadBitmap(getContext(), mDetailBackgroundImageUri, + Integer.MAX_VALUE, Integer.MAX_VALUE, null); + } + } + if (mExpandTitleWhenFocused) { + if (gainFocus) { + expandTitle(true, true); + } else { + expandTitle(false, true); + } + } + } + + /** + * Expands/folds the title area to show program title with two/one lines. + * + * @param expand {@code true} to expand the title area, or {@code false} to fold it. + * @param withAnimation {@code true} to expand/fold with animation. + */ + public void expandTitle(boolean expand, boolean withAnimation) { + if (expand != mExpanded && mFoldedTitleView.getLayout().getEllipsisCount(0) > 0) { + if (withAnimation) { + if (expand) { + mExpandTitleAnimator.start(); + } else { + mExpandTitleAnimator.reverse(); + } + } else { + if (expand) { + mFoldedTitleView.setAlpha(0.0f); + mExpandedTitleView.setAlpha(1.0f); + ViewUtils.setLayoutHeight(mTitleArea, mExpandedTitleHeight); + } else { + mFoldedTitleView.setAlpha(1.0f); + mExpandedTitleView.setAlpha(0.0f); + ViewUtils.setLayoutHeight(mTitleArea, mFoldedTitleHeight); + } + } + mExpanded = expand; + } } void setTitle(CharSequence title) { - mTitleView.setText(title); + mFoldedTitleView.setText(title); + mExpandedTitleView.setText(title); } void setContent(CharSequence majorContent, CharSequence minorContent) { @@ -118,6 +205,11 @@ class RecordingCardView extends BaseCardView { mProgressBar.getProgressDrawable().setTint(color); } + /** + * Sets the image URI of the poster should be shown on the card view. + + * @param isChannelLogo {@code true} if the image is from channels' logo. + */ void setImageUri(String uri, boolean isChannelLogo) { if (isChannelLogo) { mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); @@ -134,7 +226,7 @@ class RecordingCardView extends BaseCardView { } /** - * Set image to card view. + * Sets the {@link Drawable} of the poster should be shown on the card view. */ public void setImage(Drawable image) { if (image != null) { @@ -142,6 +234,10 @@ 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); @@ -152,6 +248,14 @@ class RecordingCardView extends BaseCardView { } /** + * 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. + */ + public void setDetailBackgroundImageUri(String uri) { + mDetailBackgroundImageUri = uri; + } + + /** * Returns image view. */ public ImageView getImageView() { @@ -178,8 +282,9 @@ class RecordingCardView extends BaseCardView { } public void reset() { - mTitleView.setText(null); + mFoldedTitleView.setText(null); + mExpandedTitleView.setText(null); setContent(null, null); mImageView.setImageDrawable(mDefaultImage); } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java new file mode 100644 index 00000000..56ec357f --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java @@ -0,0 +1,51 @@ +/* + * 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.os.Bundle; +import android.support.v17.leanback.app.DetailsFragment; + +import com.android.tv.TvApplication; +import com.android.tv.dvr.data.ScheduledRecording; + +/** + * {@link DetailsFragment} for recordings in DVR. + */ +abstract class RecordingDetailsFragment extends DvrDetailsFragment { + private ScheduledRecording mRecording; + + @Override + protected void onCreateInternal() { + setDetailsOverviewRow(DetailsContent + .createFromScheduledRecording(getContext(), mRecording)); + } + + @Override + protected boolean onLoadRecordingDetails(Bundle args) { + long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID); + mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager() + .getScheduledRecording(scheduledRecordingId); + return mRecording != null; + } + + /** + * Returns {@link ScheduledRecording} for the current fragment. + */ + public ScheduledRecording getRecording() { + return mRecording; + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java index 60816bb5..958f8bf8 100644 --- a/src/com/android/tv/dvr/ui/ScheduledRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.res.Resources; import android.os.Bundle; @@ -26,7 +26,7 @@ import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ui.DvrUiHelper; /** * {@link RecordingDetailsFragment} for scheduled recording in DVR. @@ -66,7 +66,7 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment adapter.set(ACTION_VIEW_SCHEDULE, mScheduleAction); } adapter.set(ACTION_CANCEL, new Action(ACTION_CANCEL, - res.getString(R.string.epg_dvr_dialog_message_remove_recording_schedule), null, + res.getString(R.string.dvr_detail_cancel_recording), null, res.getDrawable(R.drawable.ic_dvr_cancel_32dp))); return adapter; } diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java new file mode 100644 index 00000000..273d3d19 --- /dev/null +++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java @@ -0,0 +1,138 @@ +/* + * 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.content.Context; +import android.os.Handler; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.util.Utils; + +import java.util.concurrent.TimeUnit; + +/** + * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}. + */ +class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> { + private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); + + private final DvrManager mDvrManager; + private final int mProgressBarColor; + + private final class ScheduledRecordingViewHolder extends DvrItemViewHolder { + private final Handler mHandler = new Handler(); + private ScheduledRecording mScheduledRecording; + private final Runnable mProgressBarUpdater = new Runnable() { + @Override + public void run() { + updateProgressBar(); + mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS); + } + }; + + ScheduledRecordingViewHolder(RecordingCardView view, int progressBarColor) { + super(view); + view.setProgressBarColor(progressBarColor); + } + + @Override + protected void onBound(ScheduledRecording recording) { + mScheduledRecording = recording; + updateProgressBar(); + startUpdateProgressBar(); + } + + @Override + protected void onUnbound() { + stopUpdateProgressBar(); + mScheduledRecording = null; + getView().reset(); + } + + private void updateProgressBar() { + if (mScheduledRecording == null) { + return; + } + int recordingState = mScheduledRecording.getState(); + RecordingCardView cardView = (RecordingCardView) view; + if (recordingState == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { + cardView.setProgressBar(Math.max(0, Math.min((int) (100 * + (System.currentTimeMillis() - mScheduledRecording.getStartTimeMs()) + / mScheduledRecording.getDuration()), 100))); + } else if (recordingState == ScheduledRecording.STATE_RECORDING_FINISHED) { + cardView.setProgressBar(100); + } else { + // Hides progress bar. + cardView.setProgressBar(null); + } + } + + private void startUpdateProgressBar() { + mHandler.post(mProgressBarUpdater); + } + + private void stopUpdateProgressBar() { + mHandler.removeCallbacks(mProgressBarUpdater); + } + } + + public ScheduledRecordingPresenter(Context context) { + super(context); + mDvrManager = TvApplication.getSingletons(mContext).getDvrManager(); + mProgressBarColor = mContext.getResources() + .getColor(R.color.play_controls_recording_icon_color_on_focus); + } + + @Override + public DvrItemViewHolder onCreateDvrItemViewHolder() { + return new ScheduledRecordingViewHolder(new RecordingCardView(mContext), mProgressBarColor); + } + + @Override + public void onBindDvrItemViewHolder(DvrItemViewHolder baseHolder, + ScheduledRecording recording) { + final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder; + final RecordingCardView cardView = viewHolder.getView(); + DetailsContent details = DetailsContent.createFromScheduledRecording(mContext, recording); + cardView.setTitle(details.getTitle()); + cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo()); + cardView.setAffiliatedIcon(mDvrManager.isConflicting(recording) ? + R.drawable.ic_warning_white_32dp : 0); + cardView.setContent(generateMajorContent(recording), null); + cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri()); + } + + private String generateMajorContent(ScheduledRecording recording) { + int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(), + recording.getStartTimeMs()); + if (dateDifference <= 0) { + return mContext.getString(R.string.dvr_date_today_time, + Utils.getDurationString(mContext, recording.getStartTimeMs(), + recording.getEndTimeMs(), false, false, true, 0)); + } else if (dateDifference == 1) { + return mContext.getString(R.string.dvr_date_tomorrow_time, + Utils.getDurationString(mContext, recording.getStartTimeMs(), + recording.getEndTimeMs(), false, false, true, 0)); + } else { + return Utils.getDurationString(mContext, recording.getStartTimeMs(), + recording.getStartTimeMs(), false, true, false, 0); + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java index e9e391d4..c2aa8e98 100644 --- a/src/com/android/tv/dvr/ui/SeriesRecordingDetailsFragment.java +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; import android.content.res.Resources; import android.graphics.drawable.Drawable; @@ -28,7 +28,6 @@ import android.support.v17.leanback.widget.DetailsOverviewRow; import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; -import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; @@ -37,14 +36,12 @@ import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.BaseProgram; -import com.android.tv.data.Channel; import com.android.tv.dvr.DvrDataManager; -import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; import com.android.tv.dvr.DvrWatchedPositionManager; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import 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 java.util.Collections; import java.util.Comparator; @@ -67,7 +64,6 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement // After fragments are created, it should be cleared to save resources. private List<RecordedProgram> mRecordedPrograms; private RecordedProgram mRecommendRecordedProgram; - private DetailsContent mDetailsContent; private int mSeasonRowCount; private SparseArrayObjectAdapter mActionsAdapter; private Action mDeleteAction; @@ -85,7 +81,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement mWatchLabel = getString(R.string.dvr_detail_watch); mResumeLabel = getString(R.string.dvr_detail_series_resume); mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null); - mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true); + mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true, true); super.onCreate(savedInstanceState); } @@ -93,7 +89,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement protected void onCreateInternal() { mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity()) .getDvrWatchedPositionManager(); - setDetailsOverviewRow(mDetailsContent); + setDetailsOverviewRow(DetailsContent.createFromSeriesRecording(getContext(), mSeries)); setupRecordedProgramsRow(); mDvrDataManager.addSeriesRecordingListener(this); mDvrDataManager.addRecordedProgramListener(this); @@ -149,7 +145,6 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement } mRecordedPrograms = mDvrDataManager.getRecordedPrograms(mSeries.getId()); Collections.sort(mRecordedPrograms, RecordedProgram.SEASON_REVERSED_EPISODE_COMPARATOR); - mDetailsContent = createDetailsContent(); return true; } @@ -158,22 +153,10 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement DetailsOverviewRowPresenter rowPresenter) { ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); - presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter()); + presenterSelector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); return presenterSelector; } - private DetailsContent createDetailsContent() { - Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager() - .getChannel(mSeries.getChannelId()); - String description = TextUtils.isEmpty(mSeries.getLongDescription()) - ? mSeries.getDescription() : mSeries.getLongDescription(); - return new DetailsContent.Builder() - .setTitle(mSeries.getTitle()) - .setDescription(description) - .setImageUris(mSeries.getPosterUri(), mSeries.getPhotoUri(), channel) - .build(); - } - @Override protected SparseArrayObjectAdapter onCreateActionsAdapter() { mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector()); @@ -203,10 +186,7 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement mDvrDataManager.removeSeriesRecordingListener(this); mDvrDataManager.removeRecordedProgramListener(this); if (mSeries != null) { - DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager(); - if (dvrManager.canRemoveSeriesRecording(mSeries.getId())) { - dvrManager.removeSeriesRecording(mSeries.getId()); - } + mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeries.getId()); } mRecordedProgramPresenter.unbindAllViewHolders(); } @@ -265,7 +245,6 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { for (SeriesRecording series : seriesRecordings) { if (series.getId() == mSeries.getId()) { - mSeries = null; getActivity().finish(); return; } @@ -372,4 +351,4 @@ public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implement return program.getId(); } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java index c2c0f596..e508259d 100644 --- a/src/com/android/tv/dvr/ui/SeriesRecordingPresenter.java +++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java @@ -14,42 +14,36 @@ * limitations under the License. */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.browse; -import android.app.Activity; import android.content.Context; -import android.media.tv.TvContract; import android.media.tv.TvInputManager; import android.text.TextUtils; -import android.view.ViewGroup; import com.android.tv.ApplicationSingletons; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.data.Channel; -import com.android.tv.data.ChannelDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.RecordedProgramListener; import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrWatchedPositionManager; import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener; -import com.android.tv.dvr.RecordedProgram; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import java.util.List; /** * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}. */ -public class SeriesRecordingPresenter extends DvrItemPresenter { - private final ChannelDataManager mChannelDataManager; +class SeriesRecordingPresenter extends DvrItemPresenter<SeriesRecording> { private final DvrDataManager mDvrDataManager; private final DvrManager mDvrManager; private final DvrWatchedPositionManager mWatchedPositionManager; - private static final class SeriesRecordingViewHolder extends ViewHolder implements + private final class SeriesRecordingViewHolder extends DvrItemViewHolder implements WatchedPositionChangedListener, ScheduledRecordingListener, RecordedProgramListener { private SeriesRecording mSeriesRecording; private RecordingCardView mCardView; @@ -138,7 +132,8 @@ public class SeriesRecordingPresenter extends DvrItemPresenter { // Do nothing } - public void onBound(SeriesRecording seriesRecording) { + @Override + protected void onBound(SeriesRecording seriesRecording) { mSeriesRecording = seriesRecording; mDvrDataManager.addScheduledRecordingListener(this); mDvrDataManager.addRecordedProgramListener(this); @@ -152,10 +147,12 @@ public class SeriesRecordingPresenter extends DvrItemPresenter { updateCardViewContent(); } - public void onUnbound() { + @Override + protected void onUnbound() { mDvrDataManager.removeScheduledRecordingListener(this); mDvrDataManager.removeRecordedProgramListener(this); mWatchedPositionManager.removeListener(this); + getView().reset(); } private void updateCardViewContent() { @@ -186,29 +183,28 @@ public class SeriesRecordingPresenter extends DvrItemPresenter { } public SeriesRecordingPresenter(Context context) { + super(context); ApplicationSingletons singletons = TvApplication.getSingletons(context); - mChannelDataManager = singletons.getChannelDataManager(); mDvrDataManager = singletons.getDvrDataManager(); mDvrManager = singletons.getDvrManager(); mWatchedPositionManager = singletons.getDvrWatchedPositionManager(); } @Override - public ViewHolder onCreateViewHolder(ViewGroup parent) { - Context context = parent.getContext(); - RecordingCardView view = new RecordingCardView(context); - return new SeriesRecordingViewHolder(view, mDvrDataManager, mDvrManager, - mWatchedPositionManager); + public DvrItemViewHolder onCreateDvrItemViewHolder() { + return new SeriesRecordingViewHolder(new RecordingCardView(mContext), mDvrDataManager, + mDvrManager, mWatchedPositionManager); } @Override - public void onBindViewHolder(ViewHolder baseHolder, Object o) { + public void onBindDvrItemViewHolder(DvrItemViewHolder baseHolder, SeriesRecording series) { final SeriesRecordingViewHolder viewHolder = (SeriesRecordingViewHolder) baseHolder; - final SeriesRecording seriesRecording = (SeriesRecording) o; - final RecordingCardView cardView = (RecordingCardView) viewHolder.view; - viewHolder.onBound(seriesRecording); - setTitleAndImage(cardView, seriesRecording); - super.onBindViewHolder(baseHolder, o); + final RecordingCardView cardView = viewHolder.getView(); + viewHolder.onBound(series); + DetailsContent details = DetailsContent.createFromSeriesRecording(mContext, series); + cardView.setTitle(details.getTitle()); + cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo()); + cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri()); } @Override @@ -217,18 +213,4 @@ public class SeriesRecordingPresenter extends DvrItemPresenter { ((SeriesRecordingViewHolder) viewHolder).onUnbound(); super.onUnbindViewHolder(viewHolder); } - - private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) { - cardView.setTitle(recording.getTitle()); - if (recording.getPosterUri() != null) { - cardView.setImageUri(recording.getPosterUri(), false); - } else { - Channel channel = mChannelDataManager.getChannel(recording.getChannelId()); - String imageUri = null; - if (channel != null) { - imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString(); - } - cardView.setImageUri(imageUri, true); - } - } } diff --git a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java index d28f026c..b9407b15 100644 --- a/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/BaseDvrSchedulesFragment.java @@ -29,7 +29,7 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; /** * A base fragment to show the list of schedule recordings. @@ -40,7 +40,8 @@ public abstract class BaseDvrSchedulesFragment extends DetailsFragment /** * The key for scheduled recording which has be selected in the list. */ - public static String SCHEDULES_KEY_SCHEDULED_RECORDING = "schedules_key_scheduled_recording"; + public static final String SCHEDULES_KEY_SCHEDULED_RECORDING = + "schedules_key_scheduled_recording"; private ScheduleRowAdapter mRowsAdapter; private TextView mEmptyInfoScreenView; diff --git a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java index f6e6ac26..a0410bb3 100644 --- a/src/com/android/tv/dvr/ui/DvrSchedulesActivity.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesActivity.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.list; import android.app.Activity; import android.app.ProgressDialog; @@ -24,15 +24,13 @@ import android.support.annotation.IntDef; import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.Program; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.SeriesRecordingScheduler; -import com.android.tv.dvr.ui.list.DvrSchedulesFragment; -import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; +import com.android.tv.dvr.recorder.SeriesRecordingScheduler; +import com.android.tv.dvr.ui.BigArguments; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -72,33 +70,47 @@ public class DvrSchedulesActivity extends Activity { getFragmentManager().beginTransaction().add( R.id.fragment_container, schedulesFragment).commit(); } else if (scheduleType == TYPE_SERIES_SCHEDULE) { - final ProgressDialog dialog = ProgressDialog.show(this, null, getString( - R.string.dvr_series_schedules_progress_message_reading_programs)); - SeriesRecording seriesRecording = getIntent().getExtras() - .getParcelable(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_RECORDING); - // To get programs faster, hold the update of the series schedules. - SeriesRecordingScheduler.getInstance(this).pauseUpdate(); - new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { - @Override - protected void onPostExecute(List<Program> programs) { - SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this).resumeUpdate(); - dialog.dismiss(); - Bundle args = getIntent().getExtras(); - args.putParcelableArrayList(DvrSeriesSchedulesFragment - .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, new ArrayList<>(programs)); - DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); - schedulesFragment.setArguments(args); - getFragmentManager().beginTransaction().add( - R.id.fragment_container, schedulesFragment).commit(); - } - }.setLoadCurrentProgram(true) - .setLoadDisallowedProgram(true) - .setLoadScheduledEpisode(true) - .setIgnoreChannelOption(true) - .execute(); + if (BigArguments.getArgument(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_PROGRAMS) != null) { + // The programs will be passed to the DvrSeriesSchedulesFragment, so don't need + // to reset the BigArguments. + showDvrSeriesSchedulesFragment(getIntent().getExtras()); + } else { + final ProgressDialog dialog = ProgressDialog.show(this, null, getString( + R.string.dvr_series_progress_message_reading_programs)); + SeriesRecording seriesRecording = getIntent().getExtras() + .getParcelable(DvrSeriesSchedulesFragment + .SERIES_SCHEDULES_KEY_SERIES_RECORDING); + // To get programs faster, hold the update of the series schedules. + SeriesRecordingScheduler.getInstance(this).pauseUpdate(); + new EpisodicProgramLoadTask(this, Collections.singletonList(seriesRecording)) { + @Override + protected void onPostExecute(List<Program> programs) { + SeriesRecordingScheduler.getInstance(DvrSchedulesActivity.this) + .resumeUpdate(); + dialog.dismiss(); + Bundle args = getIntent().getExtras(); + BigArguments.reset(); + BigArguments.setArgument( + DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_PROGRAMS, + programs == null ? Collections.EMPTY_LIST : programs); + showDvrSeriesSchedulesFragment(args); + } + }.setLoadCurrentProgram(true) + .setLoadDisallowedProgram(true) + .setLoadScheduledEpisode(true) + .setIgnoreChannelOption(true) + .execute(); + } } else { finish(); } } + + private void showDvrSeriesSchedulesFragment(Bundle args) { + DvrSeriesSchedulesFragment schedulesFragment = new DvrSeriesSchedulesFragment(); + schedulesFragment.setArguments(args); + getFragmentManager().beginTransaction().add( + R.id.fragment_container, schedulesFragment).commit(); + } } diff --git a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java index 722c9b6e..3cbb500a 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSchedulesFragment.java @@ -18,12 +18,9 @@ package com.android.tv.dvr.ui.list; import android.os.Bundle; import android.support.v17.leanback.widget.ClassPresenterSelector; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; import com.android.tv.R; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter; /** diff --git a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java index 42a1e72b..57e7a88f 100644 --- a/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java +++ b/src/com/android/tv/dvr/ui/list/DvrSeriesSchedulesFragment.java @@ -17,6 +17,7 @@ package com.android.tv.dvr.ui.list; import android.annotation.TargetApi; +import android.content.Context; import android.database.ContentObserver; import android.media.tv.TvContract.Programs; import android.net.Uri; @@ -35,11 +36,13 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.Program; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener; -import com.android.tv.dvr.EpisodicProgramLoadTask; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.provider.EpisodicProgramLoadTask; +import com.android.tv.dvr.ui.BigArguments; +import java.util.Collections; import java.util.List; /** @@ -47,20 +50,22 @@ import java.util.List; */ @TargetApi(Build.VERSION_CODES.N) public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { - private static final String TAG = "DvrSeriesSchedulesFragment"; /** * The key for series recording whose scheduled recording list will be displayed. + * Type: {@link SeriesRecording} */ public static final String SERIES_SCHEDULES_KEY_SERIES_RECORDING = "series_schedules_key_series_recording"; /** - * The key for programs belong to the series recording whose scheduled recording - * list will be displayed. + * The key for programs which belong to the series recording whose scheduled recording list + * will be displayed. + * Type: List<{@link Program}> */ public static final String SERIES_SCHEDULES_KEY_SERIES_PROGRAMS = "series_schedules_key_series_programs"; private ChannelDataManager mChannelDataManager; + private DvrDataManager mDvrDataManager; private SeriesRecording mSeriesRecording; private List<Program> mPrograms; private EpisodicProgramLoadTask mProgramLoadTask; @@ -87,20 +92,22 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { && getRowsAdapter() instanceof SeriesScheduleRowAdapter) { ((SeriesScheduleRowAdapter) getRowsAdapter()) .onSeriesRecordingUpdated(r); + mSeriesRecording = r; + updateEmptyMessage(); return; } } } }; - private final ContentObserver mContentObserver = - new ContentObserver(new Handler(Looper.getMainLooper())) { - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - executeProgramLoadingTask(); - } - }; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final ContentObserver mContentObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + executeProgramLoadingTask(); + } + }; private final ChannelDataManager.Listener mChannelListener = new ChannelDataManager.Listener() { @Override @@ -120,17 +127,28 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { } @Override - public void onCreate(Bundle savedInstanceState) { + public void onAttach(Context context) { + super.onAttach(context); Bundle args = getArguments(); if (args != null) { mSeriesRecording = args.getParcelable(SERIES_SCHEDULES_KEY_SERIES_RECORDING); - mPrograms = args.getParcelableArrayList(SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); + mPrograms = (List<Program>) BigArguments.getArgument( + SERIES_SCHEDULES_KEY_SERIES_PROGRAMS); + BigArguments.reset(); } + if (args == null || mPrograms == null) { + getActivity().finish(); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ApplicationSingletons singletons = TvApplication.getSingletons(getContext()); - singletons.getDvrDataManager().addSeriesRecordingListener(mSeriesRecordingListener); mChannelDataManager = singletons.getChannelDataManager(); mChannelDataManager.addListener(mChannelListener); + mDvrDataManager = singletons.getDvrDataManager(); + mDvrDataManager.addSeriesRecordingListener(mSeriesRecordingListener); getContext().getContentResolver().registerContentObserver(Programs.CONTENT_URI, true, mContentObserver); } @@ -144,8 +162,16 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { private void onProgramsUpdated() { ((SeriesScheduleRowAdapter) getRowsAdapter()).setPrograms(mPrograms); + updateEmptyMessage(); + } + + private void updateEmptyMessage() { if (mPrograms == null || mPrograms.isEmpty()) { - showEmptyMessage(R.string.dvr_series_schedules_empty_state); + if (mSeriesRecording.getState() == SeriesRecording.STATE_SERIES_STOPPED) { + showEmptyMessage(R.string.dvr_series_schedules_stopped_empty_state); + } else { + showEmptyMessage(R.string.dvr_series_schedules_empty_state); + } } else { hideEmptyMessage(); } @@ -158,15 +184,15 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { mProgramLoadTask = null; } getContext().getContentResolver().unregisterContentObserver(mContentObserver); + mHandler.removeCallbacksAndMessages(null); mChannelDataManager.removeListener(mChannelListener); - TvApplication.getSingletons(getContext()).getDvrDataManager() - .removeSeriesRecordingListener(mSeriesRecordingListener); + mDvrDataManager.removeSeriesRecordingListener(mSeriesRecordingListener); super.onDestroy(); } @Override public SchedulesHeaderRowPresenter onCreateHeaderRowPresenter() { - return new SeriesRecordingHeaderRowPresenter(getContext()); + return new SchedulesHeaderRowPresenter.SeriesRecordingHeaderRowPresenter(getContext()); } @Override @@ -195,7 +221,7 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { mProgramLoadTask = new EpisodicProgramLoadTask(getContext(), mSeriesRecording) { @Override protected void onPostExecute(List<Program> programs) { - mPrograms = programs; + mPrograms = programs == null ? Collections.EMPTY_LIST : programs; onProgramsUpdated(); } }; @@ -205,4 +231,4 @@ public class DvrSeriesSchedulesFragment extends BaseDvrSchedulesFragment { .setIgnoreChannelOption(true) .execute(); } -}
\ No newline at end of file +} diff --git a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java index 23aebf59..2af832ec 100644 --- a/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java +++ b/src/com/android/tv/dvr/ui/list/EpisodicProgramRow.java @@ -19,13 +19,14 @@ package com.android.tv.dvr.ui.list; import android.content.Context; import com.android.tv.data.Program; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.ScheduledRecording.Builder; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording.Builder; +import com.android.tv.dvr.ui.DvrUiHelper; /** * A class for the episodic program. */ -public class EpisodicProgramRow extends ScheduleRow { +class EpisodicProgramRow extends ScheduleRow { private final String mInputId; private final Program mProgram; @@ -65,7 +66,7 @@ public class EpisodicProgramRow extends ScheduleRow { @Override public String getProgramTitleWithEpisodeNumber(Context context) { - return mProgram.getTitleWithEpisodeNumber(context); + return DvrUiHelper.getStyledTitleWithEpisodeNumber(context, mProgram, 0).toString(); } @Override diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java index 3fc92e8a..91ba393a 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java @@ -20,12 +20,13 @@ import android.content.Context; import android.support.annotation.Nullable; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; /** * A class for schedule recording row. */ -public class ScheduleRow { +class ScheduleRow { private final SchedulesHeaderRow mHeaderRow; @Nullable private ScheduledRecording mSchedule; private boolean mStopRecordingRequested; @@ -166,7 +167,8 @@ public class ScheduleRow { * Returns the program title with episode number. */ public String getProgramTitleWithEpisodeNumber(Context context) { - return mSchedule != null ? mSchedule.getProgramTitleWithEpisodeNumber(context) : null; + return mSchedule != null ? DvrUiHelper.getStyledTitleWithEpisodeNumber(context, + mSchedule, 0).toString() : null; } /** diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java index 9cc82653..97d60473 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java @@ -30,8 +30,8 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.Utils; import java.util.ArrayList; @@ -43,7 +43,7 @@ import java.util.concurrent.TimeUnit; /** * An adapter for {@link ScheduleRow}. */ -public class ScheduleRowAdapter extends ArrayObjectAdapter { +class ScheduleRowAdapter extends ArrayObjectAdapter { private static final String TAG = "ScheduleRowAdapter"; private static final boolean DEBUG = false; diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java index 1257e725..dc4e3c41 100644 --- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java @@ -42,25 +42,24 @@ import com.android.tv.R; import com.android.tv.TvApplication; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; +import com.android.tv.dialog.HalfSizedDialogFragment; import com.android.tv.dvr.DvrManager; import com.android.tv.dvr.DvrScheduleManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.dvr.ui.DvrStopRecordingFragment; -import com.android.tv.dvr.ui.HalfSizedDialogFragment; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; -import java.util.concurrent.TimeUnit; /** * A RowPresenter for {@link ScheduleRow}. */ @TargetApi(Build.VERSION_CODES.N) -public class ScheduleRowPresenter extends RowPresenter { +class ScheduleRowPresenter extends RowPresenter { private static final String TAG = "ScheduleRowPresenter"; @Retention(RetentionPolicy.SOURCE) @@ -345,7 +344,9 @@ public class ScheduleRowPresenter extends RowPresenter { viewHolder.mInfoContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - onInfoClicked(row); + if (isInfoClickable(row)) { + onInfoClicked(row); + } } }); @@ -366,8 +367,7 @@ public class ScheduleRowPresenter extends RowPresenter { viewHolder.mTimeView.setText(onGetRecordingTimeText(row)); String programInfoText = onGetProgramInfoText(row); if (TextUtils.isEmpty(programInfoText)) { - int durationMins = - Math.max((int) TimeUnit.MILLISECONDS.toMinutes(row.getDuration()), 1); + int durationMins = Math.max(1, Utils.getRoundOffMinsFromMs(row.getDuration())); programInfoText = mContext.getResources().getQuantityString( R.plurals.dvr_schedules_recording_duration, durationMins, durationMins); } @@ -403,6 +403,7 @@ public class ScheduleRowPresenter extends RowPresenter { } else { viewHolder.whiteBackInfo(); } + viewHolder.mInfoContainer.setFocusable(isInfoClickable(row)); updateActionContainer(viewHolder, viewHolder.isSelected()); } @@ -454,11 +455,13 @@ public class ScheduleRowPresenter extends RowPresenter { /** * Called when user click Info in {@link ScheduleRow}. */ - protected void onInfoClicked(ScheduleRow scheduleRow) { - ScheduledRecording schedule = scheduleRow.getSchedule(); - if (schedule != null) { - DvrUiHelper.startDetailsActivity((Activity) mContext, schedule, null, true); - } + protected void onInfoClicked(ScheduleRow row) { + DvrUiHelper.startDetailsActivity((Activity) mContext, row.getSchedule(), null, true); + } + + private boolean isInfoClickable(ScheduleRow row) { + return row.getSchedule() != null + && (row.getSchedule().isNotStarted() || row.getSchedule().isInProgress()); } /** @@ -545,7 +548,7 @@ public class ScheduleRowPresenter extends RowPresenter { // This row has been deleted. return; } - if (row.isOnAir() && row.isRecordingInProgress() && !row.isStopRecordingRequested()) { + if (row.isRecordingInProgress() && !row.isStopRecordingRequested()) { row.setStopRecordingRequested(true); mDvrManager.stopRecording(row.getSchedule()); CharSequence deletedInfo = onGetProgramInfoText(row); @@ -670,10 +673,9 @@ public class ScheduleRowPresenter extends RowPresenter { hideActionView(viewHolder.mFirstActionContainer, View.GONE); } }; - if (mLastFocusedViewId == R.id.action_first_container - || mLastFocusedViewId == R.id.action_second_container) { - mLastFocusedViewId = R.id.info_container; - } + mLastFocusedViewId = R.id.info_container; + SoftPreconditions.checkState(viewHolder.mInfoContainer.isFocusable(), TAG, + "No focusable view in this row: " + viewHolder); break; } View view = viewHolder.view.findViewById(mLastFocusedViewId); @@ -683,8 +685,10 @@ public class ScheduleRowPresenter extends RowPresenter { // requestFocus() explicitly. if (view.hasFocus()) { viewHolder.mPendingAnimationRunnable.run(); - } else { + } else if (view.isFocusable()){ view.requestFocus(); + } else { + viewHolder.view.requestFocus(); } } } else { @@ -737,10 +741,10 @@ public class ScheduleRowPresenter extends RowPresenter { @ScheduleRowAction protected int[] getAvailableActions(ScheduleRow row) { if (row.getSchedule() != null) { - if (row.isOnAir()) { - if (row.isRecordingInProgress()) { - return new int[] {ACTION_STOP_RECORDING}; - } else if (row.isRecordingNotStarted()) { + if (row.isRecordingInProgress()) { + return new int[]{ACTION_STOP_RECORDING}; + } else if (row.isOnAir()) { + if (row.isRecordingNotStarted()) { if (canResolveConflict()) { // The "START" action can change the conflict states. return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING}; diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java index 0fb0924d..715ecb8c 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRow.java @@ -16,12 +16,15 @@ package com.android.tv.dvr.ui.list; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.data.Program; +import com.android.tv.dvr.data.SeriesRecording; + +import java.util.List; /** * A base class for the rows for schedules' header. */ -public abstract class SchedulesHeaderRow { +abstract class SchedulesHeaderRow { private String mTitle; private String mDescription; private int mItemCount; @@ -98,11 +101,20 @@ public abstract class SchedulesHeaderRow { */ public static class SeriesRecordingHeaderRow extends SchedulesHeaderRow { private SeriesRecording mSeriesRecording; + private List<Program> mPrograms; public SeriesRecordingHeaderRow(String title, String description, int itemCount, - SeriesRecording series) { + SeriesRecording series, List<Program> programs) { super(title, description, itemCount); mSeriesRecording = series; + mPrograms = programs; + } + + /** + * Returns the list of programs which belong to the series. + */ + public List<Program> getPrograms() { + return mPrograms; } /** @@ -119,4 +131,4 @@ public abstract class SchedulesHeaderRow { mSeriesRecording = seriesRecording; } } -} +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java index 69c33a96..fe2033ba 100644 --- a/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SchedulesHeaderRowPresenter.java @@ -30,15 +30,14 @@ import android.widget.TextView; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.SeriesRecording; -import com.android.tv.dvr.ui.DvrSchedulesActivity; +import com.android.tv.dvr.data.SeriesRecording; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; /** * A base class for RowPresenter for {@link SchedulesHeaderRow} */ -public abstract class SchedulesHeaderRowPresenter extends RowPresenter { +abstract class SchedulesHeaderRowPresenter extends RowPresenter { private Context mContext; public SchedulesHeaderRowPresenter(Context context) { @@ -79,7 +78,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } /** - * A presenter for {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}. + * A presenter for {@link SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowPresenter extends SchedulesHeaderRowPresenter { public DateHeaderRowPresenter(Context context) { @@ -93,7 +92,7 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { /** * A ViewHolder for - * {@link com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow}. + * {@link SchedulesHeaderRow.DateHeaderRow}. */ public static class DateHeaderRowViewHolder extends SchedulesHeaderRowViewHolder { public DateHeaderRowViewHolder(Context context, ViewGroup parent) { @@ -152,9 +151,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { headerViewHolder.mSeriesSettingsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { - // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), null, false, false, false); + header.getSeriesRecording().getId(), + header.getPrograms(), false, false, false, null); } }); headerViewHolder.mToggleStartStopButton.setOnClickListener(new OnClickListener() { @@ -169,9 +168,9 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { .build(); TvApplication.getSingletons(getContext()).getDvrManager() .updateSeriesRecording(seriesRecording); - // TODO: pass channel list for settings. DvrUiHelper.startSeriesSettingsActivity(getContext(), - header.getSeriesRecording().getId(), null, false, false, false); + header.getSeriesRecording().getId(), + header.getPrograms(), false, false, false, null); } else { DvrUiHelper.showCancelAllSeriesRecordingDialog( (DvrSchedulesActivity) view.getContext(), @@ -182,11 +181,8 @@ public abstract class SchedulesHeaderRowPresenter extends RowPresenter { } private void setTextDrawable(TextView textView, Drawable drawableStart) { - if (mLtr) { - textView.setCompoundDrawablesWithIntrinsicBounds(drawableStart, null, null, null); - } else { - textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableStart, null); - } + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableStart, null, null, + null); } /** diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java index 3b493774..6b6de8b8 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowAdapter.java @@ -31,8 +31,8 @@ import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Program; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; -import com.android.tv.dvr.SeriesRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.data.SeriesRecording; import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; import com.android.tv.util.Utils; @@ -46,7 +46,7 @@ import java.util.Map; * An adapter for series schedule row. */ @TargetApi(Build.VERSION_CODES.N) -public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { +class SeriesScheduleRowAdapter extends ScheduleRowAdapter { private static final String TAG = "SeriesRowAdapter"; private static final boolean DEBUG = false; @@ -96,7 +96,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { Collections.sort(sortedPrograms); List<EpisodicProgramRow> rows = new ArrayList<>(); mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), - null, sortedPrograms.size(), mSeriesRecording); + null, sortedPrograms.size(), mSeriesRecording, programs); for (Program program : sortedPrograms) { ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(program.getId()); @@ -145,7 +145,7 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { if (index != -1) { EpisodicProgramRow row = (EpisodicProgramRow) get(index); if (!row.isStartRecordingRequested()) { - row.setSchedule(schedule); + setScheduleToRow(row, schedule); notifyArrayItemRangeChanged(index, 1); } } @@ -195,12 +195,10 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { if (!isStartOrStopRequested()) { executePendingUpdate(); } - row.setSchedule(schedule); + setScheduleToRow(row, schedule); } - } else if (willBeKept(schedule)) { - row.setSchedule(schedule); } else { - row.setSchedule(null); + setScheduleToRow(row, schedule); } notifyArrayItemRangeChanged(index, 1); } @@ -213,6 +211,14 @@ public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { } } + private void setScheduleToRow(ScheduleRow row, ScheduledRecording schedule) { + if (schedule != null && willBeKept(schedule)) { + row.setSchedule(schedule); + } else { + row.setSchedule(null); + } + } + private int findRowIndexByProgramId(long programId) { for (int i = 0; i < size(); i++) { Object item = get(i); diff --git a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java index 5d88579a..c8503e0d 100644 --- a/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java +++ b/src/com/android/tv/dvr/ui/list/SeriesScheduleRowPresenter.java @@ -22,13 +22,13 @@ import android.view.ViewGroup; import com.android.tv.R; import com.android.tv.common.SoftPreconditions; -import com.android.tv.dvr.DvrUiHelper; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.util.Utils; /** * A RowPresenter for series schedule row. */ -public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { +class SeriesScheduleRowPresenter extends ScheduleRowPresenter { private static final String TAG = "SeriesRowPresenter"; private boolean mLtr; @@ -74,13 +74,8 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { viewHolder.getProgramTitleView().setCompoundDrawablePadding(getContext() .getResources().getDimensionPixelOffset( R.dimen.dvr_schedules_warning_icon_padding)); - if (mLtr) { - viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( - R.drawable.ic_warning_gray600_36dp, 0, 0, 0); - } else { - viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds( - 0, 0, R.drawable.ic_warning_gray600_36dp, 0); - } + viewHolder.getProgramTitleView().setCompoundDrawablesRelativeWithIntrinsicBounds( + R.drawable.ic_warning_gray600_36dp, 0, 0, 0); } else { viewHolder.getProgramTitleView().setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); } @@ -88,9 +83,7 @@ public class SeriesScheduleRowPresenter extends ScheduleRowPresenter { @Override protected void onInfoClicked(ScheduleRow row) { - if (row.getSchedule() != null) { - DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); - } + DvrUiHelper.startSchedulesActivity(getContext(), row.getSchedule()); } @Override diff --git a/src/com/android/tv/dvr/DvrPlaybackActivity.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java index 5deda44a..6824cfe2 100644 --- a/src/com/android/tv/dvr/DvrPlaybackActivity.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackActivity.java @@ -14,32 +14,38 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.ui.playback; import android.app.Activity; +import android.content.ContentUris; import android.content.Intent; import android.content.res.Configuration; +import android.net.Uri; import android.os.Bundle; import android.util.Log; import com.android.tv.R; import com.android.tv.TvApplication; -import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; +import com.android.tv.dialog.PinDialogFragment.OnPinCheckedListener; +import com.android.tv.dvr.data.RecordedProgram; +import com.android.tv.util.Utils; /** * Activity to play a {@link RecordedProgram}. */ -public class DvrPlaybackActivity extends Activity { +public class DvrPlaybackActivity extends Activity implements OnPinCheckedListener { private static final String TAG = "DvrPlaybackActivity"; private static final boolean DEBUG = false; private DvrPlaybackOverlayFragment mOverlayFragment; + private OnPinCheckedListener mOnPinCheckedListener; @Override public void onCreate(Bundle savedInstanceState) { TvApplication.setCurrentRunningProcess(this, true); if (DEBUG) Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); + setIntent(createProgramIntent(getIntent())); setContentView(R.layout.activity_dvr_playback); mOverlayFragment = (DvrPlaybackOverlayFragment) getFragmentManager() .findFragmentById(R.id.dvr_playback_controls_fragment); @@ -54,7 +60,8 @@ public class DvrPlaybackActivity extends Activity { @Override protected void onNewIntent(Intent intent) { - mOverlayFragment.onNewIntent(intent); + setIntent(createProgramIntent(intent)); + mOverlayFragment.onNewIntent(createProgramIntent(intent)); } @Override @@ -64,4 +71,24 @@ public class DvrPlaybackActivity extends Activity { mOverlayFragment.onWindowSizeChanged((int) (newConfig.screenWidthDp * density), (int) (newConfig.screenHeightDp * density)); } + + 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); + } + return intent; + } + + @Override + public void onPinChecked(boolean checked, int type, String rating) { + if (mOnPinCheckedListener != null) { + mOnPinCheckedListener.onPinChecked(checked, type, rating); + } + } + + void setOnPinCheckListener(OnPinCheckedListener listener) { + mOnPinCheckedListener = listener; + } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java new file mode 100644 index 00000000..8ef0041d --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackCardPresenter.java @@ -0,0 +1,45 @@ +/* + * 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.playback; + +import android.content.Context; + +import com.android.tv.R; +import com.android.tv.dvr.ui.browse.RecordedProgramPresenter; +import com.android.tv.dvr.ui.browse.RecordingCardView; + +/** + * This class is used to generate Views and bind Objects for related recordings in DVR playback. + */ +class DvrPlaybackCardPresenter extends RecordedProgramPresenter { + private final int mRelatedRecordingCardWidth; + private final int mRelatedRecordingCardHeight; + + DvrPlaybackCardPresenter(Context context) { + super(context); + mRelatedRecordingCardWidth = + context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_width); + mRelatedRecordingCardHeight = + context.getResources().getDimensionPixelSize(R.dimen.dvr_related_recordings_height); + } + + @Override + public DvrItemViewHolder onCreateDvrItemViewHolder() { + return new RecordedProgramViewHolder(new RecordingCardView( + getContext(), mRelatedRecordingCardWidth, mRelatedRecordingCardHeight, true), null); + } +} diff --git a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java index 0bc4ecb1..1a6ae187 100644 --- a/src/com/android/tv/dvr/ui/DvrPlaybackControlHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackControlHelper.java @@ -14,66 +14,80 @@ * limitations under the License */ -package com.android.tv.dvr.ui; +package com.android.tv.dvr.ui.playback; import android.app.Activity; +import android.content.Context; import android.graphics.drawable.Drawable; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaController.TransportControls; import android.media.session.PlaybackState; -import android.support.v17.leanback.app.PlaybackControlGlue; +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v17.leanback.media.PlaybackControlGlue; import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; import android.support.v17.leanback.widget.Action; -import android.support.v17.leanback.widget.OnActionClickedListener; +import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.PlaybackControlsRow; +import android.support.v17.leanback.widget.PlaybackControlsRow.ClosedCaptioningAction; +import android.support.v17.leanback.widget.PlaybackControlsRow.MultiAction; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; import android.support.v17.leanback.widget.RowPresenter; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.View; - import com.android.tv.R; import com.android.tv.util.TimeShiftUtils; +import java.util.ArrayList; /** * A helper class to assist {@link DvrPlaybackOverlayFragment} to manage its controls row and * send command to the media controller. It also helps to update playback states displayed in the * fragment according to information the media session provides. */ -public class DvrPlaybackControlHelper extends PlaybackControlGlue { - private static final String TAG = "DvrPlaybackControlHelper"; +class DvrPlaybackControlHelper extends PlaybackControlGlue { + private static final String TAG = "DvrPlaybackControlHelpr"; private static final boolean DEBUG = false; - /** - * Indicates the ID of the media under playback is unknown. - */ - public static int UNKNOWN_MEDIA_ID = -1; + private static final int AUDIO_ACTION_ID = 1001; private int mPlaybackState = PlaybackState.STATE_NONE; private int mPlaybackSpeedLevel; private int mPlaybackSpeedId; private boolean mReadyToControl; + private final DvrPlaybackOverlayFragment mFragment; private final MediaController mMediaController; private final MediaController.Callback mMediaControllerCallback = new MediaControllerCallback(); private final TransportControls mTransportControls; private final int mExtraPaddingTopForNoDescription; + private final MultiAction mClosedCaptioningAction; + private final MultiAction mMultiAudioAction; + private ArrayObjectAdapter mSecondaryActionsAdapter; - public DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { - super(activity, overlayFragment, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); + DvrPlaybackControlHelper(Activity activity, DvrPlaybackOverlayFragment overlayFragment) { + super(activity, new int[TimeShiftUtils.MAX_SPEED_LEVEL + 1]); + mFragment = overlayFragment; mMediaController = activity.getMediaController(); mMediaController.registerCallback(mMediaControllerCallback); mTransportControls = mMediaController.getTransportControls(); mExtraPaddingTopForNoDescription = activity.getResources() .getDimensionPixelOffset(R.dimen.dvr_playback_controls_extra_padding_top); + mClosedCaptioningAction = new ClosedCaptioningAction(activity); + mMultiAudioAction = new MultiAudioAction(activity); + createControlsRowPresenter(); } - @Override - public PlaybackControlsRowPresenter createControlsRowAndPresenter() { + void createControlsRow() { PlaybackControlsRow controlsRow = new PlaybackControlsRow(this); setControlsRow(controlsRow); + mSecondaryActionsAdapter = (ArrayObjectAdapter) controlsRow.getSecondaryActionsAdapter(); + } + + private void createControlsRowPresenter() { AbstractDetailsDescriptionPresenter detailsPresenter = new AbstractDetailsDescriptionPresenter() { @Override @@ -112,30 +126,31 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { .getColor(R.color.play_controls_progress_bar_watched)); presenter.setBackgroundColor(getContext().getResources() .getColor(R.color.play_controls_body_background_enabled)); - presenter.setOnActionClickedListener(new OnActionClickedListener() { - @Override - public void onActionClicked(Action action) { - if (mReadyToControl) { - DvrPlaybackControlHelper.super.onActionClicked(action); - } - } - }); - return presenter; + setControlsRowPresenter(presenter); } @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { + public void onActionClicked(Action action) { if (mReadyToControl) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE && event.getAction() == KeyEvent.ACTION_DOWN - && (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING - || mPlaybackState == PlaybackState.STATE_REWINDING)) { - // Workaround of b/31489271. Clicks play/pause button first to reset play controls - // to "play" state. Then we can pass MEDIA_PAUSE to let playback be paused. - onActionClicked(getControlsRow().getActionForKeyCode(keyCode)); + int trackType; + if (action.getId() == mClosedCaptioningAction.getId()) { + trackType = TvTrackInfo.TYPE_SUBTITLE; + } else if (action.getId() == AUDIO_ACTION_ID) { + trackType = TvTrackInfo.TYPE_AUDIO; + } else { + super.onActionClicked(action); + return; + } + ArrayList<TvTrackInfo> trackInfos = mFragment.getTracks(trackType); + if (!trackInfos.isEmpty()) { + showSideFragment(trackInfos, mFragment.getSelectedTrackId(trackType)); } - return super.onKey(v, keyCode, event); } - return false; + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + return mReadyToControl && super.onKey(v, keyCode, event); } @Override @@ -158,10 +173,10 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { /** * Returns the ID of the media under playback. */ - public long getMediaId() { + public String getMediaId() { MediaMetadata mediaMetadata = mMediaController.getMetadata(); - return mediaMetadata == null ? UNKNOWN_MEDIA_ID - : mediaMetadata.getLong(MediaMetadata.METADATA_KEY_MEDIA_ID); + return mediaMetadata == null ? null + : mediaMetadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID); } @Override @@ -213,12 +228,45 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { /** * Unregister media controller's callback. */ - public void unregisterCallback() { + void unregisterCallback() { mMediaController.unregisterCallback(mMediaControllerCallback); } + /** + * Update the secondary controls row. + * @param hasClosedCaption {@code true} to show the closed caption selection button, + * {@code false} to hide it. + * @param hasMultiAudio {@code true} to show the audio track selection button, + * {@code false} to hide it. + */ + void updateSecondaryRow(boolean hasClosedCaption, boolean hasMultiAudio) { + if (hasClosedCaption) { + if (mSecondaryActionsAdapter.indexOf(mClosedCaptioningAction) < 0) { + mSecondaryActionsAdapter.add(0, mClosedCaptioningAction); + } + } else { + mSecondaryActionsAdapter.remove(mClosedCaptioningAction); + } + if (hasMultiAudio) { + if (mSecondaryActionsAdapter.indexOf(mMultiAudioAction) < 0) { + mSecondaryActionsAdapter.add(mMultiAudioAction); + } + } else { + mSecondaryActionsAdapter.remove(mMultiAudioAction); + } + getHost().notifyPlaybackRowChanged(); + } + + @Nullable + Boolean hasSecondaryRow() { + if (mSecondaryActionsAdapter == null) { + return null; + } + return mSecondaryActionsAdapter.size() != 0; + } + @Override - protected void startPlayback(int speedId) { + public void play(int speedId) { if (getCurrentSpeedId() == speedId) { return; } @@ -232,23 +280,16 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { } @Override - protected void pausePlayback() { + public void pause() { mTransportControls.pause(); } - @Override - protected void skipToNext() { - // Do nothing. - } - - @Override - protected void skipToPrevious() { - // Do nothing. - } - - @Override - protected void onRowChanged(PlaybackControlsRow row) { - // Do nothing. + /** + * Notifies closed caption being enabled/disabled to update related UI. + */ + void onSubtitleTrackStateChanged(boolean enabled) { + mClosedCaptioningAction.setIndex(enabled ? + ClosedCaptioningAction.ON : ClosedCaptioningAction.OFF); } private void onStateChanged(int state, long positionMs, int speedLevel) { @@ -297,6 +338,19 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { onStateChanged(); } + private void showSideFragment(ArrayList<TvTrackInfo> trackInfos, String selectedTrackId) { + Bundle args = new Bundle(); + args.putParcelableArrayList(DvrPlaybackSideFragment.TRACK_INFOS, trackInfos); + args.putString(DvrPlaybackSideFragment.SELECTED_TRACK_ID, selectedTrackId); + DvrPlaybackSideFragment sideFragment = new DvrPlaybackSideFragment(); + sideFragment.setArguments(args); + mFragment.getFragmentManager().beginTransaction() + .hide(mFragment) + .replace(R.id.dvr_playback_side_fragment, sideFragment) + .addToBackStack(null) + .commit(); + } + private class MediaControllerCallback extends MediaController.Callback { @Override public void onPlaybackStateChanged(PlaybackState state) { @@ -307,7 +361,13 @@ public class DvrPlaybackControlHelper extends PlaybackControlGlue { @Override public void onMetadataChanged(MediaMetadata metadata) { DvrPlaybackControlHelper.this.onMetadataChanged(); - ((DvrPlaybackOverlayFragment) getFragment()).onMediaControllerUpdated(); + } + } + + private static class MultiAudioAction extends MultiAction { + MultiAudioAction(Context context) { + super(AUDIO_ACTION_ID); + setDrawables(new Drawable[]{context.getDrawable(R.drawable.ic_tvoption_multi_track)}); } } }
\ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java index 9759a856..843d2dbe 100644 --- a/src/com/android/tv/dvr/DvrPlaybackMediaSessionHelper.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackMediaSessionHelper.java @@ -14,7 +14,7 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.ui.playback; import android.app.Activity; import android.content.Intent; @@ -31,14 +31,16 @@ import android.text.TextUtils; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; -import com.android.tv.dvr.ui.DvrPlaybackOverlayFragment; +import com.android.tv.dvr.DvrWatchedPositionManager; +import com.android.tv.dvr.data.RecordedProgram; import com.android.tv.util.ImageLoader; import com.android.tv.util.TimeShiftUtils; import com.android.tv.util.Utils; -public class DvrPlaybackMediaSessionHelper { +class DvrPlaybackMediaSessionHelper { private static final String TAG = "DvrPlaybackMediaSessionHelper"; private static final boolean DEBUG = false; @@ -102,6 +104,7 @@ public class DvrPlaybackMediaSessionHelper { } if (mMediaSession != null) { mMediaSession.release(); + mMediaSession = null; } } @@ -179,83 +182,88 @@ public class DvrPlaybackMediaSessionHelper { cardTitleText = (channel != null) ? channel.getDisplayName() : mActivity.getString(R.string.no_program_information); } - updateMediaMetadata(program.getId(), cardTitleText, program.getDescription(), - mProgramDurationMs, null, 0); + final MediaMetadata currentMetadata = updateMetadataTextInfo(program.getId(), cardTitleText, + program.getDescription(), mProgramDurationMs); String posterArtUri = program.getPosterArtUri(); if (posterArtUri == null) { posterArtUri = TvContract.buildChannelLogoUri(program.getChannelId()).toString(); } - updatePosterArt(program, cardTitleText, program.getDescription(), - mProgramDurationMs, null, posterArtUri); + updatePosterArt(program, currentMetadata, null, posterArtUri); mMediaSession.setActive(true); } - private void updatePosterArt(RecordedProgram program, String cardTitleText, - String cardSubtitleText, long duration, + private void updatePosterArt(RecordedProgram program, MediaMetadata currentMetadata, @Nullable Bitmap posterArt, @Nullable String posterArtUri) { if (posterArt != null) { - updateMediaMetadata(program.getId(), cardTitleText, - cardSubtitleText, duration, posterArt, 0); + updateMetadataImageInfo(program, currentMetadata, posterArt, 0); } else if (posterArtUri != null) { ImageLoader.loadBitmap(mActivity, posterArtUri, mNowPlayingCardWidth, - mNowPlayingCardHeight, new ProgramPosterArtCallback( - mActivity, program, cardTitleText, cardSubtitleText, duration)); + mNowPlayingCardHeight, + new ProgramPosterArtCallback(mActivity, program, currentMetadata)); } else { - updateMediaMetadata(program.getId(), cardTitleText, - cardSubtitleText, duration, null, R.drawable.default_now_card); + updateMetadataImageInfo(program, currentMetadata, null, R.drawable.default_now_card); } } private class ProgramPosterArtCallback extends ImageLoader.ImageLoaderCallback<Activity> { - private RecordedProgram mRecordedProgram; - private String mCardTitleText; - private String mCardSubtitleText; - private long mDuration; + private final RecordedProgram mRecordedProgram; + private final MediaMetadata mCurrentMetadata; public ProgramPosterArtCallback(Activity activity, RecordedProgram program, - String cardTitleText, String cardSubtitleText, long duration) { + MediaMetadata metadata) { super(activity); mRecordedProgram = program; - mCardTitleText = cardTitleText; - mCardSubtitleText = cardSubtitleText; - mDuration = duration; + mCurrentMetadata = metadata; } @Override public void onBitmapLoaded(Activity activity, @Nullable Bitmap posterArt) { if (isCurrentProgram(mRecordedProgram)) { - updatePosterArt(mRecordedProgram, mCardTitleText, - mCardSubtitleText, mDuration, posterArt, null); + updatePosterArt(mRecordedProgram, mCurrentMetadata, posterArt, null); } } } - private void updateMediaMetadata(final long programId, final String title, - final String subtitle, final long duration, - final Bitmap posterArt, final int imageResId) { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... arg0) { - MediaMetadata.Builder builder = new MediaMetadata.Builder(); - builder.putLong(MediaMetadata.METADATA_KEY_MEDIA_ID, programId) - .putString(MediaMetadata.METADATA_KEY_TITLE, title) - .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); - if (subtitle != null) { - builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); - } - Bitmap programPosterArt = posterArt; - if (programPosterArt == null && imageResId != 0) { - programPosterArt = - BitmapFactory.decodeResource(mActivity.getResources(), imageResId); - } - if (programPosterArt != null) { - builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); - } + private MediaMetadata updateMetadataTextInfo(final long programId, final String title, + final String subtitle, final long duration) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(); + builder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(programId)) + .putString(MediaMetadata.METADATA_KEY_TITLE, title) + .putLong(MediaMetadata.METADATA_KEY_DURATION, duration); + if (subtitle != null) { + builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, subtitle); + } + MediaMetadata metadata = builder.build(); + mMediaSession.setMetadata(metadata); + return metadata; + } + + private void updateMetadataImageInfo(final RecordedProgram program, + final MediaMetadata currentMetadata, final Bitmap posterArt, final int imageResId) { + if (mMediaSession != null && (posterArt != null || imageResId != 0)) { + MediaMetadata.Builder builder = new MediaMetadata.Builder(currentMetadata); + if (posterArt != null) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, posterArt); mMediaSession.setMetadata(builder.build()); - return null; + } else { + new AsyncTask<Void, Void, Bitmap>() { + @Override + protected Bitmap doInBackground(Void... arg0) { + return BitmapFactory.decodeResource(mActivity.getResources(), imageResId); + } + + @Override + protected void onPostExecute(Bitmap programPosterArt) { + if (mMediaSession != null && programPosterArt != null + && isCurrentProgram(program)) { + builder.putBitmap(MediaMetadata.METADATA_KEY_ART, programPosterArt); + mMediaSession.setMetadata(builder.build()); + } + } + }.execute(); } - }.execute(); + } } // An event was triggered by MediaController.TransportControls and must be handled here. diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java new file mode 100644 index 00000000..783ae682 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackOverlayFragment.java @@ -0,0 +1,494 @@ +/* + * 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.playback; + +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.graphics.Point; +import android.hardware.display.DisplayManager; +import android.media.tv.TvContentRating; +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.media.session.PlaybackState; +import android.media.tv.TvInputManager; +import android.media.tv.TvView; +import android.support.v17.leanback.app.PlaybackFragment; +import android.support.v17.leanback.app.PlaybackFragmentGlueHost; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.BaseOnItemViewClickedListener; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.support.v17.leanback.widget.HeaderItem; +import android.support.v17.leanback.widget.ListRow; +import android.support.v17.leanback.widget.Presenter; +import android.support.v17.leanback.widget.RowPresenter; +import android.support.v17.leanback.widget.SinglePresenterSelector; +import android.view.Display; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; +import android.util.Log; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.data.BaseProgram; +import com.android.tv.dialog.PinDialogFragment; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.data.RecordedProgram; +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.util.TvSettings; +import com.android.tv.util.TvTrackInfoUtils; +import com.android.tv.util.Utils; + +import java.util.List; +import java.util.ArrayList; + +public class DvrPlaybackOverlayFragment extends PlaybackFragment { + // TODO: Handles audio focus. Deals with block and ratings. + private static final String TAG = "DvrPlaybackOverlayFrag"; + private static final boolean DEBUG = false; + + private static final String MEDIA_SESSION_TAG = "com.android.tv.dvr.mediasession"; + private static final float DISPLAY_ASPECT_RATIO_EPSILON = 0.01f; + + // mProgram is only used to store program from intent. Don't use it elsewhere. + private RecordedProgram mProgram; + private DvrPlayer mDvrPlayer; + private DvrPlaybackMediaSessionHelper mMediaSessionHelper; + private DvrPlaybackControlHelper mPlaybackControlHelper; + private ArrayObjectAdapter mRowsAdapter; + private SortedArrayAdapter<BaseProgram> mRelatedRecordingsRowAdapter; + private DvrPlaybackCardPresenter mRelatedRecordingCardPresenter; + private DvrDataManager mDvrDataManager; + private ContentRatingsManager mContentRatingsManager; + private TvView mTvView; + private View mBlockScreenView; + private ListRow mRelatedRecordingsRow; + private int mVerticalPaddingBase; + private int mPaddingWithoutRelatedRow; + private int mPaddingWithoutSecondaryRow; + private int mWindowWidth; + private int mWindowHeight; + private float mAppliedAspectRatio; + private float mWindowAspectRatio; + private boolean mPinChecked; + private boolean mStarted; + private DvrPlayer.OnTrackSelectedListener mOnSubtitleTrackSelectedListener = + new DvrPlayer.OnTrackSelectedListener() { + @Override + public void onTrackSelected(String selectedTrackId) { + mPlaybackControlHelper.onSubtitleTrackStateChanged(selectedTrackId != null); + mRowsAdapter.notifyArrayItemRangeChanged(0, 1); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + if (DEBUG) Log.d(TAG, "onCreate"); + super.onCreate(savedInstanceState); + mVerticalPaddingBase = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_base); + mPaddingWithoutRelatedRow = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_related_row); + mPaddingWithoutSecondaryRow = getActivity().getResources() + .getDimensionPixelOffset(R.dimen.dvr_playback_overlay_padding_top_no_secondary_row); + mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager(); + mContentRatingsManager = TvApplication.getSingletons(getContext()) + .getTvInputManagerHelper().getContentRatingsManager(); + if (!mDvrDataManager.isRecordedProgramLoadFinished()) { + mDvrDataManager.addRecordedProgramLoadFinishedListener( + new DvrDataManager.OnRecordedProgramLoadFinishedListener() { + @Override + public void onRecordedProgramLoadFinished() { + mDvrDataManager.removeRecordedProgramLoadFinishedListener(this); + if (handleIntent(getActivity().getIntent(), true)) { + setUpRows(); + preparePlayback(getActivity().getIntent()); + } + } + } + ); + } else if (!handleIntent(getActivity().getIntent(), true)) { + return; + } + Point size = new Point(); + ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) + .getDisplay(Display.DEFAULT_DISPLAY).getSize(size); + mWindowWidth = size.x; + mWindowHeight = size.y; + mWindowAspectRatio = mAppliedAspectRatio = (float) mWindowWidth / mWindowHeight; + setBackgroundType(PlaybackFragment.BG_LIGHT); + setFadingEnabled(true); + } + + @Override + public void onStart() { + super.onStart(); + mStarted = true; + updateVerticalPosition(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mTvView = (TvView) getActivity().findViewById(R.id.dvr_tv_view); + mBlockScreenView = getActivity().findViewById(R.id.block_screen); + mDvrPlayer = new DvrPlayer(mTvView); + mMediaSessionHelper = new DvrPlaybackMediaSessionHelper( + getActivity(), MEDIA_SESSION_TAG, mDvrPlayer, this); + mPlaybackControlHelper = new DvrPlaybackControlHelper(getActivity(), this); + mRelatedRecordingsRow = getRelatedRecordingsRow(); + mDvrPlayer.setOnTracksAvailabilityChangedListener( + new DvrPlayer.OnTracksAvailabilityChangedListener() { + @Override + public void onTracksAvailabilityChanged(boolean hasClosedCaption, + boolean hasMultiAudio) { + mPlaybackControlHelper.updateSecondaryRow(hasClosedCaption, hasMultiAudio); + if (hasClosedCaption) { + mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, + mOnSubtitleTrackSelectedListener); + selectBestMatchedTrack(TvTrackInfo.TYPE_SUBTITLE); + } else { + mDvrPlayer.setOnTrackSelectedListener(TvTrackInfo.TYPE_SUBTITLE, null); + } + if (hasMultiAudio) { + selectBestMatchedTrack(TvTrackInfo.TYPE_AUDIO); + } + updateVerticalPosition(); + mPlaybackControlHelper.getHost().notifyPlaybackRowChanged(); + } + }); + mDvrPlayer.setOnAspectRatioChangedListener(new DvrPlayer.OnAspectRatioChangedListener() { + @Override + public void onAspectRatioChanged(float videoAspectRatio) { + updateAspectRatio(videoAspectRatio); + } + }); + mPinChecked = getActivity().getIntent() + .getBooleanExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, false); + mDvrPlayer.setOnContentBlockedListener( + new DvrPlayer.OnContentBlockedListener() { + @Override + public void onContentBlocked(TvContentRating contentRating) { + if (mPinChecked) { + mTvView.unblockContent(contentRating); + return; + } + mBlockScreenView.setVisibility(View.VISIBLE); + getActivity().getMediaController().getTransportControls().pause(); + ((DvrPlaybackActivity) getActivity()) + .setOnPinCheckListener( + new PinDialogFragment.OnPinCheckedListener() { + @Override + public void onPinChecked( + boolean checked, int type, String rating) { + ((DvrPlaybackActivity) getActivity()) + .setOnPinCheckListener(null); + if (checked) { + mPinChecked = true; + mTvView.unblockContent(contentRating); + mBlockScreenView.setVisibility(View.GONE); + getActivity() + .getMediaController() + .getTransportControls() + .play(); + } + } + }); + PinDialogFragment.create( + PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_DVR, + contentRating.flattenToString()) + .show( + getActivity().getFragmentManager(), + PinDialogFragment.DIALOG_TAG); + } + }); + setOnItemViewClickedListener(new BaseOnItemViewClickedListener() { + @Override + public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, + RowPresenter.ViewHolder rowViewHolder, Object row) { + if (itemViewHolder.view instanceof RecordingCardView) { + setFadingEnabled(false); + long programId = ((RecordedProgram) itemViewHolder.view.getTag()).getId(); + if (DEBUG) Log.d(TAG, "Play Related Recording:" + programId); + Intent intent = new Intent(getContext(), DvrPlaybackActivity.class); + intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId); + getContext().startActivity(intent); + } + } + }); + if (mProgram != null) { + setUpRows(); + preparePlayback(getActivity().getIntent()); + } + } + + @Override + public void onPause() { + if (DEBUG) Log.d(TAG, "onPause"); + super.onPause(); + if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_FAST_FORWARDING + || mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_REWINDING) { + getActivity().getMediaController().getTransportControls().pause(); + } + if (mMediaSessionHelper.getPlaybackState() == PlaybackState.STATE_NONE) { + getActivity().requestVisibleBehind(false); + } else { + getActivity().requestVisibleBehind(true); + } + } + + @Override + public void onDestroy() { + if (DEBUG) Log.d(TAG, "onDestroy"); + mPlaybackControlHelper.unregisterCallback(); + mMediaSessionHelper.release(); + mRelatedRecordingCardPresenter.unbindAllViewHolders(); + super.onDestroy(); + } + + /** + * Passes the intent to the fragment. + */ + public void onNewIntent(Intent intent) { + if (mDvrDataManager.isRecordedProgramLoadFinished() && handleIntent(intent, false)) { + preparePlayback(intent); + } + } + + /** + * Should be called when windows' size is changed in order to notify DVR player + * to update it's view width/height and position. + */ + public void onWindowSizeChanged(final int windowWidth, final int windowHeight) { + mWindowWidth = windowWidth; + mWindowHeight = windowHeight; + mWindowAspectRatio = (float) mWindowWidth / mWindowHeight; + updateAspectRatio(mAppliedAspectRatio); + } + + /** + * Returns next recorded episode in the same series as now playing program. + */ + public RecordedProgram getNextEpisode(RecordedProgram program) { + int position = mRelatedRecordingsRowAdapter.findInsertPosition(program); + if (position == mRelatedRecordingsRowAdapter.size()) { + return null; + } else { + return (RecordedProgram) mRelatedRecordingsRowAdapter.get(position); + } + } + + /** + * Returns the tracks of the give type of the current playback. + + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. Or returns {@code null}. + */ + public ArrayList<TvTrackInfo> getTracks(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mDvrPlayer.getAudioTracks(); + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mDvrPlayer.getSubtitleTracks(); + } + return null; + } + + /** + * Returns the ID of the selected track of the given type. + */ + public String getSelectedTrackId(int trackType) { + return mDvrPlayer.getSelectedTrackId(trackType); + } + + /** + * Returns the language setting of the given track type. + + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. + * @return {@code null} if no language has been set for the given track type. + */ + TvTrackInfo getTrackSetting(int trackType) { + return TvSettings.getDvrPlaybackTrackSettings(getContext(), trackType); + } + + /** + * Selects the given audio or subtitle track for DVR playback. + * @param trackType Should be {@link TvTrackInfo#TYPE_SUBTITLE} + * or {@link TvTrackInfo#TYPE_AUDIO}. + * @param selectedTrack {@code null} to disable the audio or subtitle track according to + * trackType. + */ + void selectTrack(int trackType, TvTrackInfo selectedTrack) { + if (mDvrPlayer.isPlaybackPrepared()) { + mDvrPlayer.selectTrack(trackType, selectedTrack); + } + } + + private boolean handleIntent(Intent intent, boolean finishActivity) { + mProgram = getProgramFromIntent(intent); + if (mProgram == null) { + Toast.makeText(getActivity(), getString(R.string.dvr_program_not_found), + Toast.LENGTH_SHORT).show(); + if (finishActivity) { + getActivity().finish(); + } + return false; + } + return true; + } + + private void selectBestMatchedTrack(int trackType) { + TvTrackInfo selectedTrack = getTrackSetting(trackType); + if (selectedTrack != null) { + TvTrackInfo bestMatchedTrack = TvTrackInfoUtils.getBestTrackInfo(getTracks(trackType), + selectedTrack.getId(), selectedTrack.getLanguage(), + trackType == TvTrackInfo.TYPE_AUDIO ? selectedTrack.getAudioChannelCount() : 0); + if (bestMatchedTrack != null && (trackType == TvTrackInfo.TYPE_AUDIO || Utils + .isEqualLanguage(bestMatchedTrack.getLanguage(), + selectedTrack.getLanguage()))) { + selectTrack(trackType, bestMatchedTrack); + return; + } + } + if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + // Disables closed captioning if there's no matched language. + selectTrack(TvTrackInfo.TYPE_SUBTITLE, null); + } + } + + private void updateAspectRatio(float videoAspectRatio) { + if (videoAspectRatio <= 0) { + // We don't have video's width or height information, use window's aspect ratio. + videoAspectRatio = mWindowAspectRatio; + } + if (Math.abs(mAppliedAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { + // No need to change + return; + } + if (Math.abs(mWindowAspectRatio - videoAspectRatio) < DISPLAY_ASPECT_RATIO_EPSILON) { + ((ViewGroup) mTvView.getParent()).setPadding(0, 0, 0, 0); + } else if (videoAspectRatio < mWindowAspectRatio) { + int newPadding = (mWindowWidth - Math.round(mWindowHeight * videoAspectRatio)) / 2; + ((ViewGroup) mTvView.getParent()).setPadding(newPadding, 0, newPadding, 0); + } else { + int newPadding = (mWindowHeight - Math.round(mWindowWidth / videoAspectRatio)) / 2; + ((ViewGroup) mTvView.getParent()).setPadding(0, newPadding, 0, newPadding); + } + mAppliedAspectRatio = videoAspectRatio; + } + + private void preparePlayback(Intent intent) { + mMediaSessionHelper.setupPlayback(mProgram, getSeekTimeFromIntent(intent)); + mPlaybackControlHelper.updateSecondaryRow(false, false); + getActivity().getMediaController().getTransportControls().prepare(); + updateRelatedRecordingsRow(); + } + + private void updateRelatedRecordingsRow() { + boolean wasEmpty = (mRelatedRecordingsRowAdapter.size() == 0); + mRelatedRecordingsRowAdapter.clear(); + long programId = mProgram.getId(); + String seriesId = mProgram.getSeriesId(); + SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId); + if (seriesRecording != null) { + if (DEBUG) Log.d(TAG, "Update related recordings with:" + seriesId); + List<RecordedProgram> relatedPrograms = + mDvrDataManager.getRecordedPrograms(seriesRecording.getId()); + for (RecordedProgram program : relatedPrograms) { + if (programId != program.getId()) { + mRelatedRecordingsRowAdapter.add(program); + } + } + } + if (mRelatedRecordingsRowAdapter.size() == 0) { + mRowsAdapter.remove(mRelatedRecordingsRow); + } else if (wasEmpty){ + mRowsAdapter.add(mRelatedRecordingsRow); + } + updateVerticalPosition(); + mRowsAdapter.notifyArrayItemRangeChanged(1, 1); + } + + private void setUpRows() { + mPlaybackControlHelper.createControlsRow(); + mPlaybackControlHelper.setHost(new PlaybackFragmentGlueHost(this)); + mRowsAdapter = (ArrayObjectAdapter) getAdapter(); + ClassPresenterSelector selector = + (ClassPresenterSelector) mRowsAdapter.getPresenterSelector(); + selector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext())); + mRowsAdapter.setPresenterSelector(selector); + if (mStarted) { + // If it's started before setting up rows, vertical position has not been updated and + // should be updated here. + updateVerticalPosition(); + } + } + + private ListRow getRelatedRecordingsRow() { + mRelatedRecordingCardPresenter = new DvrPlaybackCardPresenter(getActivity()); + mRelatedRecordingsRowAdapter = new RelatedRecordingsAdapter(mRelatedRecordingCardPresenter); + HeaderItem header = new HeaderItem(0, + getActivity().getString(R.string.dvr_playback_related_recordings)); + return new ListRow(header, mRelatedRecordingsRowAdapter); + } + + private RecordedProgram getProgramFromIntent(Intent intent) { + long programId = intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, -1); + return mDvrDataManager.getRecordedProgram(programId); + } + + private long getSeekTimeFromIntent(Intent intent) { + return intent.getLongExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, + TvInputManager.TIME_SHIFT_INVALID_TIME); + } + + private void updateVerticalPosition() { + Boolean hasSecondaryRow = mPlaybackControlHelper.hasSecondaryRow(); + if (hasSecondaryRow == null) { + return; + } + + int verticalPadding = mVerticalPaddingBase; + if (mRelatedRecordingsRowAdapter.size() == 0) { + verticalPadding += mPaddingWithoutRelatedRow; + } + if (!hasSecondaryRow) { + verticalPadding += mPaddingWithoutSecondaryRow; + } + Fragment fragment = getChildFragmentManager().findFragmentById(R.id.playback_controls_dock); + View view = fragment == null ? null : fragment.getView(); + if (view != null) { + view.setTranslationY(verticalPadding); + } + } + + private class RelatedRecordingsAdapter extends SortedArrayAdapter<BaseProgram> { + RelatedRecordingsAdapter(DvrPlaybackCardPresenter presenter) { + super(new SinglePresenterSelector(presenter), BaseProgram.EPISODE_COMPARATOR); + } + + @Override + public long getId(BaseProgram item) { + return item.getId(); + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java new file mode 100644 index 00000000..e49870f1 --- /dev/null +++ b/src/com/android/tv/dvr/ui/playback/DvrPlaybackSideFragment.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tv.dvr.ui.playback; + +import android.media.tv.TvTrackInfo; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v17.leanback.app.GuidedStepFragment; +import android.support.v17.leanback.widget.GuidedAction; +import android.text.TextUtils; +import android.transition.Transition; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.tv.R; +import com.android.tv.util.TvSettings; + +import java.util.List; +import java.util.Locale; + +/** + * Fragment for DVR playback closed-caption/multi-audio settings. + */ +public class DvrPlaybackSideFragment extends GuidedStepFragment { + /** + * The tag for passing track infos to side fragments. + */ + public static final String TRACK_INFOS = "dvr_key_track_infos"; + /** + * The tag for passing selected track's ID to side fragments. + */ + public static final String SELECTED_TRACK_ID = "dvr_key_selected_track_id"; + + private static final int ACTION_ID_NO_SUBTITLE = -1; + private static final int CHECK_SET_ID = 1; + + private List<TvTrackInfo> mTrackInfos; + private String mSelectedTrackId; + private TvTrackInfo mSelectedTrack; + private int mTrackType; + private DvrPlaybackOverlayFragment mOverlayFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + mTrackInfos = getArguments().getParcelableArrayList(TRACK_INFOS); + mTrackType = mTrackInfos.get(0).getType(); + mSelectedTrackId = getArguments().getString(SELECTED_TRACK_ID); + mOverlayFragment = ((DvrPlaybackOverlayFragment) getFragmentManager() + .findFragmentById(R.id.dvr_playback_controls_fragment)); + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateBackgroundView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View backgroundView = super.onCreateBackgroundView(inflater, container, savedInstanceState); + backgroundView.setBackgroundColor(getResources() + .getColor(R.color.lb_playback_controls_background_light)); + return backgroundView; + } + + @Override + public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { + if (mTrackType == TvTrackInfo.TYPE_SUBTITLE) { + actions.add(new GuidedAction.Builder(getActivity()) + .id(ACTION_ID_NO_SUBTITLE) + .title(getString(R.string.closed_caption_option_item_off)) + .checkSetId(CHECK_SET_ID) + .checked(mSelectedTrackId == null) + .build()); + } + for (int i = 0; i < mTrackInfos.size(); i++) { + TvTrackInfo info = mTrackInfos.get(i); + boolean checked = TextUtils.equals(info.getId(), mSelectedTrackId); + GuidedAction action = new GuidedAction.Builder(getActivity()) + .id(i) + .title(getTrackLabel(info, i)) + .checkSetId(CHECK_SET_ID) + .checked(checked) + .build(); + actions.add(action); + if (checked) { + mSelectedTrack = info; + } + } + } + + @Override + public void onGuidedActionFocused(GuidedAction action) { + int actionId = (int) action.getId(); + mOverlayFragment.selectTrack(mTrackType, actionId < 0 ? null : mTrackInfos.get(actionId)); + } + + @Override + public void onGuidedActionClicked(GuidedAction action) { + int actionId = (int) action.getId(); + mSelectedTrack = actionId < 0 ? null : mTrackInfos.get(actionId); + TvSettings.setDvrPlaybackTrackSettings(getContext(), mTrackType, mSelectedTrack); + getFragmentManager().popBackStack(); + } + + @Override + public void onStart() { + super.onStart(); + // Workaround: when overlay fragment is faded out, any focus will lost due to overlay + // fragment's implementation. So we disable overlay fragment's fading here to prevent + // losing focus while users are interacting with the side fragment. + mOverlayFragment.setFadingEnabled(false); + } + + @Override + public void onStop() { + super.onStop(); + // We disable fading of overlay fragment to prevent side fragment from losing focus, + // therefore we should resume it here. + mOverlayFragment.setFadingEnabled(true); + mOverlayFragment.selectTrack(mTrackType, mSelectedTrack); + } + + private String getTrackLabel(TvTrackInfo track, int trackIndex) { + if (track.getLanguage() != null) { + return new Locale(track.getLanguage()).getDisplayName(); + } + return track.getType() == TvTrackInfo.TYPE_SUBTITLE ? + getString(R.string.closed_caption_unknown_language, trackIndex + 1) + : getString(R.string.multi_audio_unknown_language); + } + + @Override + protected void onProvideFragmentTransitions() { + super.onProvideFragmentTransitions(); + // Excludes the background scrim from transition to prevent the blinking caused by + // hiding the overlay fragment and sliding in the side fragment at the same time. + Transition t = getEnterTransition(); + if (t != null) { + t.excludeTarget(R.id.guidedstep_background, true); + } + } +}
\ No newline at end of file diff --git a/src/com/android/tv/dvr/DvrPlayer.java b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java index 5656655c..7226c666 100644 --- a/src/com/android/tv/dvr/DvrPlayer.java +++ b/src/com/android/tv/dvr/ui/playback/DvrPlayer.java @@ -14,20 +14,24 @@ * limitations under the License */ -package com.android.tv.dvr; +package com.android.tv.dvr.ui.playback; import android.media.PlaybackParams; +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.media.session.PlaybackState; +import android.text.TextUtils; import android.util.Log; +import com.android.tv.dvr.data.RecordedProgram; + +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -public class DvrPlayer { +class DvrPlayer { private static final String TAG = "DvrPlayer"; private static final boolean DEBUG = false; @@ -47,12 +51,19 @@ public class DvrPlayer { private long mInitialSeekPositionMs; private final TvView mTvView; private DvrPlayerCallback mCallback; - private AspectRatioChangedListener mAspectRatioChangedListener; - private ContentBlockedListener mContentBlockedListener; + private OnAspectRatioChangedListener mOnAspectRatioChangedListener; + private OnContentBlockedListener mOnContentBlockedListener; + private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener; + private OnTrackSelectedListener mOnAudioTrackSelectedListener; + private OnTrackSelectedListener mOnSubtitleTrackSelectedListener; + private String mSelectedAudioTrackId; + private String mSelectedSubtitleTrackId; private float mAspectRatio = Float.NaN; private int mPlaybackState = PlaybackState.STATE_NONE; private long mTimeShiftCurrentPositionMs; private boolean mPauseOnPrepared; + private boolean mHasClosedCaption; + private boolean mHasMultiAudio; private final PlaybackParams mPlaybackParams = new PlaybackParams(); private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback(); private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME; @@ -75,22 +86,40 @@ public class DvrPlayer { public void onPlaybackEnded() { } } - public interface AspectRatioChangedListener { + public interface OnAspectRatioChangedListener { /** * Called when the Video's aspect ratio is changed. + * + * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. + * Listeners should handle it carefully. */ void onAspectRatioChanged(float videoAspectRatio); } - public interface ContentBlockedListener { + public interface OnContentBlockedListener { /** * Called when the Video's aspect ratio is changed. */ void onContentBlocked(TvContentRating rating); } + public interface OnTracksAvailabilityChangedListener { + /** + * Called when the Video's subtitle or audio tracks are changed. + */ + void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio); + } + + public interface OnTrackSelectedListener { + /** + * Called when certain subtitle or audio track is selected. + */ + void onTrackSelected(String selectedTrackId); + } + public DvrPlayer(TvView tvView) { mTvView = tvView; + mTvView.setCaptionEnabled(true); mPlaybackParams.setSpeed(1.0f); setTvViewCallbacks(); setCallback(null); @@ -236,6 +265,8 @@ public class DvrPlayer { mTimeShiftCurrentPositionMs = 0; mPlaybackParams.setSpeed(1.0f); mProgram = null; + mSelectedAudioTrackId = null; + mSelectedSubtitleTrackId = null; } /** @@ -250,17 +281,51 @@ public class DvrPlayer { } /** - * Sets listener to aspect ratio changing. + * Sets the listener to aspect ratio changing. + */ + public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) { + mOnAspectRatioChangedListener = listener; + } + + /** + * Sets the listener to content blocking. */ - public void setAspectRatioChangedListener(AspectRatioChangedListener listener) { - mAspectRatioChangedListener = listener; + public void setOnContentBlockedListener(OnContentBlockedListener listener) { + mOnContentBlockedListener = listener; } /** - * Sets listener to content blocking. + * Sets the listener to tracks changing. */ - public void setContentBlockedListener(ContentBlockedListener listener) { - mContentBlockedListener = listener; + public void setOnTracksAvailabilityChangedListener( + OnTracksAvailabilityChangedListener listener) { + mOnTracksAvailabilityChangedListener = listener; + } + + /** + * Sets the listener to tracks of the given type being selected. + * + * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} + * or {@link TvTrackInfo#TYPE_SUBTITLE}. + */ + public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + mOnAudioTrackSelectedListener = listener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + mOnSubtitleTrackSelectedListener = listener; + } + } + + /** + * Gets the listener to tracks of the given type being selected. + */ + public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mOnAudioTrackSelectedListener; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mOnSubtitleTrackSelectedListener; + } + return null; } /** @@ -306,6 +371,32 @@ public class DvrPlayer { } /** + * Returns the subtitle tracks of the current playback. + */ + public ArrayList<TvTrackInfo> getSubtitleTracks() { + return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE)); + } + + /** + * Returns the audio tracks of the current playback. + */ + public ArrayList<TvTrackInfo> getAudioTracks() { + return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_AUDIO)); + } + + /** + * Returns the ID of the selected track of the given type. + */ + public String getSelectedTrackId(int trackType) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + return mSelectedAudioTrackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + return mSelectedSubtitleTrackId; + } + return null; + } + + /** * Returns if playback of the recorded program is started. */ public boolean isPlaybackPrepared() { @@ -313,6 +404,41 @@ public class DvrPlayer { && mPlaybackState != PlaybackState.STATE_CONNECTING; } + /** + * Selects the given track. + * + * @return ID of the selected track. + */ + String selectTrack(int trackType, TvTrackInfo selectedTrack) { + String oldSelectedTrackId = getSelectedTrackId(trackType); + String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId(); + if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) { + if (selectedTrack == null) { + mTvView.selectTrack(trackType, null); + return null; + } else { + List<TvTrackInfo> tracks = mTvView.getTracks(trackType); + if (tracks != null && tracks.contains(selectedTrack)) { + mTvView.selectTrack(trackType, newSelectedTrackId); + return newSelectedTrackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) { + // Track not found, disabled closed caption. + mTvView.selectTrack(trackType, null); + return null; + } + } + } + return oldSelectedTrackId; + } + + private void setSelectedTrackId(int trackType, String trackId) { + if (trackType == TvTrackInfo.TYPE_AUDIO) { + mSelectedAudioTrackId = trackId; + } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) { + mSelectedSubtitleTrackId = trackId; + } + } + private void setPlaybackSpeed(int speed) { mPlaybackParams.setSpeed(speed); mTvView.timeShiftSetPlaybackParams(mPlaybackParams); @@ -369,28 +495,60 @@ public class DvrPlayer { if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE && mPlaybackState == PlaybackState.STATE_CONNECTING) { mTimeShiftPlayAvailable = true; + if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { + // onTimeShiftStatusChanged is sometimes called after + // onTimeShiftStartPositionChanged is called. In this case, + // resumeToWatchedPositionIfNeeded needs to be called here. + resumeToWatchedPositionIfNeeded(); + } } } @Override - public void onTrackSelected(String inputId, int type, String trackId) { - if (trackId == null || type != TvTrackInfo.TYPE_VIDEO - || mAspectRatioChangedListener == null) { - return; + public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) { + boolean hasClosedCaption = + !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty(); + boolean hasMultiAudio = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1; + if ((hasClosedCaption != mHasClosedCaption || hasMultiAudio != mHasMultiAudio) + && mOnTracksAvailabilityChangedListener != null) { + mOnTracksAvailabilityChangedListener + .onTracksAvailabilityChanged(hasClosedCaption, hasMultiAudio); } - List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); - if (trackInfos != null) { - for (TvTrackInfo trackInfo : trackInfos) { - if (trackInfo.getId().equals(trackId)) { - float videoAspectRatio = trackInfo.getVideoPixelAspectRatio() - * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); - if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); - if (!Float.isNaN(videoAspectRatio) - && mAspectRatio != videoAspectRatio) { - mAspectRatioChangedListener - .onAspectRatioChanged(videoAspectRatio); - mAspectRatio = videoAspectRatio; - return; + mHasClosedCaption = hasClosedCaption; + mHasMultiAudio = hasMultiAudio; + } + + @Override + public void onTrackSelected(String inputId, int type, String trackId) { + if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) { + setSelectedTrackId(type, trackId); + OnTrackSelectedListener listener = getOnTrackSelectedListener(type); + if (listener != null) { + listener.onTrackSelected(trackId); + } + } else if (type == TvTrackInfo.TYPE_VIDEO && trackId != null + && mOnAspectRatioChangedListener != null) { + List<TvTrackInfo> trackInfos = mTvView.getTracks(TvTrackInfo.TYPE_VIDEO); + if (trackInfos != null) { + for (TvTrackInfo trackInfo : trackInfos) { + if (trackInfo.getId().equals(trackId)) { + float videoAspectRatio; + int videoWidth = trackInfo.getVideoWidth(); + int videoHeight = trackInfo.getVideoHeight(); + if (videoWidth > 0 && videoHeight > 0) { + videoAspectRatio = trackInfo.getVideoPixelAspectRatio() + * trackInfo.getVideoWidth() / trackInfo.getVideoHeight(); + } else { + // Aspect ratio is unknown. Pass the message to listeners. + videoAspectRatio = 0; + } + if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio); + if (mAspectRatio != videoAspectRatio || videoAspectRatio == 0) { + mOnAspectRatioChangedListener + .onAspectRatioChanged(videoAspectRatio); + mAspectRatio = videoAspectRatio; + return; + } } } } @@ -399,8 +557,8 @@ public class DvrPlayer { @Override public void onContentBlocked(String inputId, TvContentRating rating) { - if (mContentBlockedListener != null) { - mContentBlockedListener.onContentBlocked(rating); + if (mOnContentBlockedListener != null) { + mOnContentBlockedListener.onContentBlocked(rating); } } }); |