/* * Copyright (C) 2015 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.util; import android.content.ContentResolver; 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.AsyncTask; import android.support.annotation.MainThread; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import android.util.Log; import android.util.Range; import com.android.tv.TvSingletons; import com.android.tv.common.BuildConfig; import com.android.tv.common.SoftPreconditions; import com.android.tv.data.ChannelImpl; import com.android.tv.data.ProgramImpl; import com.android.tv.data.api.Channel; import com.android.tv.data.api.Program; import com.android.tv.dvr.data.RecordedProgram; import com.google.common.base.Predicate; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Qualifier; /** * {@link AsyncTask} that defaults to executing on its own single threaded Executor Service. * * @param the type of the parameters sent to the task upon execution. * @param the type of the progress units published during the background computation. * @param the type of the result of the background computation. */ public abstract class AsyncDbTask extends AsyncTask { private static final String TAG = "AsyncDbTask"; private static final boolean DEBUG = false; /** Annotation for requesting the {@link Executor} for data base access. */ @Qualifier public @interface DbExecutor {} private final Executor mExecutor; boolean mCalledExecuteOnDbThread; protected AsyncDbTask(Executor mExecutor) { this.mExecutor = mExecutor; } /** * Returns the result of a {@link ContentResolver#query(Uri, String[], String, String[], * String)}. * *

{@link #doInBackground(Void...)} executes the query on call {@link #onQuery(Cursor)} which * is implemented by subclasses. * * @param the type of result returned by {@link #onQuery(Cursor)} */ public abstract static class AsyncQueryTask extends AsyncDbTask { private final WeakReference mContextReference; private final Uri mUri; private final String mSelection; private final String[] mSelectionArgs; private final String mOrderBy; private String[] mProjection; public AsyncQueryTask( @DbExecutor Executor executor, Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy) { super(executor); mContextReference = new WeakReference<>(context); mUri = uri; mProjection = projection; mSelection = selection; mSelectionArgs = selectionArgs; mOrderBy = orderBy; } @Override protected final Result doInBackground(Void... params) { if (!mCalledExecuteOnDbThread) { IllegalStateException e = new IllegalStateException( this + " should only be executed using executeOnDbThread, " + "but it was called on thread " + Thread.currentThread()); Log.w(TAG, e); if (BuildConfig.ENG) { throw e; } } if (isCancelled()) { // This is guaranteed to never call onPostExecute because the task is canceled. return null; } Context context = mContextReference.get(); if (context == null) { return null; } if (Utils.isProgramsUri(mUri) && TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) { mProjection = TvProviderUtils.addExtraColumnsToProjection( mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID); } else if (Utils.isRecordedProgramsUri(mUri)) { if (TvProviderUtils.checkSeriesIdColumn( context, TvContract.RecordedPrograms.CONTENT_URI)) { mProjection = TvProviderUtils.addExtraColumnsToProjection( mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID); } if (TvProviderUtils.checkStateColumn( context, TvContract.RecordedPrograms.CONTENT_URI)) { mProjection = TvProviderUtils.addExtraColumnsToProjection( mProjection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_STATE); } } if (DEBUG) { Log.v(TAG, "Starting query for " + this); } try (Cursor c = context.getContentResolver() .query(mUri, mProjection, mSelection, mSelectionArgs, mOrderBy)) { if (c != null && !isCancelled()) { Result result = onQuery(c); if (DEBUG) { Log.v(TAG, "Finished query for " + this); } return result; } else { if (c == null) { Log.e(TAG, "Unknown query error for " + this); } else { if (DEBUG) { Log.d(TAG, "Canceled query for " + this); } } return null; } } catch (Exception e) { SoftPreconditions.warn(TAG, null, e, "Error querying " + this); return null; } } /** * Return the result from the cursor. * *

Note This is executed on the DB thread by {@link #doInBackground(Void...)} */ @WorkerThread protected abstract Result onQuery(Cursor c); @Override public String toString() { return this.getClass().getName() + "(" + mUri + ")"; } } /** * Returns the result of a query as an {@link List} of {@code T}. * *

Subclasses must implement {@link #fromCursor(Cursor)}. * * @param the type of result returned in a list by {@link #onQuery(Cursor)} */ public abstract static class AsyncQueryListTask extends AsyncQueryTask> { private final CursorFilter mFilter; public AsyncQueryListTask( Executor executor, Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy) { this(executor, context, uri, projection, selection, selectionArgs, orderBy, null); } public AsyncQueryListTask( Executor executor, Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy, CursorFilter filter) { super(executor, context, uri, projection, selection, selectionArgs, orderBy); mFilter = filter; } @Override protected final List onQuery(Cursor c) { List result = new ArrayList<>(); while (c.moveToNext()) { if (isCancelled()) { // This is guaranteed to never call onPostExecute because the task is canceled. return null; } if (mFilter != null && !mFilter.apply(c)) { continue; } T t = fromCursor(c); result.add(t); } if (DEBUG) { Log.v(TAG, "Found " + result.size() + " for " + this); } return result; } /** * Return a single instance of {@code T} from the cursor. * *

NOTE Do not move the cursor or close it, that is handled by {@link * #onQuery(Cursor)}. * *

Note This is executed on the DB thread by {@link #onQuery(Cursor)} * * @param c The cursor with the values to create T from. */ @WorkerThread protected abstract T fromCursor(Cursor c); } /** * Returns the result of a query as a single instance of {@code T}. * *

Subclasses must implement {@link #fromCursor(Cursor)}. */ public abstract static class AsyncQueryItemTask extends AsyncQueryTask { public AsyncQueryItemTask( Executor executor, Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy) { super(executor, context, uri, projection, selection, selectionArgs, orderBy); } @Override protected final T onQuery(Cursor c) { if (c.moveToNext()) { if (isCancelled()) { // This is guaranteed to never call onPostExecute because the task is canceled. return null; } T result = fromCursor(c); if (c.moveToNext()) { Log.w(TAG, "More than one result for found for " + this); } return result; } else { if (DEBUG) { Log.v(TAG, "No result for found for " + this); } return null; } } /** * Return a single instance of {@code T} from the cursor. * *

NOTE Do not move the cursor or close it, that is handled by {@link * #onQuery(Cursor)}. * *

Note This is executed on the DB thread by {@link #onQuery(Cursor)} * * @param c The cursor with the values to create T from. */ @WorkerThread protected abstract T fromCursor(Cursor c); } /** Gets an {@link List} of {@link Channel}s from {@link TvContract.Channels#CONTENT_URI}. */ public abstract static class AsyncChannelQueryTask extends AsyncQueryListTask { public AsyncChannelQueryTask(Executor executor, Context context) { super( executor, context, TvContract.Channels.CONTENT_URI, ChannelImpl.PROJECTION, null, null, null); } @Override protected final Channel fromCursor(Cursor c) { return ChannelImpl.fromCursor(c); } } /** * Gets an {@link List} of {@link ProgramImpl}s from {@link TvContract.Programs#CONTENT_URI}. */ public abstract static class AsyncProgramQueryTask extends AsyncQueryListTask { public AsyncProgramQueryTask(Executor executor, Context context) { super( executor, context, Programs.CONTENT_URI, ProgramImpl.PROJECTION, null, null, null); } public AsyncProgramQueryTask( Executor executor, Context context, Uri uri, String selection, String[] selectionArgs, String sortOrder, CursorFilter filter) { super( executor, context, uri, ProgramImpl.PROJECTION, selection, selectionArgs, sortOrder, filter); } @Override protected final Program fromCursor(Cursor c) { return ProgramImpl.fromCursor(c); } } /** Gets an {@link List} of {@link TvContract.RecordedPrograms}s. */ public abstract static class AsyncRecordedProgramQueryTask extends AsyncQueryListTask { public AsyncRecordedProgramQueryTask(Executor executor, Context context, Uri uri) { super(executor, context, uri, RecordedProgram.PROJECTION, null, null, null); } @Override protected final RecordedProgram fromCursor(Cursor c) { return RecordedProgram.fromCursor(c); } } /** Execute the task on {@link TvSingletons#getDbExecutor()}. */ @SafeVarargs @MainThread public final void executeOnDbThread(Params... params) { mCalledExecuteOnDbThread = true; executeOnExecutor(mExecutor, params); } /** * Gets an {@link List} of {@link ProgramImpl}s for a given channel and period {@link * TvContract#buildProgramsUriForChannel(long, long, long)}. If the {@code period} is {@code * null}, then all the programs is queried. */ public static class LoadProgramsForChannelTask extends AsyncProgramQueryTask { protected final Range mPeriod; protected final long mChannelId; public LoadProgramsForChannelTask( Executor executor, Context context, long channelId, @Nullable Range period) { super( executor, context, period == null ? TvContract.buildProgramsUriForChannel(channelId) : TvContract.buildProgramsUriForChannel( channelId, period.getLower(), period.getUpper()), null, null, null, null); mPeriod = period; mChannelId = channelId; } public long getChannelId() { return mChannelId; } public final Range getPeriod() { return mPeriod; } } /** Gets a single {@link ProgramImpl} from {@link TvContract.Programs#CONTENT_URI}. */ public static class AsyncQueryProgramTask extends AsyncQueryItemTask { public AsyncQueryProgramTask(Executor executor, Context context, long programId) { super( executor, context, TvContract.buildProgramUri(programId), ProgramImpl.PROJECTION, null, null, null); } @Override protected Program fromCursor(Cursor c) { return ProgramImpl.fromCursor(c); } } /** An interface which filters the row. */ public interface CursorFilter extends Predicate {} }