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