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.content.Context;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.os.Message;
23 import android.support.v17.leanback.widget.ArrayObjectAdapter;
24 import android.support.v17.leanback.widget.ClassPresenterSelector;
25 import android.text.format.DateUtils;
26 import android.util.ArraySet;
27 import android.util.Log;
28 
29 import com.android.tv.R;
30 import com.android.tv.TvApplication;
31 import com.android.tv.common.SoftPreconditions;
32 import com.android.tv.dvr.DvrManager;
33 import com.android.tv.dvr.ScheduledRecording;
34 import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow;
35 import com.android.tv.util.Utils;
36 
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.List;
40 import java.util.Set;
41 import java.util.concurrent.TimeUnit;
42 
43 /**
44  * An adapter for {@link ScheduleRow}.
45  */
46 public class ScheduleRowAdapter extends ArrayObjectAdapter {
47     private static final String TAG = "ScheduleRowAdapter";
48     private static final boolean DEBUG = false;
49 
50     private final static long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
51 
52     private static final int MSG_UPDATE_ROW = 1;
53 
54     private Context mContext;
55     private final List<String> mTitles = new ArrayList<>();
56     private final Set<ScheduleRow> mPendingUpdate = new ArraySet<>();
57 
58     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
59         @Override
60         public void handleMessage(Message msg) {
61             if (msg.what == MSG_UPDATE_ROW) {
62                 long currentTimeMs = System.currentTimeMillis();
63                 handleUpdateRow(currentTimeMs);
64                 sendNextUpdateMessage(currentTimeMs);
65             }
66         }
67     };
68 
ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector)69     public ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector) {
70         super(classPresenterSelector);
71         mContext = context;
72         mTitles.add(mContext.getString(R.string.dvr_date_today));
73         mTitles.add(mContext.getString(R.string.dvr_date_tomorrow));
74     }
75 
76     /**
77      * Returns context.
78      */
getContext()79     protected Context getContext() {
80         return mContext;
81     }
82 
83     /**
84      * Starts schedule row adapter.
85      */
start()86     public void start() {
87         clear();
88         List<ScheduledRecording> recordingList = TvApplication.getSingletons(mContext)
89                 .getDvrDataManager().getNonStartedScheduledRecordings();
90         recordingList.addAll(TvApplication.getSingletons(mContext).getDvrDataManager()
91                 .getStartedRecordings());
92         Collections.sort(recordingList,
93                 ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR);
94         long deadLine = Utils.getLastMillisecondOfDay(System.currentTimeMillis());
95         for (int i = 0; i < recordingList.size();) {
96             ArrayList<ScheduledRecording> section = new ArrayList<>();
97             while (i < recordingList.size() && recordingList.get(i).getStartTimeMs() < deadLine) {
98                 section.add(recordingList.get(i++));
99             }
100             if (!section.isEmpty()) {
101                 SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine),
102                         mContext.getResources().getQuantityString(
103                         R.plurals.dvr_schedules_section_subtitle, section.size(), section.size()),
104                         section.size(), deadLine);
105                 add(headerRow);
106                 for(ScheduledRecording recording : section){
107                     add(new ScheduleRow(recording, headerRow));
108                 }
109             }
110             deadLine += ONE_DAY_MS;
111         }
112         sendNextUpdateMessage(System.currentTimeMillis());
113     }
114 
calculateHeaderDate(long deadLine)115     private String calculateHeaderDate(long deadLine) {
116         int titleIndex = (int) ((deadLine -
117                 Utils.getLastMillisecondOfDay(System.currentTimeMillis())) / ONE_DAY_MS);
118         String headerDate;
119         if (titleIndex < mTitles.size()) {
120             headerDate = mTitles.get(titleIndex);
121         } else {
122             headerDate = DateUtils.formatDateTime(getContext(), deadLine,
123                     DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE
124                             | DateUtils.FORMAT_ABBREV_MONTH);
125         }
126         return headerDate;
127     }
128 
129     /**
130      * Stops schedules row adapter.
131      */
stop()132     public void stop() {
133         mHandler.removeCallbacksAndMessages(null);
134         DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
135         for (int i = 0; i < size(); i++) {
136             if (get(i) instanceof ScheduleRow) {
137                 ScheduleRow row = (ScheduleRow) get(i);
138                 if (row.isScheduleCanceled()) {
139                     dvrManager.removeScheduledRecording(row.getSchedule());
140                 }
141             }
142         }
143     }
144 
145     /**
146      * Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to.
147      */
findRowByScheduledRecording(ScheduledRecording recording)148     public ScheduleRow findRowByScheduledRecording(ScheduledRecording recording) {
149         if (recording == null) {
150             return null;
151         }
152         for (int i = 0; i < size(); i++) {
153             Object item = get(i);
154             if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) {
155                 if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) {
156                     return (ScheduleRow) item;
157                 }
158             }
159         }
160         return null;
161     }
162 
findRowWithStartRequest(ScheduledRecording schedule)163     private ScheduleRow findRowWithStartRequest(ScheduledRecording schedule) {
164         for (int i = 0; i < size(); i++) {
165             Object item = get(i);
166             if (!(item instanceof ScheduleRow)) {
167                 continue;
168             }
169             ScheduleRow row = (ScheduleRow) item;
170             if (row.getSchedule() != null && row.isStartRecordingRequested()
171                     && row.matchSchedule(schedule)) {
172                 return row;
173             }
174         }
175         return null;
176     }
177 
addScheduleRow(ScheduledRecording recording)178     private void addScheduleRow(ScheduledRecording recording) {
179         // This method must not be called from inherited class.
180         SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class));
181         if (recording != null) {
182             int pre = -1;
183             int index = 0;
184             for (; index < size(); index++) {
185                 if (get(index) instanceof ScheduleRow) {
186                     ScheduleRow scheduleRow = (ScheduleRow) get(index);
187                     if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.compare(
188                             scheduleRow.getSchedule(), recording) > 0) {
189                         break;
190                     }
191                     pre = index;
192                 }
193             }
194             long deadLine = Utils.getLastMillisecondOfDay(recording.getStartTimeMs());
195             if (pre >= 0 && getHeaderRow(pre).getDeadLineMs() == deadLine) {
196                 SchedulesHeaderRow headerRow = ((ScheduleRow) get(pre)).getHeaderRow();
197                 headerRow.setItemCount(headerRow.getItemCount() + 1);
198                 ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
199                 add(++pre, addedRow);
200                 updateHeaderDescription(headerRow);
201             } else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) {
202                 SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow();
203                 headerRow.setItemCount(headerRow.getItemCount() + 1);
204                 ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
205                 add(index, addedRow);
206                 updateHeaderDescription(headerRow);
207             } else {
208                 SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine),
209                         mContext.getResources().getQuantityString(
210                         R.plurals.dvr_schedules_section_subtitle, 1, 1), 1, deadLine);
211                 add(++pre, headerRow);
212                 ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
213                 add(pre, addedRow);
214             }
215         }
216     }
217 
getHeaderRow(int index)218     private DateHeaderRow getHeaderRow(int index) {
219         return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow());
220     }
221 
removeScheduleRow(ScheduleRow scheduleRow)222     private void removeScheduleRow(ScheduleRow scheduleRow) {
223         // This method must not be called from inherited class.
224         SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class));
225         if (scheduleRow != null) {
226             scheduleRow.setSchedule(null);
227             SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow();
228             remove(scheduleRow);
229             // Changes the count information of header which the removed row belongs to.
230             if (headerRow != null) {
231                 int currentCount = headerRow.getItemCount();
232                 headerRow.setItemCount(--currentCount);
233                 if (headerRow.getItemCount() == 0) {
234                     remove(headerRow);
235                 } else {
236                     replace(indexOf(headerRow), headerRow);
237                     updateHeaderDescription(headerRow);
238                 }
239             }
240         }
241     }
242 
updateHeaderDescription(SchedulesHeaderRow headerRow)243     private void updateHeaderDescription(SchedulesHeaderRow headerRow) {
244         headerRow.setDescription(mContext.getResources().getQuantityString(
245                 R.plurals.dvr_schedules_section_subtitle,
246                 headerRow.getItemCount(), headerRow.getItemCount()));
247     }
248 
249     /**
250      * Called when a schedule recording is added to dvr date manager.
251      */
onScheduledRecordingAdded(ScheduledRecording schedule)252     public void onScheduledRecordingAdded(ScheduledRecording schedule) {
253         if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule);
254         ScheduleRow row = findRowWithStartRequest(schedule);
255         // If the start recording is requested, onScheduledRecordingAdded is called with NOT_STARTED
256         // state. And then onScheduleRecordingUpdated will be called with IN_PROGRESS.
257         // It happens in a short time and causes blinking. To avoid this intermediate state change,
258         // update the row in onScheduleRecordingUpdated when the state changes to IN_PROGRESS
259         // instead of in this method.
260         if (row == null) {
261             addScheduleRow(schedule);
262             sendNextUpdateMessage(System.currentTimeMillis());
263         }
264     }
265 
266     /**
267      * Called when a schedule recording is removed from dvr date manager.
268      */
onScheduledRecordingRemoved(ScheduledRecording schedule)269     public void onScheduledRecordingRemoved(ScheduledRecording schedule) {
270         if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule);
271         ScheduleRow row = findRowByScheduledRecording(schedule);
272         if (row != null) {
273             removeScheduleRow(row);
274             notifyArrayItemRangeChanged(indexOf(row), 1);
275             sendNextUpdateMessage(System.currentTimeMillis());
276         }
277     }
278 
279     /**
280      * Called when a schedule recording is updated in dvr date manager.
281      */
onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange)282     public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) {
283         if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule);
284         ScheduleRow row = findRowByScheduledRecording(schedule);
285         if (row != null) {
286             if (conflictChange && isStartOrStopRequested()) {
287                 // Delay the conflict update until it gets the response of the start/stop request.
288                 // The purpose is to avoid the intermediate conflict change.
289                 addPendingUpdate(row);
290                 return;
291             }
292             if (row.isStopRecordingRequested()) {
293                 // Wait until the recording is finished
294                 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
295                         || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED
296                         || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
297                     row.setStopRecordingRequested(false);
298                     if (!isStartOrStopRequested()) {
299                         executePendingUpdate();
300                     }
301                     row.setSchedule(schedule);
302                 }
303             } else {
304                 row.setSchedule(schedule);
305                 if (!willBeKept(schedule)) {
306                     removeScheduleRow(row);
307                 }
308             }
309             notifyArrayItemRangeChanged(indexOf(row), 1);
310             sendNextUpdateMessage(System.currentTimeMillis());
311         } else {
312             row = findRowWithStartRequest(schedule);
313             // When the start recording was requested, we give the highest priority. So it is
314             // guaranteed that the state will be changed from NOT_STARTED to the other state.
315             // Update the row with the next state not to show the intermediate state which causes
316             // blinking.
317             if (row != null
318                     && schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
319                 // This can be called multiple times, so do not call
320                 // ScheduleRow.setStartRecordingRequested(false) here.
321                 row.setStartRecordingRequested(false);
322                 if (!isStartOrStopRequested()) {
323                     executePendingUpdate();
324                 }
325                 row.setSchedule(schedule);
326                 notifyArrayItemRangeChanged(indexOf(row), 1);
327                 sendNextUpdateMessage(System.currentTimeMillis());
328             }
329         }
330     }
331 
332     /**
333      * Checks if there is a row which requested start/stop recording.
334      */
isStartOrStopRequested()335     protected boolean isStartOrStopRequested() {
336         for (int i = 0; i < size(); i++) {
337             Object item = get(i);
338             if (item instanceof ScheduleRow) {
339                 ScheduleRow row = (ScheduleRow) item;
340                 if (row.isStartRecordingRequested() || row.isStopRecordingRequested()) {
341                     return true;
342                 }
343             }
344         }
345         return false;
346     }
347 
348     /**
349      * Delays update of the row.
350      */
addPendingUpdate(ScheduleRow row)351     protected void addPendingUpdate(ScheduleRow row) {
352         mPendingUpdate.add(row);
353     }
354 
355     /**
356      * Executes the pending updates.
357      */
executePendingUpdate()358     protected void executePendingUpdate() {
359         for (ScheduleRow row : mPendingUpdate) {
360             int index = indexOf(row);
361             if (index != -1) {
362                 notifyArrayItemRangeChanged(index, 1);
363             }
364         }
365         mPendingUpdate.clear();
366     }
367 
368     /**
369      * To check whether the recording should be kept or not.
370      */
willBeKept(ScheduledRecording schedule)371     protected boolean willBeKept(ScheduledRecording schedule) {
372         // CANCELED state means that the schedule was removed temporarily, which should be shown
373         // in the list so that the user can reschedule it.
374         return schedule.getEndTimeMs() > System.currentTimeMillis()
375                 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS
376                 || schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
377                 || schedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED);
378     }
379 
380     /**
381      * Handle the message to update/remove rows.
382      */
handleUpdateRow(long currentTimeMs)383     protected void handleUpdateRow(long currentTimeMs) {
384         for (int i = 0; i < size(); i++) {
385             Object item = get(i);
386             if (item instanceof ScheduleRow) {
387                 ScheduleRow row = (ScheduleRow) item;
388                 if (row.getEndTimeMs() <= currentTimeMs) {
389                     removeScheduleRow(row);
390                 }
391             }
392         }
393     }
394 
395     /**
396      * Returns the next update time. Return {@link Long#MAX_VALUE} if no timer is necessary.
397      */
getNextTimerMs(long currentTimeMs)398     protected long getNextTimerMs(long currentTimeMs) {
399         long earliest = Long.MAX_VALUE;
400         for (int i = 0; i < size(); i++) {
401             Object item = get(i);
402             if (item instanceof ScheduleRow) {
403                 // If the schedule was finished earlier than the end time, it should be removed
404                 // when it reaches the end time in this class.
405                 ScheduleRow row = (ScheduleRow) item;
406                 if (earliest > row.getEndTimeMs()) {
407                     earliest = row.getEndTimeMs();
408                 }
409             }
410         }
411         return earliest;
412     }
413 
414     /**
415      * Send update message at the time returned by {@link #getNextTimerMs}.
416      */
sendNextUpdateMessage(long currentTimeMs)417     protected final void sendNextUpdateMessage(long currentTimeMs) {
418         mHandler.removeMessages(MSG_UPDATE_ROW);
419         long nextTime = getNextTimerMs(currentTimeMs);
420         if (nextTime != Long.MAX_VALUE) {
421             mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROW,
422                     nextTime - System.currentTimeMillis());
423         }
424     }
425 }
426