Home | History | Annotate | Download | only in list
      1 /*
      2  * Copyright (C) 2018 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.os.Build.VERSION_CODES;
     22 import android.support.annotation.Nullable;
     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.Log;
     27 import com.android.tv.R;
     28 import com.android.tv.TvSingletons;
     29 import com.android.tv.common.SoftPreconditions;
     30 import com.android.tv.common.util.Clock;
     31 import com.android.tv.dvr.DvrDataManager;
     32 import com.android.tv.dvr.data.RecordedProgram;
     33 import com.android.tv.dvr.data.ScheduledRecording;
     34 import com.android.tv.dvr.recorder.ScheduledProgramReaper;
     35 import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow;
     36 import com.android.tv.util.Utils;
     37 import java.util.ArrayList;
     38 import java.util.HashMap;
     39 import java.util.List;
     40 import java.util.Map;
     41 import java.util.concurrent.TimeUnit;
     42 
     43 /** An adapter for DVR history. */
     44 @TargetApi(VERSION_CODES.N)
     45 @SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
     46 class DvrHistoryRowAdapter extends ArrayObjectAdapter {
     47     private static final String TAG = "DvrHistoryRowAdapter";
     48     private static final boolean DEBUG = false;
     49 
     50     private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
     51     private static final int MAX_HISTORY_DAYS = ScheduledProgramReaper.DAYS;
     52 
     53     private final Context mContext;
     54     private final Clock mClock;
     55     private final DvrDataManager mDvrDataManager;
     56     private final List<String> mTitles = new ArrayList<>();
     57     private final Map<Long, ScheduledRecording> mRecordedProgramScheduleMap = new HashMap<>();
     58 
     59     public DvrHistoryRowAdapter(
     60             Context context, ClassPresenterSelector classPresenterSelector, Clock clock) {
     61         super(classPresenterSelector);
     62         mContext = context;
     63         mClock = clock;
     64         mDvrDataManager = TvSingletons.getSingletons(mContext).getDvrDataManager();
     65         mTitles.add(mContext.getString(R.string.dvr_date_today));
     66         mTitles.add(mContext.getString(R.string.dvr_date_yesterday));
     67     }
     68 
     69     /** Returns context. */
     70     protected Context getContext() {
     71         return mContext;
     72     }
     73 
     74     /** Starts row adapter. */
     75     public void start() {
     76         clear();
     77         List<ScheduledRecording> recordingList = mDvrDataManager.getFailedScheduledRecordings();
     78         List<RecordedProgram> recordedProgramList = mDvrDataManager.getRecordedPrograms();
     79 
     80         recordingList.addAll(
     81                 recordedProgramsToScheduledRecordings(recordedProgramList, MAX_HISTORY_DAYS));
     82         recordingList
     83                 .sort(ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed());
     84         long deadLine = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis());
     85         for (int i = 0; i < recordingList.size(); ) {
     86             ArrayList<ScheduledRecording> section = new ArrayList<>();
     87             while (i < recordingList.size() && recordingList.get(i).getStartTimeMs() >= deadLine) {
     88                 section.add(recordingList.get(i++));
     89             }
     90             if (!section.isEmpty()) {
     91                 SchedulesHeaderRow headerRow =
     92                         new DateHeaderRow(
     93                                 calculateHeaderDate(deadLine),
     94                                 mContext.getResources()
     95                                         .getQuantityString(
     96                                                 R.plurals.dvr_schedules_section_subtitle,
     97                                                 section.size(),
     98                                                 section.size()),
     99                                 section.size(),
    100                                 deadLine);
    101                 add(headerRow);
    102                 for (ScheduledRecording recording : section) {
    103                     add(new ScheduleRow(recording, headerRow));
    104                 }
    105             }
    106             deadLine -= ONE_DAY_MS;
    107         }
    108     }
    109 
    110     private String calculateHeaderDate(long timeMs) {
    111         int titleIndex =
    112                 (int)
    113                         ((Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis()) - timeMs)
    114                                 / ONE_DAY_MS);
    115         String headerDate;
    116         if (titleIndex < mTitles.size()) {
    117             headerDate = mTitles.get(titleIndex);
    118         } else {
    119             headerDate =
    120                     DateUtils.formatDateTime(
    121                             getContext(),
    122                             timeMs,
    123                             DateUtils.FORMAT_SHOW_WEEKDAY
    124                                     | DateUtils.FORMAT_SHOW_DATE
    125                                     | DateUtils.FORMAT_ABBREV_MONTH);
    126         }
    127         return headerDate;
    128     }
    129 
    130     private List<ScheduledRecording> recordedProgramsToScheduledRecordings(
    131             List<RecordedProgram> programs, int maxDays) {
    132         List<ScheduledRecording> result = new ArrayList<>();
    133         for (RecordedProgram recordedProgram : programs) {
    134             ScheduledRecording scheduledRecording =
    135                     recordedProgramsToScheduledRecordings(recordedProgram, maxDays);
    136             if (scheduledRecording != null) {
    137                 result.add(scheduledRecording);
    138             }
    139         }
    140         return result;
    141     }
    142 
    143     @Nullable
    144     private ScheduledRecording recordedProgramsToScheduledRecordings(
    145             RecordedProgram program, int maxDays) {
    146         long firstMillisecondToday = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis());
    147         if (maxDays
    148                 < Utils.computeDateDifference(
    149                         program.getStartTimeUtcMillis(),
    150                         firstMillisecondToday)) {
    151             return null;
    152         }
    153         ScheduledRecording scheduledRecording = ScheduledRecording.builder(program).build();
    154         mRecordedProgramScheduleMap.put(program.getId(), scheduledRecording);
    155         return scheduledRecording;
    156     }
    157 
    158     public void onScheduledRecordingAdded(ScheduledRecording schedule) {
    159         if (DEBUG) {
    160             Log.d(TAG, "onScheduledRecordingAdded: " + schedule);
    161         }
    162         if (findRowByScheduledRecording(schedule) == null
    163                 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
    164                         || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED
    165                         || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) {
    166             addScheduleRow(schedule);
    167         }
    168     }
    169 
    170     public void onScheduledRecordingAdded(RecordedProgram program) {
    171         if (DEBUG) {
    172             Log.d(TAG, "onScheduledRecordingAdded: " + program);
    173         }
    174         if (mRecordedProgramScheduleMap.get(program.getId()) != null) {
    175             return;
    176         }
    177         ScheduledRecording schedule =
    178                 recordedProgramsToScheduledRecordings(program, MAX_HISTORY_DAYS);
    179         if (schedule == null) {
    180             return;
    181         }
    182         addScheduleRow(schedule);
    183     }
    184 
    185     public void onScheduledRecordingRemoved(ScheduledRecording schedule) {
    186         if (DEBUG) {
    187             Log.d(TAG, "onScheduledRecordingRemoved: " + schedule);
    188         }
    189         ScheduleRow row = findRowByScheduledRecording(schedule);
    190         if (row != null) {
    191             removeScheduleRow(row);
    192             notifyArrayItemRangeChanged(indexOf(row), 1);
    193         }
    194     }
    195 
    196     public void onScheduledRecordingRemoved(RecordedProgram program) {
    197         if (DEBUG) {
    198             Log.d(TAG, "onScheduledRecordingRemoved: " + program);
    199         }
    200         ScheduledRecording scheduledRecording = mRecordedProgramScheduleMap.get(program.getId());
    201         if (scheduledRecording != null) {
    202             mRecordedProgramScheduleMap.remove(program.getId());
    203             ScheduleRow row = findRowByRecordedProgram(program);
    204             if (row != null) {
    205                 removeScheduleRow(row);
    206                 notifyArrayItemRangeChanged(indexOf(row), 1);
    207             }
    208         }
    209     }
    210 
    211     public void onScheduledRecordingUpdated(ScheduledRecording schedule) {
    212         if (DEBUG) {
    213             Log.d(TAG, "onScheduledRecordingUpdated: " + schedule);
    214         }
    215         ScheduleRow row = findRowByScheduledRecording(schedule);
    216         if (row != null) {
    217             row.setSchedule(schedule);
    218             if (schedule.getState() != ScheduledRecording.STATE_RECORDING_FAILED) {
    219                 // Only handle failed schedules. Finished schedules are handled as recorded programs
    220                 removeScheduleRow(row);
    221             }
    222             notifyArrayItemRangeChanged(indexOf(row), 1);
    223         }
    224     }
    225 
    226     public void onScheduledRecordingUpdated(RecordedProgram program) {
    227         if (DEBUG) {
    228             Log.d(TAG, "onScheduledRecordingUpdated: " + program);
    229         }
    230         ScheduleRow row = findRowByRecordedProgram(program);
    231         if (row != null) {
    232             removeScheduleRow(row);
    233             notifyArrayItemRangeChanged(indexOf(row), 1);
    234             ScheduledRecording schedule = mRecordedProgramScheduleMap.get(program.getId());
    235             if (schedule != null) {
    236                 mRecordedProgramScheduleMap.remove(program.getId());
    237             }
    238         }
    239         onScheduledRecordingAdded(program);
    240     }
    241 
    242     private void addScheduleRow(ScheduledRecording recording) {
    243         // This method must not be called from inherited class.
    244         SoftPreconditions.checkState(getClass().equals(DvrHistoryRowAdapter.class));
    245         if (recording != null) {
    246             int pre = -1;
    247             int index = 0;
    248             for (; index < size(); index++) {
    249                 if (get(index) instanceof ScheduleRow) {
    250                     ScheduleRow scheduleRow = (ScheduleRow) get(index);
    251                     if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed()
    252                             .compare(scheduleRow.getSchedule(), recording) > 0) {
    253                         break;
    254                     }
    255                     pre = index;
    256                 }
    257             }
    258             long deadLine = Utils.getFirstMillisecondOfDay(recording.getStartTimeMs());
    259             if (pre >= 0 && getHeaderRow(pre).getDeadLineMs() == deadLine) {
    260                 SchedulesHeaderRow headerRow = ((ScheduleRow) get(pre)).getHeaderRow();
    261                 headerRow.setItemCount(headerRow.getItemCount() + 1);
    262                 ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
    263                 add(++pre, addedRow);
    264                 updateHeaderDescription(headerRow);
    265             } else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) {
    266                 SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow();
    267                 headerRow.setItemCount(headerRow.getItemCount() + 1);
    268                 ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
    269                 add(index, addedRow);
    270                 updateHeaderDescription(headerRow);
    271             } else {
    272                 SchedulesHeaderRow headerRow =
    273                         new DateHeaderRow(
    274                                 calculateHeaderDate(deadLine),
    275                                 mContext.getResources()
    276                                         .getQuantityString(
    277                                                 R.plurals.dvr_schedules_section_subtitle, 1, 1),
    278                                 1,
    279                                 deadLine);
    280                 add(++pre, headerRow);
    281                 ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
    282                 add(pre, addedRow);
    283             }
    284         }
    285     }
    286 
    287     private DateHeaderRow getHeaderRow(int index) {
    288         return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow());
    289     }
    290 
    291     /** Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to. */
    292     private ScheduleRow findRowByScheduledRecording(ScheduledRecording recording) {
    293         if (recording == null) {
    294             return null;
    295         }
    296         for (int i = 0; i < size(); i++) {
    297             Object item = get(i);
    298             if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) {
    299                 if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) {
    300                     return (ScheduleRow) item;
    301                 }
    302             }
    303         }
    304         return null;
    305     }
    306 
    307     private ScheduleRow findRowByRecordedProgram(RecordedProgram program) {
    308         if (program == null) {
    309             return null;
    310         }
    311         for (int i = 0; i < size(); i++) {
    312             Object item = get(i);
    313             if (item instanceof ScheduleRow) {
    314                 ScheduleRow row = (ScheduleRow) item;
    315                 if (row.hasRecordedProgram()
    316                         && row.getSchedule().getRecordedProgramId() == program.getId()) {
    317                     return (ScheduleRow) item;
    318                 }
    319             }
    320         }
    321         return null;
    322     }
    323 
    324     private void removeScheduleRow(ScheduleRow scheduleRow) {
    325         // This method must not be called from inherited class.
    326         SoftPreconditions.checkState(getClass().equals(DvrHistoryRowAdapter.class));
    327         if (scheduleRow != null) {
    328             scheduleRow.setSchedule(null);
    329             SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow();
    330             remove(scheduleRow);
    331             // Changes the count information of header which the removed row belongs to.
    332             if (headerRow != null) {
    333                 int currentCount = headerRow.getItemCount();
    334                 headerRow.setItemCount(--currentCount);
    335                 if (headerRow.getItemCount() == 0) {
    336                     remove(headerRow);
    337                 } else {
    338                     replace(indexOf(headerRow), headerRow);
    339                     updateHeaderDescription(headerRow);
    340                 }
    341             }
    342         }
    343     }
    344 
    345     private void updateHeaderDescription(SchedulesHeaderRow headerRow) {
    346         headerRow.setDescription(
    347                 mContext.getResources()
    348                         .getQuantityString(
    349                                 R.plurals.dvr_schedules_section_subtitle,
    350                                 headerRow.getItemCount(),
    351                                 headerRow.getItemCount()));
    352     }
    353 }
    354