• 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.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. */
EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording)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      */
EpisodicProgramLoadTask(Context context, Collection<SeriesRecording> seriesRecordings)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. */
getSeriesRecordings()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
getTask()105     public AsyncProgramQueryTask getTask() {
106         return mProgramQueryTask;
107     }
108 
109     /** Enables loading current programs. The default value is {@code false}. */
setLoadCurrentProgram(boolean loadCurrentProgram)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}. */
setLoadScheduledEpisode(boolean loadScheduledEpisode)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      */
setLoadDisallowedProgram(boolean loadDisallowedProgram)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      */
setIgnoreChannelOption(boolean ignoreChannelOption)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      */
execute()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      */
cancel(boolean mayInterruptIfRunning)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. */
onPostExecute(List<Program> programs)180     protected void onPostExecute(List<Program> programs) {}
181 
182     /** Runs on the UI thread after the program loading was canceled. */
onCancelled(List<Program> programs)183     protected void onCancelled(List<Program> programs) {}
184 
createTask()185     private AsyncProgramQueryTask createTask() {
186         SqlParams sqlParams = createSqlParams();
187         return new AsyncProgramQueryTask(
188                 TvSingletons.getSingletons(mContext).getDbExecutor(),
189                 mContext,
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 apply(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 apply(Cursor c) {
322             return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis())
323                     && c.getInt(RECORDING_PROHIBITED_INDEX) != 0
324                     && super.apply(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