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