| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.dvr.provider; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.media.tv.TvContract; |
| import android.media.tv.TvContract.Programs; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.WorkerThread; |
| import com.android.tv.TvSingletons; |
| import com.android.tv.common.SoftPreconditions; |
| import com.android.tv.common.util.PermissionUtils; |
| import com.android.tv.data.Program; |
| import com.android.tv.dvr.DvrDataManager; |
| import com.android.tv.dvr.data.ScheduledRecording; |
| import com.android.tv.dvr.data.SeasonEpisodeNumber; |
| import com.android.tv.dvr.data.SeriesRecording; |
| import com.android.tv.util.AsyncDbTask.AsyncProgramQueryTask; |
| import com.android.tv.util.AsyncDbTask.CursorFilter; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** A wrapper of AsyncProgramQueryTask to load the episodic programs for the series recordings. */ |
| @TargetApi(Build.VERSION_CODES.N) |
| public abstract class EpisodicProgramLoadTask { |
| private static final String TAG = "EpisodicProgramLoadTask"; |
| |
| private static final int PROGRAM_ID_INDEX = Program.getColumnIndex(Programs._ID); |
| private static final int START_TIME_INDEX = |
| Program.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS); |
| private static final int RECORDING_PROHIBITED_INDEX = |
| Program.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED); |
| |
| private static final String PARAM_START_TIME = "start_time"; |
| private static final String PARAM_END_TIME = "end_time"; |
| |
| private static final String PROGRAM_PREDICATE = |
| Programs.COLUMN_START_TIME_UTC_MILLIS |
| + ">? AND " |
| + Programs.COLUMN_RECORDING_PROHIBITED |
| + "=0"; |
| private static final String PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM = |
| Programs.COLUMN_END_TIME_UTC_MILLIS |
| + ">? AND " |
| + Programs.COLUMN_RECORDING_PROHIBITED |
| + "=0"; |
| private static final String CHANNEL_ID_PREDICATE = Programs.COLUMN_CHANNEL_ID + "=?"; |
| private static final String PROGRAM_TITLE_PREDICATE = Programs.COLUMN_TITLE + "=?"; |
| |
| private final Context mContext; |
| private final DvrDataManager mDataManager; |
| private boolean mQueryAllChannels; |
| private boolean mLoadCurrentProgram; |
| private boolean mLoadScheduledEpisode; |
| private boolean mLoadDisallowedProgram; |
| // If true, match programs with OPTION_CHANNEL_ALL. |
| private boolean mIgnoreChannelOption; |
| private final ArrayList<SeriesRecording> mSeriesRecordings = new ArrayList<>(); |
| private AsyncProgramQueryTask mProgramQueryTask; |
| |
| /** Constructor used to load programs for one series recording with the given channel option. */ |
| public EpisodicProgramLoadTask(Context context, SeriesRecording seriesRecording) { |
| this(context, Collections.singletonList(seriesRecording)); |
| } |
| |
| /** |
| * Constructor used to load programs for multiple series recordings. The channel option is |
| * {@link SeriesRecording#OPTION_CHANNEL_ALL}. |
| */ |
| public EpisodicProgramLoadTask(Context context, Collection<SeriesRecording> seriesRecordings) { |
| mContext = context.getApplicationContext(); |
| mDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); |
| mSeriesRecordings.addAll(seriesRecordings); |
| } |
| |
| /** Returns the series recordings. */ |
| public List<SeriesRecording> getSeriesRecordings() { |
| return mSeriesRecordings; |
| } |
| |
| /** Returns the program query task. It is {@code null} until it is executed. */ |
| @Nullable |
| public AsyncProgramQueryTask getTask() { |
| return mProgramQueryTask; |
| } |
| |
| /** Enables loading current programs. The default value is {@code false}. */ |
| public EpisodicProgramLoadTask setLoadCurrentProgram(boolean loadCurrentProgram) { |
| SoftPreconditions.checkState( |
| mProgramQueryTask == null, TAG, "Can't change setting after execution."); |
| mLoadCurrentProgram = loadCurrentProgram; |
| return this; |
| } |
| |
| /** Enables already schedules episodes. The default value is {@code false}. */ |
| public EpisodicProgramLoadTask setLoadScheduledEpisode(boolean loadScheduledEpisode) { |
| SoftPreconditions.checkState( |
| mProgramQueryTask == null, TAG, "Can't change setting after execution."); |
| mLoadScheduledEpisode = loadScheduledEpisode; |
| return this; |
| } |
| |
| /** |
| * Enables loading disallowed programs whose schedules were removed manually by the user. The |
| * default value is {@code false}. |
| */ |
| public EpisodicProgramLoadTask setLoadDisallowedProgram(boolean loadDisallowedProgram) { |
| SoftPreconditions.checkState( |
| mProgramQueryTask == null, TAG, "Can't change setting after execution."); |
| mLoadDisallowedProgram = loadDisallowedProgram; |
| return this; |
| } |
| |
| /** |
| * Gives the option whether to ignore the channel option when matching programs. If {@code |
| * ignoreChannelOption} is {@code true}, the program will be matched with {@link |
| * SeriesRecording#OPTION_CHANNEL_ALL} option. |
| */ |
| public EpisodicProgramLoadTask setIgnoreChannelOption(boolean ignoreChannelOption) { |
| SoftPreconditions.checkState( |
| mProgramQueryTask == null, TAG, "Can't change setting after execution."); |
| mIgnoreChannelOption = ignoreChannelOption; |
| return this; |
| } |
| |
| /** |
| * Executes the task. |
| * |
| * @see com.android.tv.util.AsyncDbTask#executeOnDbThread |
| */ |
| public void execute() { |
| if (SoftPreconditions.checkState( |
| mProgramQueryTask == null, |
| TAG, |
| "Can't execute task: the task is already running.")) { |
| mQueryAllChannels = |
| mSeriesRecordings.size() > 1 |
| || mSeriesRecordings.get(0).getChannelOption() |
| == SeriesRecording.OPTION_CHANNEL_ALL |
| || mIgnoreChannelOption; |
| mProgramQueryTask = createTask(); |
| mProgramQueryTask.executeOnDbThread(); |
| } |
| } |
| |
| /** |
| * Cancels the task. |
| * |
| * @see android.os.AsyncTask#cancel |
| */ |
| public void cancel(boolean mayInterruptIfRunning) { |
| if (mProgramQueryTask != null) { |
| mProgramQueryTask.cancel(mayInterruptIfRunning); |
| } |
| } |
| |
| /** Runs on the UI thread after the program loading finishes successfully. */ |
| protected void onPostExecute(List<Program> programs) {} |
| |
| /** Runs on the UI thread after the program loading was canceled. */ |
| protected void onCancelled(List<Program> programs) {} |
| |
| private AsyncProgramQueryTask createTask() { |
| SqlParams sqlParams = createSqlParams(); |
| return new AsyncProgramQueryTask( |
| TvSingletons.getSingletons(mContext).getDbExecutor(), |
| mContext, |
| sqlParams.uri, |
| sqlParams.selection, |
| sqlParams.selectionArgs, |
| null, |
| sqlParams.filter) { |
| @Override |
| protected void onPostExecute(List<Program> programs) { |
| EpisodicProgramLoadTask.this.onPostExecute(programs); |
| } |
| |
| @Override |
| protected void onCancelled(List<Program> programs) { |
| EpisodicProgramLoadTask.this.onCancelled(programs); |
| } |
| }; |
| } |
| |
| private SqlParams createSqlParams() { |
| SqlParams sqlParams = new SqlParams(); |
| if (PermissionUtils.hasAccessAllEpg(mContext)) { |
| sqlParams.uri = Programs.CONTENT_URI; |
| // Base |
| StringBuilder selection = |
| new StringBuilder( |
| mLoadCurrentProgram |
| ? PROGRAM_PREDICATE_WITH_CURRENT_PROGRAM |
| : PROGRAM_PREDICATE); |
| List<String> args = new ArrayList<>(); |
| args.add(Long.toString(System.currentTimeMillis())); |
| // Channel option |
| if (!mQueryAllChannels) { |
| selection.append(" AND ").append(CHANNEL_ID_PREDICATE); |
| args.add(Long.toString(mSeriesRecordings.get(0).getChannelId())); |
| } |
| // Title |
| if (mSeriesRecordings.size() == 1) { |
| selection.append(" AND ").append(PROGRAM_TITLE_PREDICATE); |
| args.add(mSeriesRecordings.get(0).getTitle()); |
| } |
| sqlParams.selection = selection.toString(); |
| sqlParams.selectionArgs = args.toArray(new String[args.size()]); |
| sqlParams.filter = new SeriesRecordingCursorFilter(mSeriesRecordings); |
| } else { |
| // The query includes the current program. Will be filtered if needed. |
| if (mQueryAllChannels) { |
| sqlParams.uri = |
| Programs.CONTENT_URI |
| .buildUpon() |
| .appendQueryParameter( |
| PARAM_START_TIME, |
| String.valueOf(System.currentTimeMillis())) |
| .appendQueryParameter( |
| PARAM_END_TIME, String.valueOf(Long.MAX_VALUE)) |
| .build(); |
| } else { |
| sqlParams.uri = |
| TvContract.buildProgramsUriForChannel( |
| mSeriesRecordings.get(0).getChannelId(), |
| System.currentTimeMillis(), |
| Long.MAX_VALUE); |
| } |
| sqlParams.selection = null; |
| sqlParams.selectionArgs = null; |
| sqlParams.filter = new SeriesRecordingCursorFilterForNonSystem(mSeriesRecordings); |
| } |
| return sqlParams; |
| } |
| |
| /** |
| * Filter the programs which match the series recording. The episodes which the schedules are |
| * already created for are filtered out too. |
| */ |
| private class SeriesRecordingCursorFilter implements CursorFilter { |
| private final Set<Long> mDisallowedProgramIds = new HashSet<>(); |
| private final Set<SeasonEpisodeNumber> mSeasonEpisodeNumbers = new HashSet<>(); |
| |
| SeriesRecordingCursorFilter(List<SeriesRecording> seriesRecordings) { |
| if (!mLoadDisallowedProgram) { |
| mDisallowedProgramIds.addAll(mDataManager.getDisallowedProgramIds()); |
| } |
| if (!mLoadScheduledEpisode) { |
| Set<Long> seriesRecordingIds = new HashSet<>(); |
| for (SeriesRecording r : seriesRecordings) { |
| seriesRecordingIds.add(r.getId()); |
| } |
| for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) { |
| if (seriesRecordingIds.contains(r.getSeriesRecordingId()) |
| && r.getState() != ScheduledRecording.STATE_RECORDING_FAILED |
| && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { |
| mSeasonEpisodeNumbers.add(new SeasonEpisodeNumber(r)); |
| } |
| } |
| } |
| } |
| |
| @Override |
| @WorkerThread |
| public boolean apply(Cursor c) { |
| if (!mLoadDisallowedProgram |
| && mDisallowedProgramIds.contains(c.getLong(PROGRAM_ID_INDEX))) { |
| return false; |
| } |
| Program program = Program.fromCursor(c); |
| for (SeriesRecording seriesRecording : mSeriesRecordings) { |
| boolean programMatches; |
| if (mIgnoreChannelOption) { |
| programMatches = |
| seriesRecording.matchProgram( |
| program, SeriesRecording.OPTION_CHANNEL_ALL); |
| } else { |
| programMatches = seriesRecording.matchProgram(program); |
| } |
| if (programMatches) { |
| return mLoadScheduledEpisode |
| || !mSeasonEpisodeNumbers.contains( |
| new SeasonEpisodeNumber( |
| seriesRecording.getId(), |
| program.getSeasonNumber(), |
| program.getEpisodeNumber())); |
| } |
| } |
| return false; |
| } |
| } |
| |
| private class SeriesRecordingCursorFilterForNonSystem extends SeriesRecordingCursorFilter { |
| SeriesRecordingCursorFilterForNonSystem(List<SeriesRecording> seriesRecordings) { |
| super(seriesRecordings); |
| } |
| |
| @Override |
| public boolean apply(Cursor c) { |
| return (mLoadCurrentProgram || c.getLong(START_TIME_INDEX) > System.currentTimeMillis()) |
| && c.getInt(RECORDING_PROHIBITED_INDEX) != 0 |
| && super.apply(c); |
| } |
| } |
| |
| private static class SqlParams { |
| public Uri uri; |
| public String selection; |
| public String[] selectionArgs; |
| public CursorFilter filter; |
| } |
| } |