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