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.annotation.TargetApi; 20 import android.content.Context; 21 import android.media.tv.TvInputInfo; 22 import android.os.Build; 23 import android.support.v17.leanback.widget.ClassPresenterSelector; 24 import android.util.ArrayMap; 25 import android.util.Log; 26 import com.android.tv.R; 27 import com.android.tv.TvSingletons; 28 import com.android.tv.common.SoftPreconditions; 29 import com.android.tv.data.Program; 30 import com.android.tv.dvr.DvrDataManager; 31 import com.android.tv.dvr.DvrManager; 32 import com.android.tv.dvr.data.ScheduledRecording; 33 import com.android.tv.dvr.data.SeriesRecording; 34 import com.android.tv.dvr.ui.list.SchedulesHeaderRow.SeriesRecordingHeaderRow; 35 import com.android.tv.util.Utils; 36 import java.util.ArrayList; 37 import java.util.Collections; 38 import java.util.Iterator; 39 import java.util.List; 40 import java.util.Map; 41 42 /** An adapter for series schedule row. */ 43 @TargetApi(Build.VERSION_CODES.N) 44 class SeriesScheduleRowAdapter extends ScheduleRowAdapter { 45 private static final String TAG = "SeriesRowAdapter"; 46 private static final boolean DEBUG = false; 47 48 private final SeriesRecording mSeriesRecording; 49 private final String mInputId; 50 private final DvrManager mDvrManager; 51 private final DvrDataManager mDataManager; 52 private final Map<Long, Program> mPrograms = new ArrayMap<>(); 53 private SeriesRecordingHeaderRow mHeaderRow; 54 SeriesScheduleRowAdapter( Context context, ClassPresenterSelector classPresenterSelector, SeriesRecording seriesRecording)55 public SeriesScheduleRowAdapter( 56 Context context, 57 ClassPresenterSelector classPresenterSelector, 58 SeriesRecording seriesRecording) { 59 super(context, classPresenterSelector); 60 mSeriesRecording = seriesRecording; 61 TvInputInfo input = Utils.getTvInputInfoForInputId(context, mSeriesRecording.getInputId()); 62 if (SoftPreconditions.checkNotNull(input) != null) { 63 mInputId = input.getId(); 64 } else { 65 mInputId = null; 66 } 67 TvSingletons singletons = TvSingletons.getSingletons(context); 68 mDvrManager = singletons.getDvrManager(); 69 mDataManager = singletons.getDvrDataManager(); 70 setHasStableIds(true); 71 } 72 73 @Override start()74 public void start() { 75 setPrograms(Collections.emptyList()); 76 } 77 78 @Override stop()79 public void stop() { 80 super.stop(); 81 } 82 83 /** Sets the programs to show. */ setPrograms(List<Program> programs)84 public void setPrograms(List<Program> programs) { 85 if (programs == null) { 86 programs = Collections.emptyList(); 87 } 88 clear(); 89 mPrograms.clear(); 90 List<Program> sortedPrograms = new ArrayList<>(programs); 91 Collections.sort(sortedPrograms); 92 List<EpisodicProgramRow> rows = new ArrayList<>(); 93 mHeaderRow = 94 new SeriesRecordingHeaderRow( 95 mSeriesRecording.getTitle(), 96 null, 97 sortedPrograms.size(), 98 mSeriesRecording, 99 programs); 100 for (Program program : sortedPrograms) { 101 ScheduledRecording schedule = 102 mDataManager.getScheduledRecordingForProgramId(program.getId()); 103 if (schedule != null && !willBeKept(schedule)) { 104 schedule = null; 105 } 106 rows.add(new EpisodicProgramRow(mInputId, program, schedule, mHeaderRow)); 107 mPrograms.put(program.getId(), program); 108 } 109 mHeaderRow.setDescription(getDescription()); 110 add(mHeaderRow); 111 for (EpisodicProgramRow row : rows) { 112 add(row); 113 } 114 sendNextUpdateMessage(System.currentTimeMillis()); 115 } 116 getDescription()117 private String getDescription() { 118 int conflicts = 0; 119 for (long programId : mPrograms.keySet()) { 120 if (mDvrManager.isConflicting( 121 mDataManager.getScheduledRecordingForProgramId(programId))) { 122 ++conflicts; 123 } 124 } 125 return conflicts == 0 126 ? null 127 : getContext() 128 .getResources() 129 .getQuantityString( 130 R.plurals.dvr_series_schedules_header_description, 131 conflicts, 132 conflicts); 133 } 134 135 @Override getId(int position)136 public long getId(int position) { 137 Object obj = get(position); 138 if (obj instanceof EpisodicProgramRow) { 139 return ((EpisodicProgramRow) obj).getProgram().getId(); 140 } 141 if (obj instanceof SeriesRecordingHeaderRow) { 142 return 0; 143 } 144 return super.getId(position); 145 } 146 147 @Override onScheduledRecordingAdded(ScheduledRecording schedule)148 public void onScheduledRecordingAdded(ScheduledRecording schedule) { 149 if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule); 150 int index = findRowIndexByProgramId(schedule.getProgramId()); 151 if (index != -1) { 152 EpisodicProgramRow row = (EpisodicProgramRow) get(index); 153 if (!row.isStartRecordingRequested()) { 154 setScheduleToRow(row, schedule); 155 notifyArrayItemRangeChanged(index, 1); 156 } 157 } 158 } 159 160 @Override onScheduledRecordingRemoved(ScheduledRecording schedule)161 public void onScheduledRecordingRemoved(ScheduledRecording schedule) { 162 if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); 163 int index = findRowIndexByProgramId(schedule.getProgramId()); 164 if (index != -1) { 165 EpisodicProgramRow row = (EpisodicProgramRow) get(index); 166 row.setSchedule(null); 167 notifyArrayItemRangeChanged(index, 1); 168 } 169 } 170 171 @Override onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange)172 public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) { 173 if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); 174 int index = findRowIndexByProgramId(schedule.getProgramId()); 175 if (index != -1) { 176 EpisodicProgramRow row = (EpisodicProgramRow) get(index); 177 if (conflictChange && isStartOrStopRequested()) { 178 // Delay the conflict update until it gets the response of the start/stop request. 179 // The purpose is to avoid the intermediate conflict change. 180 addPendingUpdate(row); 181 return; 182 } 183 if (row.isStopRecordingRequested()) { 184 // Wait until the recording is finished 185 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED 186 || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED 187 || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { 188 row.setStopRecordingRequested(false); 189 if (!isStartOrStopRequested()) { 190 executePendingUpdate(); 191 } 192 row.setSchedule(null); 193 } 194 } else if (row.isStartRecordingRequested()) { 195 // When the start recording was requested, we give the highest priority. So it is 196 // guaranteed that the state will be changed from NOT_STARTED to the other state. 197 // Update the row with the next state not to show the intermediate state to avoid 198 // blinking. 199 if (schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) { 200 row.setStartRecordingRequested(false); 201 if (!isStartOrStopRequested()) { 202 executePendingUpdate(); 203 } 204 setScheduleToRow(row, schedule); 205 } 206 } else { 207 setScheduleToRow(row, schedule); 208 } 209 notifyArrayItemRangeChanged(index, 1); 210 } 211 } 212 onSeriesRecordingUpdated(SeriesRecording seriesRecording)213 public void onSeriesRecordingUpdated(SeriesRecording seriesRecording) { 214 if (seriesRecording.getId() == mSeriesRecording.getId()) { 215 mHeaderRow.setSeriesRecording(seriesRecording); 216 notifyArrayItemRangeChanged(0, 1); 217 } 218 } 219 setScheduleToRow(ScheduleRow row, ScheduledRecording schedule)220 private void setScheduleToRow(ScheduleRow row, ScheduledRecording schedule) { 221 if (schedule != null && willBeKept(schedule)) { 222 row.setSchedule(schedule); 223 } else { 224 row.setSchedule(null); 225 } 226 } 227 findRowIndexByProgramId(long programId)228 private int findRowIndexByProgramId(long programId) { 229 for (int i = 0; i < size(); i++) { 230 Object item = get(i); 231 if (item instanceof EpisodicProgramRow) { 232 if (((EpisodicProgramRow) item).getProgram().getId() == programId) { 233 return i; 234 } 235 } 236 } 237 return -1; 238 } 239 240 @Override notifyArrayItemRangeChanged(int positionStart, int itemCount)241 public void notifyArrayItemRangeChanged(int positionStart, int itemCount) { 242 mHeaderRow.setDescription(getDescription()); 243 super.notifyArrayItemRangeChanged(0, 1); 244 super.notifyArrayItemRangeChanged(positionStart, itemCount); 245 } 246 247 @Override handleUpdateRow(long currentTimeMs)248 protected void handleUpdateRow(long currentTimeMs) { 249 for (Iterator<Program> iter = mPrograms.values().iterator(); iter.hasNext(); ) { 250 Program program = iter.next(); 251 if (program.getEndTimeUtcMillis() <= currentTimeMs) { 252 // Remove the old program. 253 removeItems(findRowIndexByProgramId(program.getId()), 1); 254 iter.remove(); 255 } else if (program.getStartTimeUtcMillis() < currentTimeMs) { 256 // Change the button "START RECORDING" 257 notifyItemRangeChanged(findRowIndexByProgramId(program.getId()), 1); 258 } 259 } 260 } 261 262 /** 263 * Should take the current time argument which is the time when the programs are checked in 264 * handler. 265 */ 266 @Override getNextTimerMs(long currentTimeMs)267 protected long getNextTimerMs(long currentTimeMs) { 268 long earliest = Long.MAX_VALUE; 269 for (Program program : mPrograms.values()) { 270 if (earliest > program.getStartTimeUtcMillis() 271 && program.getStartTimeUtcMillis() >= currentTimeMs) { 272 // Need the button from "CREATE SCHEDULE" to "START RECORDING" 273 earliest = program.getStartTimeUtcMillis(); 274 } else if (earliest > program.getEndTimeUtcMillis()) { 275 // Need to remove the row. 276 earliest = program.getEndTimeUtcMillis(); 277 } 278 } 279 return earliest; 280 } 281 } 282