1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.tv.dvr.ui.list; 18 19 import android.annotation.TargetApi; 20 import android.content.Context; 21 import android.os.Build.VERSION_CODES; 22 import android.support.annotation.Nullable; 23 import android.support.v17.leanback.widget.ArrayObjectAdapter; 24 import android.support.v17.leanback.widget.ClassPresenterSelector; 25 import android.text.format.DateUtils; 26 import android.util.Log; 27 import com.android.tv.R; 28 import com.android.tv.TvSingletons; 29 import com.android.tv.common.SoftPreconditions; 30 import com.android.tv.common.util.Clock; 31 import com.android.tv.dvr.DvrDataManager; 32 import com.android.tv.dvr.data.RecordedProgram; 33 import com.android.tv.dvr.data.ScheduledRecording; 34 import com.android.tv.dvr.recorder.ScheduledProgramReaper; 35 import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; 36 import com.android.tv.util.Utils; 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.concurrent.TimeUnit; 42 43 /** An adapter for DVR history. */ 44 @TargetApi(VERSION_CODES.N) 45 @SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated 46 class DvrHistoryRowAdapter extends ArrayObjectAdapter { 47 private static final String TAG = "DvrHistoryRowAdapter"; 48 private static final boolean DEBUG = false; 49 50 private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); 51 private static final int MAX_HISTORY_DAYS = ScheduledProgramReaper.DAYS; 52 53 private final Context mContext; 54 private final Clock mClock; 55 private final DvrDataManager mDvrDataManager; 56 private final List<String> mTitles = new ArrayList<>(); 57 private final Map<Long, ScheduledRecording> mRecordedProgramScheduleMap = new HashMap<>(); 58 59 public DvrHistoryRowAdapter( 60 Context context, ClassPresenterSelector classPresenterSelector, Clock clock) { 61 super(classPresenterSelector); 62 mContext = context; 63 mClock = clock; 64 mDvrDataManager = TvSingletons.getSingletons(mContext).getDvrDataManager(); 65 mTitles.add(mContext.getString(R.string.dvr_date_today)); 66 mTitles.add(mContext.getString(R.string.dvr_date_yesterday)); 67 } 68 69 /** Returns context. */ 70 protected Context getContext() { 71 return mContext; 72 } 73 74 /** Starts row adapter. */ 75 public void start() { 76 clear(); 77 List<ScheduledRecording> recordingList = mDvrDataManager.getFailedScheduledRecordings(); 78 List<RecordedProgram> recordedProgramList = mDvrDataManager.getRecordedPrograms(); 79 80 recordingList.addAll( 81 recordedProgramsToScheduledRecordings(recordedProgramList, MAX_HISTORY_DAYS)); 82 recordingList 83 .sort(ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed()); 84 long deadLine = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis()); 85 for (int i = 0; i < recordingList.size(); ) { 86 ArrayList<ScheduledRecording> section = new ArrayList<>(); 87 while (i < recordingList.size() && recordingList.get(i).getStartTimeMs() >= deadLine) { 88 section.add(recordingList.get(i++)); 89 } 90 if (!section.isEmpty()) { 91 SchedulesHeaderRow headerRow = 92 new DateHeaderRow( 93 calculateHeaderDate(deadLine), 94 mContext.getResources() 95 .getQuantityString( 96 R.plurals.dvr_schedules_section_subtitle, 97 section.size(), 98 section.size()), 99 section.size(), 100 deadLine); 101 add(headerRow); 102 for (ScheduledRecording recording : section) { 103 add(new ScheduleRow(recording, headerRow)); 104 } 105 } 106 deadLine -= ONE_DAY_MS; 107 } 108 } 109 110 private String calculateHeaderDate(long timeMs) { 111 int titleIndex = 112 (int) 113 ((Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis()) - timeMs) 114 / ONE_DAY_MS); 115 String headerDate; 116 if (titleIndex < mTitles.size()) { 117 headerDate = mTitles.get(titleIndex); 118 } else { 119 headerDate = 120 DateUtils.formatDateTime( 121 getContext(), 122 timeMs, 123 DateUtils.FORMAT_SHOW_WEEKDAY 124 | DateUtils.FORMAT_SHOW_DATE 125 | DateUtils.FORMAT_ABBREV_MONTH); 126 } 127 return headerDate; 128 } 129 130 private List<ScheduledRecording> recordedProgramsToScheduledRecordings( 131 List<RecordedProgram> programs, int maxDays) { 132 List<ScheduledRecording> result = new ArrayList<>(); 133 for (RecordedProgram recordedProgram : programs) { 134 ScheduledRecording scheduledRecording = 135 recordedProgramsToScheduledRecordings(recordedProgram, maxDays); 136 if (scheduledRecording != null) { 137 result.add(scheduledRecording); 138 } 139 } 140 return result; 141 } 142 143 @Nullable 144 private ScheduledRecording recordedProgramsToScheduledRecordings( 145 RecordedProgram program, int maxDays) { 146 long firstMillisecondToday = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis()); 147 if (maxDays 148 < Utils.computeDateDifference( 149 program.getStartTimeUtcMillis(), 150 firstMillisecondToday)) { 151 return null; 152 } 153 ScheduledRecording scheduledRecording = ScheduledRecording.builder(program).build(); 154 mRecordedProgramScheduleMap.put(program.getId(), scheduledRecording); 155 return scheduledRecording; 156 } 157 158 public void onScheduledRecordingAdded(ScheduledRecording schedule) { 159 if (DEBUG) { 160 Log.d(TAG, "onScheduledRecordingAdded: " + schedule); 161 } 162 if (findRowByScheduledRecording(schedule) == null 163 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED 164 || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED 165 || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) { 166 addScheduleRow(schedule); 167 } 168 } 169 170 public void onScheduledRecordingAdded(RecordedProgram program) { 171 if (DEBUG) { 172 Log.d(TAG, "onScheduledRecordingAdded: " + program); 173 } 174 if (mRecordedProgramScheduleMap.get(program.getId()) != null) { 175 return; 176 } 177 ScheduledRecording schedule = 178 recordedProgramsToScheduledRecordings(program, MAX_HISTORY_DAYS); 179 if (schedule == null) { 180 return; 181 } 182 addScheduleRow(schedule); 183 } 184 185 public void onScheduledRecordingRemoved(ScheduledRecording schedule) { 186 if (DEBUG) { 187 Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); 188 } 189 ScheduleRow row = findRowByScheduledRecording(schedule); 190 if (row != null) { 191 removeScheduleRow(row); 192 notifyArrayItemRangeChanged(indexOf(row), 1); 193 } 194 } 195 196 public void onScheduledRecordingRemoved(RecordedProgram program) { 197 if (DEBUG) { 198 Log.d(TAG, "onScheduledRecordingRemoved: " + program); 199 } 200 ScheduledRecording scheduledRecording = mRecordedProgramScheduleMap.get(program.getId()); 201 if (scheduledRecording != null) { 202 mRecordedProgramScheduleMap.remove(program.getId()); 203 ScheduleRow row = findRowByRecordedProgram(program); 204 if (row != null) { 205 removeScheduleRow(row); 206 notifyArrayItemRangeChanged(indexOf(row), 1); 207 } 208 } 209 } 210 211 public void onScheduledRecordingUpdated(ScheduledRecording schedule) { 212 if (DEBUG) { 213 Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); 214 } 215 ScheduleRow row = findRowByScheduledRecording(schedule); 216 if (row != null) { 217 row.setSchedule(schedule); 218 if (schedule.getState() != ScheduledRecording.STATE_RECORDING_FAILED) { 219 // Only handle failed schedules. Finished schedules are handled as recorded programs 220 removeScheduleRow(row); 221 } 222 notifyArrayItemRangeChanged(indexOf(row), 1); 223 } 224 } 225 226 public void onScheduledRecordingUpdated(RecordedProgram program) { 227 if (DEBUG) { 228 Log.d(TAG, "onScheduledRecordingUpdated: " + program); 229 } 230 ScheduleRow row = findRowByRecordedProgram(program); 231 if (row != null) { 232 removeScheduleRow(row); 233 notifyArrayItemRangeChanged(indexOf(row), 1); 234 ScheduledRecording schedule = mRecordedProgramScheduleMap.get(program.getId()); 235 if (schedule != null) { 236 mRecordedProgramScheduleMap.remove(program.getId()); 237 } 238 } 239 onScheduledRecordingAdded(program); 240 } 241 242 private void addScheduleRow(ScheduledRecording recording) { 243 // This method must not be called from inherited class. 244 SoftPreconditions.checkState(getClass().equals(DvrHistoryRowAdapter.class)); 245 if (recording != null) { 246 int pre = -1; 247 int index = 0; 248 for (; index < size(); index++) { 249 if (get(index) instanceof ScheduleRow) { 250 ScheduleRow scheduleRow = (ScheduleRow) get(index); 251 if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed() 252 .compare(scheduleRow.getSchedule(), recording) > 0) { 253 break; 254 } 255 pre = index; 256 } 257 } 258 long deadLine = Utils.getFirstMillisecondOfDay(recording.getStartTimeMs()); 259 if (pre >= 0 && getHeaderRow(pre).getDeadLineMs() == deadLine) { 260 SchedulesHeaderRow headerRow = ((ScheduleRow) get(pre)).getHeaderRow(); 261 headerRow.setItemCount(headerRow.getItemCount() + 1); 262 ScheduleRow addedRow = new ScheduleRow(recording, headerRow); 263 add(++pre, addedRow); 264 updateHeaderDescription(headerRow); 265 } else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) { 266 SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow(); 267 headerRow.setItemCount(headerRow.getItemCount() + 1); 268 ScheduleRow addedRow = new ScheduleRow(recording, headerRow); 269 add(index, addedRow); 270 updateHeaderDescription(headerRow); 271 } else { 272 SchedulesHeaderRow headerRow = 273 new DateHeaderRow( 274 calculateHeaderDate(deadLine), 275 mContext.getResources() 276 .getQuantityString( 277 R.plurals.dvr_schedules_section_subtitle, 1, 1), 278 1, 279 deadLine); 280 add(++pre, headerRow); 281 ScheduleRow addedRow = new ScheduleRow(recording, headerRow); 282 add(pre, addedRow); 283 } 284 } 285 } 286 287 private DateHeaderRow getHeaderRow(int index) { 288 return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow()); 289 } 290 291 /** Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to. */ 292 private ScheduleRow findRowByScheduledRecording(ScheduledRecording recording) { 293 if (recording == null) { 294 return null; 295 } 296 for (int i = 0; i < size(); i++) { 297 Object item = get(i); 298 if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) { 299 if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) { 300 return (ScheduleRow) item; 301 } 302 } 303 } 304 return null; 305 } 306 307 private ScheduleRow findRowByRecordedProgram(RecordedProgram program) { 308 if (program == null) { 309 return null; 310 } 311 for (int i = 0; i < size(); i++) { 312 Object item = get(i); 313 if (item instanceof ScheduleRow) { 314 ScheduleRow row = (ScheduleRow) item; 315 if (row.hasRecordedProgram() 316 && row.getSchedule().getRecordedProgramId() == program.getId()) { 317 return (ScheduleRow) item; 318 } 319 } 320 } 321 return null; 322 } 323 324 private void removeScheduleRow(ScheduleRow scheduleRow) { 325 // This method must not be called from inherited class. 326 SoftPreconditions.checkState(getClass().equals(DvrHistoryRowAdapter.class)); 327 if (scheduleRow != null) { 328 scheduleRow.setSchedule(null); 329 SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow(); 330 remove(scheduleRow); 331 // Changes the count information of header which the removed row belongs to. 332 if (headerRow != null) { 333 int currentCount = headerRow.getItemCount(); 334 headerRow.setItemCount(--currentCount); 335 if (headerRow.getItemCount() == 0) { 336 remove(headerRow); 337 } else { 338 replace(indexOf(headerRow), headerRow); 339 updateHeaderDescription(headerRow); 340 } 341 } 342 } 343 } 344 345 private void updateHeaderDescription(SchedulesHeaderRow headerRow) { 346 headerRow.setDescription( 347 mContext.getResources() 348 .getQuantityString( 349 R.plurals.dvr_schedules_section_subtitle, 350 headerRow.getItemCount(), 351 headerRow.getItemCount())); 352 } 353 } 354