Home | History | Annotate | Download | only in list
      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 
     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      */
     79     protected Context getContext() {
     80         return mContext;
     81     }
     82 
     83     /**
     84      * Starts schedule row adapter.
     85      */
     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 
    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      */
    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      */
    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 
    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 
    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 
    218     private DateHeaderRow getHeaderRow(int index) {
    219         return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow());
    220     }
    221 
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    351     protected void addPendingUpdate(ScheduleRow row) {
    352         mPendingUpdate.add(row);
    353     }
    354 
    355     /**
    356      * Executes the pending updates.
    357      */
    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      */
    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      */
    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      */
    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      */
    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