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