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