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