Home | History | Annotate | Download | only in dvr
      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;
     18 
     19 import android.annotation.TargetApi;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.media.tv.TvContract;
     23 import android.media.tv.TvContract.Programs;
     24 import android.net.Uri;
     25 import android.os.Build;
     26 import android.support.annotation.Nullable;
     27 import android.support.annotation.VisibleForTesting;
     28 import android.support.annotation.WorkerThread;
     29 import android.text.TextUtils;
     30 
     31 import com.android.tv.TvApplication;
     32 import com.android.tv.common.SoftPreconditions;
     33 import com.android.tv.data.Program;
     34 import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask;
     35 import com.android.tv.util.AsyncDbTask.CursorFilter;
     36 import com.android.tv.util.PermissionUtils;
     37 
     38 import java.util.ArrayList;
     39 import java.util.Collection;
     40 import java.util.Collections;
     41 import java.util.HashSet;
     42 import java.util.List;
     43 import java.util.Objects;
     44 import java.util.Set;
     45 
     46 /**
     47  * A wrapper of AsyncProgramQueryTask to load the episodic programs for the series recordings.
     48  */
     49 @TargetApi(Build.VERSION_CODES.N)
     50 abstract public class EpisodicProgramLoadTask {
     51     private static final String TAG = "EpisodicProgramLoadTask";
     52 
     53     private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID);
     54     private static final int START_TIME_INDEX =
     55             Program.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS);
     56     private static final int RECORDING_PROHIBITED_INDEX =
     57             Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED);
     58 
     59     private static final String PARAM_START_TIME = "start_time";
     60     private static final String PARAM_END_TIME = "end_time";
     61 
     62     private static final String PROGRAM_PREDICATE =
     63             Programs.COLUMN_START_TIME_UTC_MILLIS + ">? AND "
     64                     + Programs.COLUMN_RECORDING_PROHIBITED + "=0";
     65     private static final String PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM =
     66             Programs.COLUMN_END_TIME_UTC_MILLIS + ">? AND "
     67                     + Programs.COLUMN_RECORDING_PROHIBITED + "=0";
     68     private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?";
     69     private static final String PROGRAM_TITLE_PREDICATE = Programs.COLUMN_TITLE + "=?";
     70 
     71     private final Context mContext;
     72     private final DvrDataManager mDataManager;
     73     private boolean mQueryAllChannels;
     74     private boolean mLoadCurrentProgram;
     75     private boolean mLoadScheduledEpisode;
     76     private boolean mLoadDisallowedProgram;
     77     // If true, match programs with OPTION_CHANNEL_ALL.
     78     private boolean mIgnoreChannelOption;
     79     private final ArrayList<SeriesRecording> mSeriesRecordings = new ArrayList<>();
     80     private AsyncProgramQueryTask mProgramQueryTask;
     81 
     82     /**
     83      *
     84      * Constructor used to load programs for one series recording with the given channel option.
     85      */
     86     public EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording) {
     87         this(context, Collections.singletonList(seriesRecording));
     88     }
     89 
     90     /**
     91      * Constructor used to load programs for multiple series recordings. The channel option is
     92      * {@link SeriesRecording#OPTION_CHANNEL_ALL}.
     93      */
     94     public EpisodicProgramLoadTask(Context context, Collection<SeriesRecording> seriesRecordings) {
     95         mContext = context.getApplicationContext();
     96         mDataManager = TvApplication.getSingletons(context).getDvrDataManager();
     97         mSeriesRecordings.addAll(seriesRecordings);
     98     }
     99 
    100     /**
    101      * Returns the series recordings.
    102      */
    103     public List<SeriesRecording> getSeriesRecordings() {
    104         return mSeriesRecordings;
    105     }
    106 
    107     /**
    108      * Returns the program query task. It is {@code null} until it is executed.
    109      */
    110     @Nullable
    111     public AsyncProgramQueryTask getTask() {
    112         return mProgramQueryTask;
    113     }
    114 
    115     /**
    116      * Enables loading current programs. The default value is {@code false}.
    117      */
    118     public EpisodicProgramLoadTask setLoadCurrentProgram(boolean loadCurrentProgram) {
    119         SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
    120                 "Can't change setting after execution.");
    121         mLoadCurrentProgram = loadCurrentProgram;
    122         return this;
    123     }
    124 
    125     /**
    126      * Enables already schedules episodes. The default value is {@code false}.
    127      */
    128     public EpisodicProgramLoadTask setLoadScheduledEpisode(boolean loadScheduledEpisode) {
    129         SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
    130                 "Can't change setting after execution.");
    131         mLoadScheduledEpisode = loadScheduledEpisode;
    132         return this;
    133     }
    134 
    135     /**
    136      * Enables loading disallowed programs whose schedules were removed manually by the user.
    137      * The default value is {@code false}.
    138      */
    139     public EpisodicProgramLoadTask setLoadDisallowedProgram(boolean loadDisallowedProgram) {
    140         SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
    141                 "Can't change setting after execution.");
    142         mLoadDisallowedProgram = loadDisallowedProgram;
    143         return this;
    144     }
    145 
    146     /**
    147      * Gives the option whether to ignore the channel option when matching programs.
    148      * If {@code ignoreChannelOption} is {@code true}, the program will be matched with
    149      * {@link SeriesRecording#OPTION_CHANNEL_ALL} option.
    150      */
    151     public EpisodicProgramLoadTask setIgnoreChannelOption(boolean ignoreChannelOption) {
    152         SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
    153                 "Can't change setting after execution.");
    154         mIgnoreChannelOption = ignoreChannelOption;
    155         return this;
    156     }
    157 
    158     /**
    159      * Executes the task.
    160      *
    161      * @see com.android.tv.util.AsyncDbTask#executeOnDbThread
    162      */
    163     public void execute() {
    164         if (SoftPreconditions.checkState(mProgramQueryTask == null, TAG,
    165                 "Can't execute task: the task is already running.")) {
    166             mQueryAllChannels = mSeriesRecordings.size() > 1
    167                     || mSeriesRecordings.get(0).getChannelOption()
    168                             == SeriesRecording.OPTION_CHANNEL_ALL
    169                     || mIgnoreChannelOption;
    170             mProgramQueryTask = createTask();
    171             mProgramQueryTask.executeOnDbThread();
    172         }
    173     }
    174 
    175     /**
    176      * Cancels the task.
    177      *
    178      * @see android.os.AsyncTask#cancel
    179      */
    180     public void cancel(boolean mayInterruptIfRunning) {
    181         if (mProgramQueryTask != null) {
    182             mProgramQueryTask.cancel(mayInterruptIfRunning);
    183         }
    184     }
    185 
    186     /**
    187      * Runs on the UI thread after the program loading finishes successfully.
    188      */
    189     protected void onPostExecute(List<Program> programs) {
    190     }
    191 
    192     /**
    193      * Runs on the UI thread after the program loading was canceled.
    194      */
    195     protected void onCancelled(List<Program> programs) {
    196     }
    197 
    198     private AsyncProgramQueryTask createTask() {
    199         SqlParams sqlParams = createSqlParams();
    200         return new AsyncProgramQueryTask(mContext.getContentResolver(), sqlParams.uri,
    201                 sqlParams.selection, sqlParams.selectionArgs, null, sqlParams.filter) {
    202             @Override
    203             protected void onPostExecute(List<Program> programs) {
    204                 EpisodicProgramLoadTask.this.onPostExecute(programs);
    205             }
    206 
    207             @Override
    208             protected void onCancelled(List<Program> programs) {
    209                 EpisodicProgramLoadTask.this.onCancelled(programs);
    210             }
    211         };
    212     }
    213 
    214     private SqlParams createSqlParams() {
    215         SqlParams sqlParams = new SqlParams();
    216         if (PermissionUtils.hasAccessAllEpg(mContext)) {
    217             sqlParams.uri = Programs.CONTENT_URI;
    218             // Base
    219             StringBuilder selection = new StringBuilder(mLoadCurrentProgram
    220                     ? PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM : PROGRAM_PREDICATE);
    221             List<String> args = new ArrayList<>();
    222             args.add(Long.toString(System.currentTimeMillis()));
    223             // Channel option
    224             if (!mQueryAllChannels) {
    225                 selection.append(" AND ").append(CHANNEL_ID_PREDICATE);
    226                 args.add(Long.toString(mSeriesRecordings.get(0).getChannelId()));
    227             }
    228             // Title
    229             if (mSeriesRecordings.size() == 1) {
    230                 selection.append(" AND ").append(PROGRAM_TITLE_PREDICATE);
    231                 args.add(mSeriesRecordings.get(0).getTitle());
    232             }
    233             sqlParams.selection = selection.toString();
    234             sqlParams.selectionArgs = args.toArray(new String[args.size()]);
    235             sqlParams.filter = new SeriesRecordingCursorFilter(mSeriesRecordings);
    236         } else {
    237             // The query includes the current program. Will be filtered if needed.
    238             if (mQueryAllChannels) {
    239                 sqlParams.uri = Programs.CONTENT_URI.buildUpon()
    240                         .appendQueryParameter(PARAM_START_TIME,
    241                                 String.valueOf(System.currentTimeMillis()))
    242                         .appendQueryParameter(PARAM_END_TIME, String.valueOf(Long.MAX_VALUE))
    243                         .build();
    244             } else {
    245                 sqlParams.uri = TvContract.buildProgramsUriForChannel(
    246                         mSeriesRecordings.get(0).getChannelId(),
    247                         System.currentTimeMillis(), Long.MAX_VALUE);
    248             }
    249             sqlParams.selection = null;
    250             sqlParams.selectionArgs = null;
    251             sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(mSeriesRecordings);
    252         }
    253         return sqlParams;
    254     }
    255 
    256     @VisibleForTesting
    257     static boolean isEpisodeScheduled(Collection<ScheduledEpisode> scheduledEpisodes,
    258             ScheduledEpisode episode) {
    259         // The episode whose season number or episode number is null will always be scheduled.
    260         return scheduledEpisodes.contains(episode) && !TextUtils.isEmpty(episode.seasonNumber)
    261                 && !TextUtils.isEmpty(episode.episodeNumber);
    262     }
    263 
    264     /**
    265      * Filter the programs which match the series recording. The episodes which the schedules are
    266      * already created for are filtered out too.
    267      */
    268     private class SeriesRecordingCursorFilter implements CursorFilter {
    269         private final Set<Long> mDisallowedProgramIds = new HashSet<>();
    270         private final Set<ScheduledEpisode> mScheduledEpisodes = new HashSet<>();
    271 
    272         SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) {
    273             if (!mLoadDisallowedProgram) {
    274                 mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds());
    275             }
    276             if (!mLoadScheduledEpisode) {
    277                 Set<Long> seriesRecordingIds = new HashSet<>();
    278                 for (SeriesRecording r : seriesRecordings) {
    279                     seriesRecordingIds.add(r.getId());
    280                 }
    281                 for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) {
    282                     if (seriesRecordingIds.contains(r.getSeriesRecordingId())
    283                             && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED
    284                             && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) {
    285                         mScheduledEpisodes.add(new ScheduledEpisode(r));
    286                     }
    287                 }
    288             }
    289         }
    290 
    291         @Override
    292         @WorkerThread
    293         public boolean filter(Cursor c) {
    294             if (!mLoadDisallowedProgram
    295                     && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) {
    296                 return false;
    297             }
    298             Program program = Program.fromCursor(c);
    299             for (SeriesRecording seriesRecording : mSeriesRecordings) {
    300                 boolean programMatches;
    301                 if (mIgnoreChannelOption) {
    302                     programMatches = seriesRecording.matchProgram(program,
    303                             SeriesRecording.OPTION_CHANNEL_ALL);
    304                 } else {
    305                     programMatches = seriesRecording.matchProgram(program);
    306                 }
    307                 if (programMatches) {
    308                     return mLoadScheduledEpisode
    309                             || !isEpisodeScheduled(mScheduledEpisodes, new ScheduledEpisode(
    310                                     seriesRecording.getId(), program.getSeasonNumber(),
    311                                     program.getEpisodeNumber()));
    312                 }
    313             }
    314             return false;
    315         }
    316     }
    317 
    318     private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter {
    319         SeriesRecordingCursorFilterForNonSystem(List<SeriesRecording> seriesRecordings) {
    320             super(seriesRecordings);
    321         }
    322 
    323         @Override
    324         public boolean filter(Cursor c) {
    325             return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis())
    326                     && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 && super.filter(c);
    327         }
    328     }
    329 
    330     private static class SqlParams {
    331         public Uri uri;
    332         public String selection;
    333         public String[] selectionArgs;
    334         public CursorFilter filter;
    335     }
    336 
    337     /**
    338      * A plain java object which includes the season/episode number for the series recording.
    339      */
    340     public static class ScheduledEpisode {
    341         public final long seriesRecordingId;
    342         public final String seasonNumber;
    343         public final String episodeNumber;
    344 
    345         /**
    346          * Create a new Builder with the values set from an existing {@link ScheduledRecording}.
    347          */
    348         ScheduledEpisode(ScheduledRecording r) {
    349             this(r.getSeriesRecordingId(), r.getSeasonNumber(), r.getEpisodeNumber());
    350         }
    351 
    352         public ScheduledEpisode(long seriesRecordingId, String seasonNumber, String episodeNumber) {
    353             this.seriesRecordingId = seriesRecordingId;
    354             this.seasonNumber = seasonNumber;
    355             this.episodeNumber = episodeNumber;
    356         }
    357 
    358         @Override
    359         public boolean equals(Object o) {
    360             if (this == o) return true;
    361             if (!(o instanceof ScheduledEpisode)) return false;
    362             ScheduledEpisode that = (ScheduledEpisode) o;
    363             return seriesRecordingId == that.seriesRecordingId
    364                     && Objects.equals(seasonNumber, that.seasonNumber)
    365                     && Objects.equals(episodeNumber, that.episodeNumber);
    366         }
    367 
    368         @Override
    369         public int hashCode() {
    370             return Objects.hash(seriesRecordingId, seasonNumber, episodeNumber);
    371         }
    372 
    373         @Override
    374         public String toString() {
    375             return "ScheduledEpisode{" +
    376                     "seriesRecordingId=" + seriesRecordingId +
    377                     ", seasonNumber='" + seasonNumber +
    378                     ", episodeNumber=" + episodeNumber +
    379                     '}';
    380         }
    381     }
    382 }
    383