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