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