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 27 import com.android.tv.ApplicationSingletons; 28 import com.android.tv.R; 29 import com.android.tv.TvApplication; 30 import com.android.tv.common.SoftPreconditions; 31 import com.android.tv.data.Program; 32 import com.android.tv.dvr.DvrDataManager; 33 import com.android.tv.dvr.DvrManager; 34 import com.android.tv.dvr.ScheduledRecording; 35 import com.android.tv.dvr.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 /** 46 * An adapter for series schedule row. 47 */ 48 @TargetApi(Build.VERSION_CODES.N) 49 public class SeriesScheduleRowAdapter extends ScheduleRowAdapter { 50 private static final String TAG = "SeriesRowAdapter"; 51 private static final boolean DEBUG = false; 52 53 private final SeriesRecording mSeriesRecording; 54 private final String mInputId; 55 private final DvrManager mDvrManager; 56 private final DvrDataManager mDataManager; 57 private final Map<Long, Program> mPrograms = new ArrayMap<>(); 58 private SeriesRecordingHeaderRow mHeaderRow; 59 SeriesScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector, SeriesRecording seriesRecording)60 public SeriesScheduleRowAdapter(Context context, 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 ApplicationSingletons singletons = TvApplication.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 /** 87 * Sets the programs to show. 88 */ setPrograms(List<Program> programs)89 public void setPrograms(List<Program> programs) { 90 if (programs == null) { 91 programs = Collections.emptyList(); 92 } 93 clear(); 94 mPrograms.clear(); 95 List<Program> sortedPrograms = new ArrayList<>(programs); 96 Collections.sort(sortedPrograms); 97 List<EpisodicProgramRow> rows = new ArrayList<>(); 98 mHeaderRow = new SeriesRecordingHeaderRow(mSeriesRecording.getTitle(), 99 null, sortedPrograms.size(), mSeriesRecording); 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 ? null : getContext().getResources().getQuantityString( 126 R.plurals.dvr_series_schedules_header_description, conflicts, conflicts); 127 } 128 129 @Override getId(int position)130 public long getId(int position) { 131 Object obj = get(position); 132 if (obj instanceof EpisodicProgramRow) { 133 return ((EpisodicProgramRow) obj).getProgram().getId(); 134 } 135 if (obj instanceof SeriesRecordingHeaderRow) { 136 return 0; 137 } 138 return super.getId(position); 139 } 140 141 @Override onScheduledRecordingAdded(ScheduledRecording schedule)142 public void onScheduledRecordingAdded(ScheduledRecording schedule) { 143 if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule); 144 int index = findRowIndexByProgramId(schedule.getProgramId()); 145 if (index != -1) { 146 EpisodicProgramRow row = (EpisodicProgramRow) get(index); 147 if (!row.isStartRecordingRequested()) { 148 row.setSchedule(schedule); 149 notifyArrayItemRangeChanged(index, 1); 150 } 151 } 152 } 153 154 @Override onScheduledRecordingRemoved(ScheduledRecording schedule)155 public void onScheduledRecordingRemoved(ScheduledRecording schedule) { 156 if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); 157 int index = findRowIndexByProgramId(schedule.getProgramId()); 158 if (index != -1) { 159 EpisodicProgramRow row = (EpisodicProgramRow) get(index); 160 row.setSchedule(null); 161 notifyArrayItemRangeChanged(index, 1); 162 } 163 } 164 165 @Override onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange)166 public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) { 167 if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); 168 int index = findRowIndexByProgramId(schedule.getProgramId()); 169 if (index != -1) { 170 EpisodicProgramRow row = (EpisodicProgramRow) get(index); 171 if (conflictChange && isStartOrStopRequested()) { 172 // Delay the conflict update until it gets the response of the start/stop request. 173 // The purpose is to avoid the intermediate conflict change. 174 addPendingUpdate(row); 175 return; 176 } 177 if (row.isStopRecordingRequested()) { 178 // Wait until the recording is finished 179 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED 180 || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED 181 || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { 182 row.setStopRecordingRequested(false); 183 if (!isStartOrStopRequested()) { 184 executePendingUpdate(); 185 } 186 row.setSchedule(null); 187 } 188 } else if (row.isStartRecordingRequested()) { 189 // When the start recording was requested, we give the highest priority. So it is 190 // guaranteed that the state will be changed from NOT_STARTED to the other state. 191 // Update the row with the next state not to show the intermediate state to avoid 192 // blinking. 193 if (schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) { 194 row.setStartRecordingRequested(false); 195 if (!isStartOrStopRequested()) { 196 executePendingUpdate(); 197 } 198 row.setSchedule(schedule); 199 } 200 } else if (willBeKept(schedule)) { 201 row.setSchedule(schedule); 202 } else { 203 row.setSchedule(null); 204 } 205 notifyArrayItemRangeChanged(index, 1); 206 } 207 } 208 onSeriesRecordingUpdated(SeriesRecording seriesRecording)209 public void onSeriesRecordingUpdated(SeriesRecording seriesRecording) { 210 if (seriesRecording.getId() == mSeriesRecording.getId()) { 211 mHeaderRow.setSeriesRecording(seriesRecording); 212 notifyArrayItemRangeChanged(0, 1); 213 } 214 } 215 findRowIndexByProgramId(long programId)216 private int findRowIndexByProgramId(long programId) { 217 for (int i = 0; i < size(); i++) { 218 Object item = get(i); 219 if (item instanceof EpisodicProgramRow) { 220 if (((EpisodicProgramRow) item).getProgram().getId() == programId) { 221 return i; 222 } 223 } 224 } 225 return -1; 226 } 227 228 @Override notifyArrayItemRangeChanged(int positionStart, int itemCount)229 public void notifyArrayItemRangeChanged(int positionStart, int itemCount) { 230 mHeaderRow.setDescription(getDescription()); 231 super.notifyArrayItemRangeChanged(0, 1); 232 super.notifyArrayItemRangeChanged(positionStart, itemCount); 233 } 234 235 @Override handleUpdateRow(long currentTimeMs)236 protected void handleUpdateRow(long currentTimeMs) { 237 for (Iterator<Program> iter = mPrograms.values().iterator(); iter.hasNext(); ) { 238 Program program = iter.next(); 239 if (program.getEndTimeUtcMillis() <= currentTimeMs) { 240 // Remove the old program. 241 removeItems(findRowIndexByProgramId(program.getId()), 1); 242 iter.remove(); 243 } else if (program.getStartTimeUtcMillis() < currentTimeMs) { 244 // Change the button "START RECORDING" 245 notifyItemRangeChanged(findRowIndexByProgramId(program.getId()), 1); 246 } 247 } 248 } 249 250 /** 251 * Should take the current time argument which is the time when the programs are checked in 252 * handler. 253 */ 254 @Override getNextTimerMs(long currentTimeMs)255 protected long getNextTimerMs(long currentTimeMs) { 256 long earliest = Long.MAX_VALUE; 257 for (Program program : mPrograms.values()) { 258 if (earliest > program.getStartTimeUtcMillis() 259 && program.getStartTimeUtcMillis() >= currentTimeMs) { 260 // Need the button from "CREATE SCHEDULE" to "START RECORDING" 261 earliest = program.getStartTimeUtcMillis(); 262 } else if (earliest > program.getEndTimeUtcMillis()) { 263 // Need to remove the row. 264 earliest = program.getEndTimeUtcMillis(); 265 } 266 } 267 return earliest; 268 } 269 } 270