1 /* 2 * Copyright (C) 2016 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.content.Context; 20 import android.os.Handler; 21 import android.os.Looper; 22 import android.os.Message; 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.ArraySet; 27 import android.util.Log; 28 29 import com.android.tv.R; 30 import com.android.tv.TvApplication; 31 import com.android.tv.common.SoftPreconditions; 32 import com.android.tv.dvr.DvrManager; 33 import com.android.tv.dvr.ScheduledRecording; 34 import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; 35 import com.android.tv.util.Utils; 36 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.List; 40 import java.util.Set; 41 import java.util.concurrent.TimeUnit; 42 43 /** 44 * An adapter for {@link ScheduleRow}. 45 */ 46 public class ScheduleRowAdapter extends ArrayObjectAdapter { 47 private static final String TAG = "ScheduleRowAdapter"; 48 private static final boolean DEBUG = false; 49 50 private final static long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); 51 52 private static final int MSG_UPDATE_ROW = 1; 53 54 private Context mContext; 55 private final List<String> mTitles = new ArrayList<>(); 56 private final Set<ScheduleRow> mPendingUpdate = new ArraySet<>(); 57 58 private final Handler mHandler = new Handler(Looper.getMainLooper()) { 59 @Override 60 public void handleMessage(Message msg) { 61 if (msg.what == MSG_UPDATE_ROW) { 62 long currentTimeMs = System.currentTimeMillis(); 63 handleUpdateRow(currentTimeMs); 64 sendNextUpdateMessage(currentTimeMs); 65 } 66 } 67 }; 68 ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector)69 public ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector) { 70 super(classPresenterSelector); 71 mContext = context; 72 mTitles.add(mContext.getString(R.string.dvr_date_today)); 73 mTitles.add(mContext.getString(R.string.dvr_date_tomorrow)); 74 } 75 76 /** 77 * Returns context. 78 */ getContext()79 protected Context getContext() { 80 return mContext; 81 } 82 83 /** 84 * Starts schedule row adapter. 85 */ start()86 public void start() { 87 clear(); 88 List<ScheduledRecording> recordingList = TvApplication.getSingletons(mContext) 89 .getDvrDataManager().getNonStartedScheduledRecordings(); 90 recordingList.addAll(TvApplication.getSingletons(mContext).getDvrDataManager() 91 .getStartedRecordings()); 92 Collections.sort(recordingList, 93 ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); 94 long deadLine = Utils.getLastMillisecondOfDay(System.currentTimeMillis()); 95 for (int i = 0; i < recordingList.size();) { 96 ArrayList<ScheduledRecording> section = new ArrayList<>(); 97 while (i < recordingList.size() && recordingList.get(i).getStartTimeMs() < deadLine) { 98 section.add(recordingList.get(i++)); 99 } 100 if (!section.isEmpty()) { 101 SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine), 102 mContext.getResources().getQuantityString( 103 R.plurals.dvr_schedules_section_subtitle, section.size(), section.size()), 104 section.size(), deadLine); 105 add(headerRow); 106 for(ScheduledRecording recording : section){ 107 add(new ScheduleRow(recording, headerRow)); 108 } 109 } 110 deadLine += ONE_DAY_MS; 111 } 112 sendNextUpdateMessage(System.currentTimeMillis()); 113 } 114 calculateHeaderDate(long deadLine)115 private String calculateHeaderDate(long deadLine) { 116 int titleIndex = (int) ((deadLine - 117 Utils.getLastMillisecondOfDay(System.currentTimeMillis())) / ONE_DAY_MS); 118 String headerDate; 119 if (titleIndex < mTitles.size()) { 120 headerDate = mTitles.get(titleIndex); 121 } else { 122 headerDate = DateUtils.formatDateTime(getContext(), deadLine, 123 DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE 124 | DateUtils.FORMAT_ABBREV_MONTH); 125 } 126 return headerDate; 127 } 128 129 /** 130 * Stops schedules row adapter. 131 */ stop()132 public void stop() { 133 mHandler.removeCallbacksAndMessages(null); 134 DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); 135 for (int i = 0; i < size(); i++) { 136 if (get(i) instanceof ScheduleRow) { 137 ScheduleRow row = (ScheduleRow) get(i); 138 if (row.isScheduleCanceled()) { 139 dvrManager.removeScheduledRecording(row.getSchedule()); 140 } 141 } 142 } 143 } 144 145 /** 146 * Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to. 147 */ findRowByScheduledRecording(ScheduledRecording recording)148 public ScheduleRow findRowByScheduledRecording(ScheduledRecording recording) { 149 if (recording == null) { 150 return null; 151 } 152 for (int i = 0; i < size(); i++) { 153 Object item = get(i); 154 if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) { 155 if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) { 156 return (ScheduleRow) item; 157 } 158 } 159 } 160 return null; 161 } 162 findRowWithStartRequest(ScheduledRecording schedule)163 private ScheduleRow findRowWithStartRequest(ScheduledRecording schedule) { 164 for (int i = 0; i < size(); i++) { 165 Object item = get(i); 166 if (!(item instanceof ScheduleRow)) { 167 continue; 168 } 169 ScheduleRow row = (ScheduleRow) item; 170 if (row.getSchedule() != null && row.isStartRecordingRequested() 171 && row.matchSchedule(schedule)) { 172 return row; 173 } 174 } 175 return null; 176 } 177 addScheduleRow(ScheduledRecording recording)178 private void addScheduleRow(ScheduledRecording recording) { 179 // This method must not be called from inherited class. 180 SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class)); 181 if (recording != null) { 182 int pre = -1; 183 int index = 0; 184 for (; index < size(); index++) { 185 if (get(index) instanceof ScheduleRow) { 186 ScheduleRow scheduleRow = (ScheduleRow) get(index); 187 if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.compare( 188 scheduleRow.getSchedule(), recording) > 0) { 189 break; 190 } 191 pre = index; 192 } 193 } 194 long deadLine = Utils.getLastMillisecondOfDay(recording.getStartTimeMs()); 195 if (pre >= 0 && getHeaderRow(pre).getDeadLineMs() == deadLine) { 196 SchedulesHeaderRow headerRow = ((ScheduleRow) get(pre)).getHeaderRow(); 197 headerRow.setItemCount(headerRow.getItemCount() + 1); 198 ScheduleRow addedRow = new ScheduleRow(recording, headerRow); 199 add(++pre, addedRow); 200 updateHeaderDescription(headerRow); 201 } else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) { 202 SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow(); 203 headerRow.setItemCount(headerRow.getItemCount() + 1); 204 ScheduleRow addedRow = new ScheduleRow(recording, headerRow); 205 add(index, addedRow); 206 updateHeaderDescription(headerRow); 207 } else { 208 SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine), 209 mContext.getResources().getQuantityString( 210 R.plurals.dvr_schedules_section_subtitle, 1, 1), 1, deadLine); 211 add(++pre, headerRow); 212 ScheduleRow addedRow = new ScheduleRow(recording, headerRow); 213 add(pre, addedRow); 214 } 215 } 216 } 217 getHeaderRow(int index)218 private DateHeaderRow getHeaderRow(int index) { 219 return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow()); 220 } 221 removeScheduleRow(ScheduleRow scheduleRow)222 private void removeScheduleRow(ScheduleRow scheduleRow) { 223 // This method must not be called from inherited class. 224 SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class)); 225 if (scheduleRow != null) { 226 scheduleRow.setSchedule(null); 227 SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow(); 228 remove(scheduleRow); 229 // Changes the count information of header which the removed row belongs to. 230 if (headerRow != null) { 231 int currentCount = headerRow.getItemCount(); 232 headerRow.setItemCount(--currentCount); 233 if (headerRow.getItemCount() == 0) { 234 remove(headerRow); 235 } else { 236 replace(indexOf(headerRow), headerRow); 237 updateHeaderDescription(headerRow); 238 } 239 } 240 } 241 } 242 updateHeaderDescription(SchedulesHeaderRow headerRow)243 private void updateHeaderDescription(SchedulesHeaderRow headerRow) { 244 headerRow.setDescription(mContext.getResources().getQuantityString( 245 R.plurals.dvr_schedules_section_subtitle, 246 headerRow.getItemCount(), headerRow.getItemCount())); 247 } 248 249 /** 250 * Called when a schedule recording is added to dvr date manager. 251 */ onScheduledRecordingAdded(ScheduledRecording schedule)252 public void onScheduledRecordingAdded(ScheduledRecording schedule) { 253 if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule); 254 ScheduleRow row = findRowWithStartRequest(schedule); 255 // If the start recording is requested, onScheduledRecordingAdded is called with NOT_STARTED 256 // state. And then onScheduleRecordingUpdated will be called with IN_PROGRESS. 257 // It happens in a short time and causes blinking. To avoid this intermediate state change, 258 // update the row in onScheduleRecordingUpdated when the state changes to IN_PROGRESS 259 // instead of in this method. 260 if (row == null) { 261 addScheduleRow(schedule); 262 sendNextUpdateMessage(System.currentTimeMillis()); 263 } 264 } 265 266 /** 267 * Called when a schedule recording is removed from dvr date manager. 268 */ onScheduledRecordingRemoved(ScheduledRecording schedule)269 public void onScheduledRecordingRemoved(ScheduledRecording schedule) { 270 if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); 271 ScheduleRow row = findRowByScheduledRecording(schedule); 272 if (row != null) { 273 removeScheduleRow(row); 274 notifyArrayItemRangeChanged(indexOf(row), 1); 275 sendNextUpdateMessage(System.currentTimeMillis()); 276 } 277 } 278 279 /** 280 * Called when a schedule recording is updated in dvr date manager. 281 */ onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange)282 public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) { 283 if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); 284 ScheduleRow row = findRowByScheduledRecording(schedule); 285 if (row != null) { 286 if (conflictChange && isStartOrStopRequested()) { 287 // Delay the conflict update until it gets the response of the start/stop request. 288 // The purpose is to avoid the intermediate conflict change. 289 addPendingUpdate(row); 290 return; 291 } 292 if (row.isStopRecordingRequested()) { 293 // Wait until the recording is finished 294 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED 295 || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED 296 || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { 297 row.setStopRecordingRequested(false); 298 if (!isStartOrStopRequested()) { 299 executePendingUpdate(); 300 } 301 row.setSchedule(schedule); 302 } 303 } else { 304 row.setSchedule(schedule); 305 if (!willBeKept(schedule)) { 306 removeScheduleRow(row); 307 } 308 } 309 notifyArrayItemRangeChanged(indexOf(row), 1); 310 sendNextUpdateMessage(System.currentTimeMillis()); 311 } else { 312 row = findRowWithStartRequest(schedule); 313 // When the start recording was requested, we give the highest priority. So it is 314 // guaranteed that the state will be changed from NOT_STARTED to the other state. 315 // Update the row with the next state not to show the intermediate state which causes 316 // blinking. 317 if (row != null 318 && schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) { 319 // This can be called multiple times, so do not call 320 // ScheduleRow.setStartRecordingRequested(false) here. 321 row.setStartRecordingRequested(false); 322 if (!isStartOrStopRequested()) { 323 executePendingUpdate(); 324 } 325 row.setSchedule(schedule); 326 notifyArrayItemRangeChanged(indexOf(row), 1); 327 sendNextUpdateMessage(System.currentTimeMillis()); 328 } 329 } 330 } 331 332 /** 333 * Checks if there is a row which requested start/stop recording. 334 */ isStartOrStopRequested()335 protected boolean isStartOrStopRequested() { 336 for (int i = 0; i < size(); i++) { 337 Object item = get(i); 338 if (item instanceof ScheduleRow) { 339 ScheduleRow row = (ScheduleRow) item; 340 if (row.isStartRecordingRequested() || row.isStopRecordingRequested()) { 341 return true; 342 } 343 } 344 } 345 return false; 346 } 347 348 /** 349 * Delays update of the row. 350 */ addPendingUpdate(ScheduleRow row)351 protected void addPendingUpdate(ScheduleRow row) { 352 mPendingUpdate.add(row); 353 } 354 355 /** 356 * Executes the pending updates. 357 */ executePendingUpdate()358 protected void executePendingUpdate() { 359 for (ScheduleRow row : mPendingUpdate) { 360 int index = indexOf(row); 361 if (index != -1) { 362 notifyArrayItemRangeChanged(index, 1); 363 } 364 } 365 mPendingUpdate.clear(); 366 } 367 368 /** 369 * To check whether the recording should be kept or not. 370 */ willBeKept(ScheduledRecording schedule)371 protected boolean willBeKept(ScheduledRecording schedule) { 372 // CANCELED state means that the schedule was removed temporarily, which should be shown 373 // in the list so that the user can reschedule it. 374 return schedule.getEndTimeMs() > System.currentTimeMillis() 375 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS 376 || schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED 377 || schedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED); 378 } 379 380 /** 381 * Handle the message to update/remove rows. 382 */ handleUpdateRow(long currentTimeMs)383 protected void handleUpdateRow(long currentTimeMs) { 384 for (int i = 0; i < size(); i++) { 385 Object item = get(i); 386 if (item instanceof ScheduleRow) { 387 ScheduleRow row = (ScheduleRow) item; 388 if (row.getEndTimeMs() <= currentTimeMs) { 389 removeScheduleRow(row); 390 } 391 } 392 } 393 } 394 395 /** 396 * Returns the next update time. Return {@link Long#MAX_VALUE} if no timer is necessary. 397 */ getNextTimerMs(long currentTimeMs)398 protected long getNextTimerMs(long currentTimeMs) { 399 long earliest = Long.MAX_VALUE; 400 for (int i = 0; i < size(); i++) { 401 Object item = get(i); 402 if (item instanceof ScheduleRow) { 403 // If the schedule was finished earlier than the end time, it should be removed 404 // when it reaches the end time in this class. 405 ScheduleRow row = (ScheduleRow) item; 406 if (earliest > row.getEndTimeMs()) { 407 earliest = row.getEndTimeMs(); 408 } 409 } 410 } 411 return earliest; 412 } 413 414 /** 415 * Send update message at the time returned by {@link #getNextTimerMs}. 416 */ sendNextUpdateMessage(long currentTimeMs)417 protected final void sendNextUpdateMessage(long currentTimeMs) { 418 mHandler.removeMessages(MSG_UPDATE_ROW); 419 long nextTime = getNextTimerMs(currentTimeMs); 420 if (nextTime != Long.MAX_VALUE) { 421 mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROW, 422 nextTime - System.currentTimeMillis()); 423 } 424 } 425 } 426