/* * Copyright (C) 2010 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.calendar; import com.android.calendar.AsyncQueryServiceHelper.OperationInfo; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.os.Message; import android.util.Log; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; /** * A helper class that executes {@link ContentResolver} calls in a background * {@link android.app.Service}. This minimizes the chance of the call getting * lost because the caller ({@link android.app.Activity}) is killed. It is * designed for easy migration from {@link android.content.AsyncQueryHandler} * which calls the {@link ContentResolver} in a background thread. This supports * query/insert/update/delete and also batch mode i.e. * {@link ContentProviderOperation}. It also supports delay execution and cancel * which allows for time-limited undo. Note that there's one queue per * application which serializes all the calls. */ public class AsyncQueryService extends Handler { private static final String TAG = "AsyncQuery"; static final boolean localLOGV = false; // Used for generating unique tokens for calls to this service private static AtomicInteger mUniqueToken = new AtomicInteger(0); private Context mContext; private Handler mHandler = this; // can be overridden for testing /** * Data class which holds into info of the queued operation */ public static class Operation { static final int EVENT_ARG_QUERY = 1; static final int EVENT_ARG_INSERT = 2; static final int EVENT_ARG_UPDATE = 3; static final int EVENT_ARG_DELETE = 4; static final int EVENT_ARG_BATCH = 5; /** * unique identify for cancellation purpose */ public int token; /** * One of the EVENT_ARG_ constants in the class describing the operation */ public int op; /** * {@link SystemClock.elapsedRealtime()} based */ public long scheduledExecutionTime; protected static char opToChar(int op) { switch (op) { case Operation.EVENT_ARG_QUERY: return 'Q'; case Operation.EVENT_ARG_INSERT: return 'I'; case Operation.EVENT_ARG_UPDATE: return 'U'; case Operation.EVENT_ARG_DELETE: return 'D'; case Operation.EVENT_ARG_BATCH: return 'B'; default: return '?'; } } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Operation [op="); builder.append(op); builder.append(", token="); builder.append(token); builder.append(", scheduledExecutionTime="); builder.append(scheduledExecutionTime); builder.append("]"); return builder.toString(); } } public AsyncQueryService(Context context) { mContext = context; } /** * returns a practically unique token for db operations */ public final int getNextToken() { return mUniqueToken.getAndIncrement(); } /** * Gets the last delayed operation. It is typically used for canceling. * * @return Operation object which contains of the last cancelable operation */ public final Operation getLastCancelableOperation() { return AsyncQueryServiceHelper.getLastCancelableOperation(); } /** * Attempts to cancel operation that has not already started. Note that * there is no guarantee that the operation will be canceled. They still may * result in a call to on[Query/Insert/Update/Delete/Batch]Complete after * this call has completed. * * @param token The token representing the operation to be canceled. If * multiple operations have the same token they will all be * canceled. */ public final int cancelOperation(int token) { return AsyncQueryServiceHelper.cancelOperation(token); } /** * This method begins an asynchronous query. When the query is done * {@link #onQueryComplete} is called. * * @param token A token passed into {@link #onQueryComplete} to identify the * query. * @param cookie An object that gets passed into {@link #onQueryComplete} * @param uri The URI, using the content:// scheme, for the content to * retrieve. * @param projection A list of which columns to return. Passing null will * return all columns, which is discouraged to prevent reading * data from storage that isn't going to be used. * @param selection A filter declaring which rows to return, formatted as an * SQL WHERE clause (excluding the WHERE itself). Passing null * will return all rows for the given URI. * @param selectionArgs You may include ?s in selection, which will be * replaced by the values from selectionArgs, in the order that * they appear in the selection. The values will be bound as * Strings. * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause * (excluding the ORDER BY itself). Passing null will use the * default sort order, which may be unordered. */ public void startQuery(int token, Object cookie, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy) { OperationInfo info = new OperationInfo(); info.op = Operation.EVENT_ARG_QUERY; info.resolver = mContext.getContentResolver(); info.handler = mHandler; info.token = token; info.cookie = cookie; info.uri = uri; info.projection = projection; info.selection = selection; info.selectionArgs = selectionArgs; info.orderBy = orderBy; AsyncQueryServiceHelper.queueOperation(mContext, info); } /** * This method begins an asynchronous insert. When the insert operation is * done {@link #onInsertComplete} is called. * * @param token A token passed into {@link #onInsertComplete} to identify * the insert operation. * @param cookie An object that gets passed into {@link #onInsertComplete} * @param uri the Uri passed to the insert operation. * @param initialValues the ContentValues parameter passed to the insert * operation. * @param delayMillis delay in executing the operation. This operation will * execute before the delayed time when another operation is * added. Useful for implementing single level undo. */ public void startInsert(int token, Object cookie, Uri uri, ContentValues initialValues, long delayMillis) { OperationInfo info = new OperationInfo(); info.op = Operation.EVENT_ARG_INSERT; info.resolver = mContext.getContentResolver(); info.handler = mHandler; info.token = token; info.cookie = cookie; info.uri = uri; info.values = initialValues; info.delayMillis = delayMillis; AsyncQueryServiceHelper.queueOperation(mContext, info); } /** * This method begins an asynchronous update. When the update operation is * done {@link #onUpdateComplete} is called. * * @param token A token passed into {@link #onUpdateComplete} to identify * the update operation. * @param cookie An object that gets passed into {@link #onUpdateComplete} * @param uri the Uri passed to the update operation. * @param values the ContentValues parameter passed to the update operation. * @param selection A filter declaring which rows to update, formatted as an * SQL WHERE clause (excluding the WHERE itself). Passing null * will update all rows for the given URI. * @param selectionArgs You may include ?s in selection, which will be * replaced by the values from selectionArgs, in the order that * they appear in the selection. The values will be bound as * Strings. * @param delayMillis delay in executing the operation. This operation will * execute before the delayed time when another operation is * added. Useful for implementing single level undo. */ public void startUpdate(int token, Object cookie, Uri uri, ContentValues values, String selection, String[] selectionArgs, long delayMillis) { OperationInfo info = new OperationInfo(); info.op = Operation.EVENT_ARG_UPDATE; info.resolver = mContext.getContentResolver(); info.handler = mHandler; info.token = token; info.cookie = cookie; info.uri = uri; info.values = values; info.selection = selection; info.selectionArgs = selectionArgs; info.delayMillis = delayMillis; AsyncQueryServiceHelper.queueOperation(mContext, info); } /** * This method begins an asynchronous delete. When the delete operation is * done {@link #onDeleteComplete} is called. * * @param token A token passed into {@link #onDeleteComplete} to identify * the delete operation. * @param cookie An object that gets passed into {@link #onDeleteComplete} * @param uri the Uri passed to the delete operation. * @param selection A filter declaring which rows to delete, formatted as an * SQL WHERE clause (excluding the WHERE itself). Passing null * will delete all rows for the given URI. * @param selectionArgs You may include ?s in selection, which will be * replaced by the values from selectionArgs, in the order that * they appear in the selection. The values will be bound as * Strings. * @param delayMillis delay in executing the operation. This operation will * execute before the delayed time when another operation is * added. Useful for implementing single level undo. */ public void startDelete(int token, Object cookie, Uri uri, String selection, String[] selectionArgs, long delayMillis) { OperationInfo info = new OperationInfo(); info.op = Operation.EVENT_ARG_DELETE; info.resolver = mContext.getContentResolver(); info.handler = mHandler; info.token = token; info.cookie = cookie; info.uri = uri; info.selection = selection; info.selectionArgs = selectionArgs; info.delayMillis = delayMillis; AsyncQueryServiceHelper.queueOperation(mContext, info); } /** * This method begins an asynchronous {@link ContentProviderOperation}. When * the operation is done {@link #onBatchComplete} is called. * * @param token A token passed into {@link #onDeleteComplete} to identify * the delete operation. * @param cookie An object that gets passed into {@link #onDeleteComplete} * @param authority the authority used for the * {@link ContentProviderOperation}. * @param cpo the {@link ContentProviderOperation} to be executed. * @param delayMillis delay in executing the operation. This operation will * execute before the delayed time when another operation is * added. Useful for implementing single level undo. */ public void startBatch(int token, Object cookie, String authority, ArrayList<ContentProviderOperation> cpo, long delayMillis) { OperationInfo info = new OperationInfo(); info.op = Operation.EVENT_ARG_BATCH; info.resolver = mContext.getContentResolver(); info.handler = mHandler; info.token = token; info.cookie = cookie; info.authority = authority; info.cpo = cpo; info.delayMillis = delayMillis; AsyncQueryServiceHelper.queueOperation(mContext, info); } /** * Called when an asynchronous query is completed. * * @param token the token to identify the query, passed in from * {@link #startQuery}. * @param cookie the cookie object passed in from {@link #startQuery}. * @param cursor The cursor holding the results from the query. */ protected void onQueryComplete(int token, Object cookie, Cursor cursor) { if (localLOGV) { Log.d(TAG, "########## default onQueryComplete"); } } /** * Called when an asynchronous insert is completed. * * @param token the token to identify the query, passed in from * {@link #startInsert}. * @param cookie the cookie object that's passed in from * {@link #startInsert}. * @param uri the uri returned from the insert operation. */ protected void onInsertComplete(int token, Object cookie, Uri uri) { if (localLOGV) { Log.d(TAG, "########## default onInsertComplete"); } } /** * Called when an asynchronous update is completed. * * @param token the token to identify the query, passed in from * {@link #startUpdate}. * @param cookie the cookie object that's passed in from * {@link #startUpdate}. * @param result the result returned from the update operation */ protected void onUpdateComplete(int token, Object cookie, int result) { if (localLOGV) { Log.d(TAG, "########## default onUpdateComplete"); } } /** * Called when an asynchronous delete is completed. * * @param token the token to identify the query, passed in from * {@link #startDelete}. * @param cookie the cookie object that's passed in from * {@link #startDelete}. * @param result the result returned from the delete operation */ protected void onDeleteComplete(int token, Object cookie, int result) { if (localLOGV) { Log.d(TAG, "########## default onDeleteComplete"); } } /** * Called when an asynchronous {@link ContentProviderOperation} is * completed. * * @param token the token to identify the query, passed in from * {@link #startDelete}. * @param cookie the cookie object that's passed in from * {@link #startDelete}. * @param results the result returned from executing the * {@link ContentProviderOperation} */ protected void onBatchComplete(int token, Object cookie, ContentProviderResult[] results) { if (localLOGV) { Log.d(TAG, "########## default onBatchComplete"); } } @Override public void handleMessage(Message msg) { OperationInfo info = (OperationInfo) msg.obj; int token = msg.what; int op = msg.arg1; if (localLOGV) { Log.d(TAG, "AsyncQueryService.handleMessage: token=" + token + ", op=" + op + ", result=" + info.result); } // pass token back to caller on each callback. switch (op) { case Operation.EVENT_ARG_QUERY: onQueryComplete(token, info.cookie, (Cursor) info.result); break; case Operation.EVENT_ARG_INSERT: onInsertComplete(token, info.cookie, (Uri) info.result); break; case Operation.EVENT_ARG_UPDATE: onUpdateComplete(token, info.cookie, (Integer) info.result); break; case Operation.EVENT_ARG_DELETE: onDeleteComplete(token, info.cookie, (Integer) info.result); break; case Operation.EVENT_ARG_BATCH: onBatchComplete(token, info.cookie, (ContentProviderResult[]) info.result); break; } } // @VisibleForTesting protected void setTestHandler(Handler handler) { mHandler = handler; } }