• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording)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      */
EpisodicProgramLoadTask(Context context, Collection<SeriesRecording> seriesRecordings)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      */
getSeriesRecordings()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
getTask()111     public AsyncProgramQueryTask getTask() {
112         return mProgramQueryTask;
113     }
114 
115     /**
116      * Enables loading current programs. The default value is {@code false}.
117      */
setLoadCurrentProgram(boolean loadCurrentProgram)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      */
setLoadScheduledEpisode(boolean loadScheduledEpisode)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      */
setLoadDisallowedProgram(boolean loadDisallowedProgram)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      */
setIgnoreChannelOption(boolean ignoreChannelOption)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      */
execute()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      */
cancel(boolean mayInterruptIfRunning)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      */
onPostExecute(List<Program> programs)189     protected void onPostExecute(List<Program> programs) {
190     }
191 
192     /**
193      * Runs on the UI thread after the program loading was canceled.
194      */
onCancelled(List<Program> programs)195     protected void onCancelled(List<Program> programs) {
196     }
197 
createTask()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