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