diff options
-rw-r--r-- | AndroidManifest.xml | 3 | ||||
-rw-r--r-- | src/com/android/calendar/AgendaWindowAdapter.java | 1 | ||||
-rw-r--r-- | src/com/android/calendar/AlertActivity.java | 18 | ||||
-rw-r--r-- | src/com/android/calendar/AsyncQueryService.java | 426 | ||||
-rw-r--r-- | src/com/android/calendar/AsyncQueryServiceHelper.java | 371 | ||||
-rw-r--r-- | src/com/android/calendar/EditEvent.java | 25 | ||||
-rw-r--r-- | src/com/android/calendar/SelectCalendarsAdapter.java | 13 | ||||
-rw-r--r-- | src/com/android/calendar/Utils.java | 3 | ||||
-rw-r--r-- | tests/src/com/android/calendar/AsyncQueryServiceTest.java | 628 |
9 files changed, 1460 insertions, 28 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 975276a4..cceff3b5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -130,6 +130,9 @@ <category android:name="android.intent.category.UNIT_TEST" /> </intent-filter> </activity> + + <service android:name=".AsyncQueryServiceHelper" /> + </application> </manifest> diff --git a/src/com/android/calendar/AgendaWindowAdapter.java b/src/com/android/calendar/AgendaWindowAdapter.java index 11c1d0de..5d43c8ff 100644 --- a/src/com/android/calendar/AgendaWindowAdapter.java +++ b/src/com/android/calendar/AgendaWindowAdapter.java @@ -522,6 +522,7 @@ public class AgendaWindowAdapter extends BaseAdapter { do { info = mAdapterInfos.poll(); if (info != null) { + // TODO the following causes ANR's. Do this in a thread. info.cursor.close(); deletedRows += info.size; recycleMe = info; diff --git a/src/com/android/calendar/AlertActivity.java b/src/com/android/calendar/AlertActivity.java index 1e35c50e..7732c331 100644 --- a/src/com/android/calendar/AlertActivity.java +++ b/src/com/android/calendar/AlertActivity.java @@ -22,16 +22,15 @@ import static android.provider.Calendar.EVENT_END_TIME; import android.app.Activity; import android.app.AlarmManager; import android.app.NotificationManager; -import android.content.AsyncQueryHandler; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.res.TypedArray; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; import android.provider.Calendar.CalendarAlerts; import android.provider.Calendar.CalendarAlertsColumns; import android.provider.Calendar.Events; @@ -105,7 +104,7 @@ public class AlertActivity extends Activity { values.put(PROJECTION[INDEX_STATE], CalendarAlerts.DISMISSED); String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.FIRED; mQueryHandler.startUpdate(0, null, CalendarAlerts.CONTENT_URI, values, - selection, null /* selectionArgs */); + selection, null /* selectionArgs */, Utils.UNDO_DELAY); } private void dismissAlarm(long id) { @@ -113,12 +112,12 @@ public class AlertActivity extends Activity { values.put(PROJECTION[INDEX_STATE], CalendarAlerts.DISMISSED); String selection = CalendarAlerts._ID + "=" + id; mQueryHandler.startUpdate(0, null, CalendarAlerts.CONTENT_URI, values, - selection, null /* selectionArgs */); + selection, null /* selectionArgs */, Utils.UNDO_DELAY); } - private class QueryHandler extends AsyncQueryHandler { - public QueryHandler(ContentResolver cr) { - super(cr); + private class QueryHandler extends AsyncQueryService { + public QueryHandler(Context context) { + super(context); } @Override @@ -211,7 +210,7 @@ public class AlertActivity extends Activity { getWindow().setAttributes(lp); mResolver = getContentResolver(); - mQueryHandler = new QueryHandler(mResolver); + mQueryHandler = new QueryHandler(this); mAdapter = new AlertAdapter(this, R.layout.alert_item); mListView = (ListView) findViewById(R.id.alert_container); @@ -286,7 +285,8 @@ public class AlertActivity extends Activity { if (mCursor.isLast()) { scheduleAlarmTime = alarmTime; } - mQueryHandler.startInsert(0, scheduleAlarmTime, CalendarAlerts.CONTENT_URI, values); + mQueryHandler.startInsert(0, scheduleAlarmTime, CalendarAlerts.CONTENT_URI, values, + Utils.UNDO_DELAY); } dismissFiredAlarms(); diff --git a/src/com/android/calendar/AsyncQueryService.java b/src/com/android/calendar/AsyncQueryService.java new file mode 100644 index 00000000..b663363d --- /dev/null +++ b/src/com/android/calendar/AsyncQueryService.java @@ -0,0 +1,426 @@ +/* + * 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; + +/** + * 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 = true; + + 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; + } + + /** + * 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 final 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 final 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 final 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 final 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; + } +} diff --git a/src/com/android/calendar/AsyncQueryServiceHelper.java b/src/com/android/calendar/AsyncQueryServiceHelper.java new file mode 100644 index 00000000..36ee581b --- /dev/null +++ b/src/com/android/calendar/AsyncQueryServiceHelper.java @@ -0,0 +1,371 @@ +/* + * 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.AsyncQueryService.Operation; + +import android.app.IntentService; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.OperationApplicationException; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.PriorityQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +public class AsyncQueryServiceHelper extends IntentService { + private static final String TAG = "AsyncQuery"; + + private static final PriorityQueue<OperationInfo> sWorkQueue = + new PriorityQueue<OperationInfo>(); + + protected Class<AsyncQueryService> mService = AsyncQueryService.class; + + protected static class OperationInfo implements Delayed{ + public int token; // Used for cancel + public int op; + public ContentResolver resolver; + public Uri uri; + public String authority; + public Handler handler; + public String[] projection; + public String selection; + public String[] selectionArgs; + public String orderBy; + public Object result; + public Object cookie; + public ContentValues values; + public ArrayList<ContentProviderOperation> cpo; + + /** + * delayMillis is relative time e.g. 10,000 milliseconds + */ + public long delayMillis; + + /** + * scheduleTimeMillis is the time scheduled for this to be processed. + * e.g. SystemClock.elapsedRealtime() + 10,000 milliseconds Based on + * {@link android.os.SystemClock#elapsedRealtime } + */ + private long mScheduledTimeMillis = 0; + + // @VisibleForTesting + void calculateScheduledTime() { + mScheduledTimeMillis = SystemClock.elapsedRealtime() + delayMillis; + } + + // @Override // Uncomment with Java6 + public long getDelay(TimeUnit unit) { + return unit.convert(mScheduledTimeMillis - SystemClock.elapsedRealtime(), + TimeUnit.MILLISECONDS); + } + + // @Override // Uncomment with Java6 + public int compareTo(Delayed another) { + OperationInfo anotherArgs = (OperationInfo) another; + if (this.mScheduledTimeMillis == anotherArgs.mScheduledTimeMillis) { + return 0; + } else if (this.mScheduledTimeMillis < anotherArgs.mScheduledTimeMillis) { + return -1; + } else { + return 1; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("OperationInfo [\n\t token= "); + builder.append(token); + builder.append(",\n\t op= "); + builder.append(Operation.opToChar(op)); + builder.append(",\n\t uri= "); + builder.append(uri); + builder.append(",\n\t authority= "); + builder.append(authority); + builder.append(",\n\t delayMillis= "); + builder.append(delayMillis); + builder.append(",\n\t mScheduledTimeMillis= "); + builder.append(mScheduledTimeMillis); + builder.append(",\n\t resolver= "); + builder.append(resolver); + builder.append(",\n\t handler= "); + builder.append(handler); + builder.append(",\n\t projection= "); + builder.append(Arrays.toString(projection)); + builder.append(",\n\t selection= "); + builder.append(selection); + builder.append(",\n\t selectionArgs= "); + builder.append(Arrays.toString(selectionArgs)); + builder.append(",\n\t orderBy= "); + builder.append(orderBy); + builder.append(",\n\t result= "); + builder.append(result); + builder.append(",\n\t cookie= "); + builder.append(cookie); + builder.append(",\n\t values= "); + builder.append(values); + builder.append(",\n\t cpo= "); + builder.append(cpo); + builder.append("\n]"); + return builder.toString(); + } + + /** + * Compares an user-visible operation to this private OperationInfo + * object + * + * @param o operation to be compared + * @return true if logically equivalent + */ + public boolean equivalent(Operation o) { + return o.token == this.token && o.op == this.op; + } + } + + /** + * Queues the operation for execution + * + * @param context + * @param args OperationInfo object describing the operation + */ + static public void queueOperation(Context context, OperationInfo args) { + // Set the schedule time for execution based on the desired delay. + args.calculateScheduledTime(); + + synchronized (sWorkQueue) { + sWorkQueue.add(args); + sWorkQueue.notify(); + } + + context.startService(new Intent(context, AsyncQueryServiceHelper.class)); + } + + /** + * Gets the last delayed operation. It is typically used for canceling. + * + * @return Operation object which contains of the last cancelable operation + */ + static public Operation getLastCancelableOperation() { + long lastScheduleTime = Long.MIN_VALUE; + Operation op = null; + + synchronized (sWorkQueue) { + // Unknown order even for a PriorityQueue + Iterator<OperationInfo> it = sWorkQueue.iterator(); + while (it.hasNext()) { + OperationInfo info = it.next(); + if (info.delayMillis > 0 && lastScheduleTime < info.mScheduledTimeMillis) { + if (op == null) { + op = new Operation(); + } + + op.token = info.token; + op.op = info.op; + op.scheduledExecutionTime = info.mScheduledTimeMillis; + + lastScheduleTime = info.mScheduledTimeMillis; + } + } + } + + if (AsyncQueryService.localLOGV) { + Log.d(TAG, "getLastCancelableOperation -> Operation:" + Operation.opToChar(op.op) + + " token:" + op.token); + } + return op; + } + + /** + * 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. + */ + static public int cancelOperation(int token) { + int canceled = 0; + synchronized (sWorkQueue) { + Iterator<OperationInfo> it = sWorkQueue.iterator(); + while (it.hasNext()) { + if (it.next().token == token) { + it.remove(); + ++canceled; + } + } + } + + if (AsyncQueryService.localLOGV) { + Log.d(TAG, "cancelOperation(" + token + ") -> " + canceled); + } + return canceled; + } + + public AsyncQueryServiceHelper(String name) { + super(name); + } + + public AsyncQueryServiceHelper() { + super("AsyncQueryServiceHelper"); + } + + @Override + protected void onHandleIntent(Intent intent) { + OperationInfo args; + + if (AsyncQueryService.localLOGV) { + Log.d(TAG, "onHandleIntent: queue size=" + sWorkQueue.size()); + } + synchronized (sWorkQueue) { + while (true) { + /* + * This method can be called with no work because of + * cancellations + */ + if (sWorkQueue.size() == 0) { + return; + } else if (sWorkQueue.size() == 1) { + OperationInfo first = sWorkQueue.peek(); + long waitTime = first.mScheduledTimeMillis - SystemClock.elapsedRealtime(); + if (waitTime > 0) { + try { + sWorkQueue.wait(waitTime); + } catch (InterruptedException e) { + } + } + } + + args = sWorkQueue.poll(); + if (args != null) { + // Got work to do. Break out of waiting loop + break; + } + } + } + + if (AsyncQueryService.localLOGV) { + Log.d(TAG, "onHandleIntent: " + args); + } + + ContentResolver resolver = args.resolver; + if (resolver != null) { + + switch (args.op) { + case Operation.EVENT_ARG_QUERY: + Cursor cursor; + try { + cursor = resolver.query(args.uri, args.projection, args.selection, + args.selectionArgs, args.orderBy); + /* + * Calling getCount() causes the cursor window to be + * filled, which will make the first access on the main + * thread a lot faster + */ + if (cursor != null) { + cursor.getCount(); + } + } catch (Exception e) { + Log.w(TAG, e.toString()); + cursor = null; + } + + args.result = cursor; + break; + + case Operation.EVENT_ARG_INSERT: + args.result = resolver.insert(args.uri, args.values); + break; + + case Operation.EVENT_ARG_UPDATE: + args.result = resolver.update(args.uri, args.values, args.selection, + args.selectionArgs); + break; + + case Operation.EVENT_ARG_DELETE: + args.result = resolver.delete(args.uri, args.selection, args.selectionArgs); + break; + + case Operation.EVENT_ARG_BATCH: + try { + args.result = resolver.applyBatch(args.authority, args.cpo); + } catch (RemoteException e) { + Log.e(TAG, e.toString()); + args.result = null; + } catch (OperationApplicationException e) { + Log.e(TAG, e.toString()); + args.result = null; + } + break; + } + + /* + * passing the original token value back to the caller on top of the + * event values in arg1. + */ + Message reply = args.handler.obtainMessage(args.token); + reply.obj = args; + reply.arg1 = args.op; + + if (AsyncQueryService.localLOGV) { + Log.d(TAG, "onHandleIntent: op=" + Operation.opToChar(args.op) + ", token=" + + reply.what); + } + + reply.sendToTarget(); + } + } + + @Override + public void onStart(Intent intent, int startId) { + if (AsyncQueryService.localLOGV) { + Log.d(TAG, "onStart startId=" + startId); + } + super.onStart(intent, startId); + } + + @Override + public void onCreate() { + if (AsyncQueryService.localLOGV) { + Log.d(TAG, "onCreate"); + } + super.onCreate(); + } + + @Override + public void onDestroy() { + if (AsyncQueryService.localLOGV) { + Log.d(TAG, "onDestroy"); + } + super.onDestroy(); + } +} diff --git a/src/com/android/calendar/EditEvent.java b/src/com/android/calendar/EditEvent.java index 3966e239..d1dd586c 100644 --- a/src/com/android/calendar/EditEvent.java +++ b/src/com/android/calendar/EditEvent.java @@ -1679,20 +1679,21 @@ public class EditEvent extends Activity implements View.OnClickListener, } } - try { - // TODO Move this to background thread - ContentProviderResult[] results = - getContentResolver().applyBatch(android.provider.Calendar.AUTHORITY, ops); - if (DEBUG) { - for (int i = 0; i < results.length; i++) { - Log.v(TAG, "results = " + results[i].toString()); + AsyncQueryService aqs; + if (DEBUG) { + aqs = new AsyncQueryService(this) { + @Override + protected void onBatchComplete(int token, Object cookie, + ContentProviderResult[] results) { + for (int i = 0; i < results.length; i++) { + Log.v(TAG, "results = " + results[i].toString()); + } } - } - } catch (RemoteException e) { - Log.w(TAG, "Ignoring unexpected remote exception", e); - } catch (OperationApplicationException e) { - Log.w(TAG, "Ignoring unexpected exception", e); + }; + } else { + aqs = new AsyncQueryService(this); } + aqs.startBatch(0, 0, android.provider.Calendar.AUTHORITY, ops, Utils.UNDO_DELAY); return true; } diff --git a/src/com/android/calendar/SelectCalendarsAdapter.java b/src/com/android/calendar/SelectCalendarsAdapter.java index c2c21ac4..9ae45257 100644 --- a/src/com/android/calendar/SelectCalendarsAdapter.java +++ b/src/com/android/calendar/SelectCalendarsAdapter.java @@ -59,7 +59,6 @@ public class SelectCalendarsAdapter extends CursorTreeAdapter implements View.On }; private final LayoutInflater mInflater; - private final ContentResolver mResolver; private final SelectCalendarsActivity mActivity; private final View mView; private final static Runnable mStopRefreshing = new Runnable() { @@ -123,10 +122,10 @@ public class SelectCalendarsAdapter extends CursorTreeAdapter implements View.On private static final int SYNCED_COLUMN = 6; private static final int PRIMARY_COLUMN = 7; - private class AsyncCalendarsUpdater extends AsyncQueryHandler { + private class AsyncCalendarsUpdater extends AsyncQueryService { - public AsyncCalendarsUpdater(ContentResolver cr) { - super(cr); + public AsyncCalendarsUpdater(Context context) { + super(context); } @Override @@ -219,10 +218,9 @@ public class SelectCalendarsAdapter extends CursorTreeAdapter implements View.On notSyncedNotVisible = context.getString(R.string.not_synced_not_visible); mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mResolver = context.getContentResolver(); mActivity = act; if (mCalendarsUpdater == null) { - mCalendarsUpdater = new AsyncCalendarsUpdater(mResolver); + mCalendarsUpdater = new AsyncCalendarsUpdater(context); } mNumAccounts = cursor.getCount(); @@ -271,7 +269,8 @@ public class SelectCalendarsAdapter extends CursorTreeAdapter implements View.On ContentValues values = new ContentValues(); values.put(Calendars.SELECTED, newSelected); values.put(Calendars.SYNC_EVENTS, newSynced); - mCalendarsUpdater.startUpdate(mUpdateToken, id, uri, values, null, null); + mCalendarsUpdater.startUpdate(mUpdateToken, id, uri, values, null, null, + Utils.UNDO_DELAY); } } diff --git a/src/com/android/calendar/Utils.java b/src/com/android/calendar/Utils.java index d37c7ffb..4d5b4ad5 100644 --- a/src/com/android/calendar/Utils.java +++ b/src/com/android/calendar/Utils.java @@ -37,6 +37,9 @@ import java.util.List; import java.util.Map; public class Utils { + // Set to 0 until we have UI to perform undo + public static final long UNDO_DELAY = 0; + private static final int CLEAR_ALPHA_MASK = 0x00FFFFFF; private static final int HIGH_ALPHA = 255 << 24; private static final int MED_ALPHA = 180 << 24; diff --git a/tests/src/com/android/calendar/AsyncQueryServiceTest.java b/tests/src/com/android/calendar/AsyncQueryServiceTest.java new file mode 100644 index 00000000..1757efd5 --- /dev/null +++ b/tests/src/com/android/calendar/AsyncQueryServiceTest.java @@ -0,0 +1,628 @@ +/* + * Copyright (C) 2007 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.AsyncQueryService.Operation; +import com.android.calendar.AsyncQueryServiceHelper.OperationInfo; + +import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.test.ServiceTestCase; +import android.test.mock.MockContentResolver; +import android.test.mock.MockContext; +import android.test.mock.MockCursor; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for {@link android.text.format.DateUtils#formatDateRange}. + */ +public class AsyncQueryServiceTest extends ServiceTestCase<AsyncQueryServiceHelper> { + private static final String TAG = "AsyncQueryServiceTest"; + + private static final String AUTHORITY_URI = "content://AsyncQueryAuthority/"; + + private static final String AUTHORITY = "AsyncQueryAuthority"; + + private static final int MIN_DELAY = 50; + + private static final int BASE_TEST_WAIT_TIME = MIN_DELAY * 5; + + private static int mId = 0; + + private static final String[] TEST_PROJECTION = new String[] { + "col1", "col2", "col3" + }; + + private static final String TEST_SELECTION = "selection"; + + private static final String[] TEST_SELECTION_ARGS = new String[] { + "arg1", "arg2", "arg3" + }; + + public AsyncQueryServiceTest() { + super(AsyncQueryServiceHelper.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @SmallTest + public void testQuery() throws Exception { + int index = 0; + final OperationInfo[] work = new OperationInfo[1]; + work[index] = new OperationInfo(); + work[index].op = Operation.EVENT_ARG_QUERY; + + work[index].token = ++mId; + work[index].cookie = ++mId; + work[index].uri = Uri.parse(AUTHORITY_URI + "blah"); + work[index].projection = TEST_PROJECTION; + work[index].selection = TEST_SELECTION; + work[index].selectionArgs = TEST_SELECTION_ARGS; + work[index].orderBy = "order"; + + work[index].delayMillis = 0; + work[index].result = new TestCursor(); + + TestAsyncQueryService aqs = new TestAsyncQueryService(buildTestContext(work), work); + aqs.startQuery(work[index].token, work[index].cookie, work[index].uri, + work[index].projection, work[index].selection, work[index].selectionArgs, + work[index].orderBy); + + Log.d(TAG, "testQuery Waiting >>>>>>>>>>>"); + assertEquals("Not all operations were executed.", work.length, aqs + .waitForCompletion(BASE_TEST_WAIT_TIME)); + Log.d(TAG, "testQuery Done <<<<<<<<<<<<<<"); + } + + @SmallTest + public void testInsert() throws Exception { + int index = 0; + final OperationInfo[] work = new OperationInfo[1]; + work[index] = new OperationInfo(); + work[index].op = Operation.EVENT_ARG_INSERT; + + work[index].token = ++mId; + work[index].cookie = ++mId; + work[index].uri = Uri.parse(AUTHORITY_URI + "blah"); + work[index].values = new ContentValues(); + work[index].values.put("key", ++mId); + + work[index].delayMillis = 0; + work[index].result = Uri.parse(AUTHORITY_URI + "Result=" + ++mId); + + TestAsyncQueryService aqs = new TestAsyncQueryService(buildTestContext(work), work); + aqs.startInsert(work[index].token, work[index].cookie, work[index].uri, work[index].values, + work[index].delayMillis); + + Log.d(TAG, "testInsert Waiting >>>>>>>>>>>"); + assertEquals("Not all operations were executed.", work.length, aqs + .waitForCompletion(BASE_TEST_WAIT_TIME)); + Log.d(TAG, "testInsert Done <<<<<<<<<<<<<<"); + } + + @SmallTest + public void testUpdate() throws Exception { + int index = 0; + final OperationInfo[] work = new OperationInfo[1]; + work[index] = new OperationInfo(); + work[index].op = Operation.EVENT_ARG_UPDATE; + + work[index].token = ++mId; + work[index].cookie = ++mId; + work[index].uri = Uri.parse(AUTHORITY_URI + ++mId); + work[index].values = new ContentValues(); + work[index].values.put("key", ++mId); + work[index].selection = TEST_SELECTION; + work[index].selectionArgs = TEST_SELECTION_ARGS; + + work[index].delayMillis = 0; + work[index].result = ++mId; + + TestAsyncQueryService aqs = new TestAsyncQueryService(buildTestContext(work), work); + aqs.startUpdate(work[index].token, work[index].cookie, work[index].uri, work[index].values, + work[index].selection, work[index].selectionArgs, work[index].delayMillis); + + Log.d(TAG, "testUpdate Waiting >>>>>>>>>>>"); + assertEquals("Not all operations were executed.", work.length, aqs + .waitForCompletion(BASE_TEST_WAIT_TIME)); + Log.d(TAG, "testUpdate Done <<<<<<<<<<<<<<"); + } + + @SmallTest + public void testDelete() throws Exception { + int index = 0; + final OperationInfo[] work = new OperationInfo[1]; + work[index] = new OperationInfo(); + work[index].op = Operation.EVENT_ARG_DELETE; + + work[index].token = ++mId; + work[index].cookie = ++mId; + work[index].uri = Uri.parse(AUTHORITY_URI + "blah"); + work[index].selection = TEST_SELECTION; + work[index].selectionArgs = TEST_SELECTION_ARGS; + + work[index].delayMillis = 0; + work[index].result = ++mId; + + TestAsyncQueryService aqs = new TestAsyncQueryService(buildTestContext(work), work); + aqs.startDelete(work[index].token, + work[index].cookie, + work[index].uri, + work[index].selection, + work[index].selectionArgs, + work[index].delayMillis); + + Log.d(TAG, "testDelete Waiting >>>>>>>>>>>"); + assertEquals("Not all operations were executed.", work.length, aqs + .waitForCompletion(BASE_TEST_WAIT_TIME)); + Log.d(TAG, "testDelete Done <<<<<<<<<<<<<<"); + } + + @SmallTest + public void testBatch() throws Exception { + int index = 0; + final OperationInfo[] work = new OperationInfo[1]; + work[index] = new OperationInfo(); + work[index].op = Operation.EVENT_ARG_BATCH; + + work[index].token = ++mId; + work[index].cookie = ++mId; + work[index].authority = AUTHORITY; + work[index].cpo = new ArrayList<ContentProviderOperation>(); + work[index].cpo.add(ContentProviderOperation.newInsert(Uri.parse(AUTHORITY_URI + ++mId)) + .build()); + + work[index].delayMillis = 0; + ContentProviderResult[] resultArray = new ContentProviderResult[1]; + resultArray[0] = new ContentProviderResult(++mId); + work[index].result = resultArray; + + TestAsyncQueryService aqs = new TestAsyncQueryService(buildTestContext(work), work); + aqs.startBatch(work[index].token, + work[index].cookie, + work[index].authority, + work[index].cpo, + work[index].delayMillis); + + Log.d(TAG, "testBatch Waiting >>>>>>>>>>>"); + assertEquals("Not all operations were executed.", work.length, aqs + .waitForCompletion(BASE_TEST_WAIT_TIME)); + Log.d(TAG, "testBatch Done <<<<<<<<<<<<<<"); + } + + @LargeTest + public void testDelay() throws Exception { + // Tests the ordering of the workqueue + int index = 0; + OperationInfo[] work = new OperationInfo[5]; + work[index++] = generateWork(MIN_DELAY * 2); + work[index++] = generateWork(0); + work[index++] = generateWork(MIN_DELAY * 1); + work[index++] = generateWork(0); + work[index++] = generateWork(MIN_DELAY * 3); + + OperationInfo[] sorted = generateSortedWork(work, work.length); + + TestAsyncQueryService aqs = new TestAsyncQueryService(buildTestContext(sorted), sorted); + startWork(aqs, work); + + Log.d(TAG, "testDelay Waiting >>>>>>>>>>>"); + assertEquals("Not all operations were executed.", work.length, aqs + .waitForCompletion(BASE_TEST_WAIT_TIME)); + Log.d(TAG, "testDelay Done <<<<<<<<<<<<<<"); + } + + @LargeTest + public void testCancel_simpleCancelLastTest() throws Exception { + int index = 0; + OperationInfo[] work = new OperationInfo[5]; + work[index++] = generateWork(MIN_DELAY * 2); + work[index++] = generateWork(0); + work[index++] = generateWork(MIN_DELAY); + work[index++] = generateWork(0); + work[index] = generateWork(MIN_DELAY * 3); + + // Not part of the expected as it will be canceled + OperationInfo toBeCancelled1 = work[index]; + OperationInfo[] expected = generateSortedWork(work, work.length - 1); + + TestAsyncQueryService aqs = new TestAsyncQueryService(buildTestContext(expected), expected); + startWork(aqs, work); + Operation lastOne = aqs.getLastCancelableOperation(); + // Log.d(TAG, "lastOne = " + lastOne.toString()); + // Log.d(TAG, "toBeCancelled1 = " + toBeCancelled1.toString()); + assertTrue("1) delay=3 is not last", toBeCancelled1.equivalent(lastOne)); + assertEquals("Can't cancel delay 3", 1, aqs.cancelOperation(lastOne.token)); + + Log.d(TAG, "testCancel_simpleCancelLastTest Waiting >>>>>>>>>>>"); + assertEquals("Not all operations were executed.", expected.length, aqs + .waitForCompletion(BASE_TEST_WAIT_TIME)); + Log.d(TAG, "testCancel_simpleCancelLastTest Done <<<<<<<<<<<<<<"); + } + + @LargeTest + public void testCancel_cancelSecondToLast() throws Exception { + int index = 0; + OperationInfo[] work = new OperationInfo[5]; + work[index++] = generateWork(MIN_DELAY * 2); + work[index++] = generateWork(0); + work[index++] = generateWork(MIN_DELAY); + work[index++] = generateWork(0); + work[index] = generateWork(MIN_DELAY * 3); + + // Not part of the expected as it will be canceled + OperationInfo toBeCancelled1 = work[index]; + OperationInfo[] expected = new OperationInfo[4]; + expected[0] = work[1]; // delay = 0 + expected[1] = work[3]; // delay = 0 + expected[2] = work[2]; // delay = MIN_DELAY + expected[3] = work[4]; // delay = MIN_DELAY * 3 + + TestAsyncQueryService aqs = new TestAsyncQueryService(buildTestContext(expected), expected); + startWork(aqs, work); + + Operation lastOne = aqs.getLastCancelableOperation(); // delay = 3 + assertTrue("2) delay=3 is not last", toBeCancelled1.equivalent(lastOne)); + assertEquals("Can't cancel delay 2", 1, aqs.cancelOperation(work[0].token)); + assertEquals("Delay 2 should be gone", 0, aqs.cancelOperation(work[0].token)); + + Log.d(TAG, "testCancel_cancelSecondToLast Waiting >>>>>>>>>>>"); + assertEquals("Not all operations were executed.", expected.length, aqs + .waitForCompletion(BASE_TEST_WAIT_TIME)); + Log.d(TAG, "testCancel_cancelSecondToLast Done <<<<<<<<<<<<<<"); + } + + @LargeTest + public void testCancel_multipleCancels() throws Exception { + int index = 0; + OperationInfo[] work = new OperationInfo[5]; + work[index++] = generateWork(MIN_DELAY * 2); + work[index++] = generateWork(0); + work[index++] = generateWork(MIN_DELAY); + work[index++] = generateWork(0); + work[index] = generateWork(MIN_DELAY * 3); + + // Not part of the expected as it will be canceled + OperationInfo[] expected = new OperationInfo[3]; + expected[0] = work[1]; // delay = 0 + expected[1] = work[3]; // delay = 0 + expected[2] = work[2]; // delay = MIN_DELAY + + TestAsyncQueryService aqs = new TestAsyncQueryService(buildTestContext(expected), expected); + startWork(aqs, work); + + Operation lastOne = aqs.getLastCancelableOperation(); // delay = 3 + assertTrue("3) delay=3 is not last", work[4].equivalent(lastOne)); + assertEquals("Can't cancel delay 2", 1, aqs.cancelOperation(work[0].token)); + assertEquals("Delay 2 should be gone", 0, aqs.cancelOperation(work[0].token)); + assertEquals("Can't cancel delay 3", 1, aqs.cancelOperation(work[4].token)); + assertEquals("Delay 3 should be gone", 0, aqs.cancelOperation(work[4].token)); + + Log.d(TAG, "testCancel_multipleCancels Waiting >>>>>>>>>>>"); + assertEquals("Not all operations were executed.", expected.length, aqs + .waitForCompletion(BASE_TEST_WAIT_TIME)); + Log.d(TAG, "testCancel_multipleCancels Done <<<<<<<<<<<<<<"); + } + + private OperationInfo generateWork(long delayMillis) { + OperationInfo work = new OperationInfo(); + work.op = Operation.EVENT_ARG_DELETE; + + work.token = ++mId; + work.cookie = 100 + work.token; + work.uri = Uri.parse(AUTHORITY_URI + "blah"); + work.selection = TEST_SELECTION; + work.selectionArgs = TEST_SELECTION_ARGS; + + work.delayMillis = delayMillis; + work.result = 1000 + work.token; + return work; + } + + private void startWork(TestAsyncQueryService aqs, OperationInfo[] work) { + for (OperationInfo w : work) { + if (w != null) { + aqs.startDelete(w.token, w.cookie, w.uri, w.selection, w.selectionArgs, + w.delayMillis); + } + } + } + + OperationInfo[] generateSortedWork(OperationInfo[] work, int length) { + OperationInfo[] sorted = new OperationInfo[length]; + System.arraycopy(work, 0, sorted, 0, length); + + // Set the scheduled time so they get sorted properly + for (OperationInfo w : sorted) { + if (w != null) { + w.calculateScheduledTime(); + } + } + + // Stable sort by scheduled time + Arrays.sort(sorted); + + Log.d(TAG, "Unsorted work: " + work.length); + for (OperationInfo w : work) { + if (w != null) { + Log.d(TAG, "Token#" + w.token + " delay=" + w.delayMillis); + } + } + Log.d(TAG, "Sorted work: " + sorted.length); + for (OperationInfo w : sorted) { + if (w != null) { + Log.d(TAG, "Token#" + w.token + " delay=" + w.delayMillis); + } + } + + return sorted; + } + + private Context buildTestContext(final OperationInfo[] work) { + MockContext context = new MockContext() { + MockContentResolver mResolver; + + @Override + public ContentResolver getContentResolver() { + if (mResolver == null) { + ContentProvider provider = new TestProvider(work); + mResolver = new MockContentResolver(); + mResolver.addProvider(AUTHORITY, provider); + } + return mResolver; + } + + @Override + public String getPackageName() { + return AsyncQueryServiceTest.class.getPackage().getName(); + } + + public ComponentName startService(Intent service) { + AsyncQueryServiceTest.this.startService(service); + return service.getComponent(); + } + }; + + return context; + } + + private final class TestCursor extends MockCursor { + int mUnique = ++mId; + + @Override + public int getCount() { + return mUnique; + } + } + + /** + * TestAsyncQueryService takes the expected results in the constructor. They + * are used to verify the data passed to the callbacks. + */ + class TestAsyncQueryService extends AsyncQueryService { + int mIndex = 0; + + private OperationInfo[] mWork; + + private Semaphore mCountingSemaphore; + + public TestAsyncQueryService(Context context, OperationInfo[] work) { + super(context); + mCountingSemaphore = new Semaphore(0); + + // run in a separate thread but call the same code + HandlerThread thread = new HandlerThread("TestAsyncQueryService"); + thread.start(); + super.setTestHandler(new Handler(thread.getLooper()) { + @Override + public void handleMessage(Message msg) { + TestAsyncQueryService.this.handleMessage(msg); + } + }); + + mWork = work; + } + + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + Log.d(TAG, "onQueryComplete tid=" + Thread.currentThread().getId()); + Log.d(TAG, "mWork.length=" + mWork.length + " mIndex=" + mIndex); + + assertEquals(mWork[mIndex].op, Operation.EVENT_ARG_QUERY); + assertEquals(mWork[mIndex].token, token); + /* + * Even though our TestProvider returned mWork[mIndex].result, it is + * wrapped with new'ed CursorWrapperInner and there's no equal() in + * CursorWrapperInner. assertEquals the two cursor will always fail. + * So just compare the count which will be unique in our TestCursor; + */ + assertEquals(((Cursor) mWork[mIndex].result).getCount(), cursor.getCount()); + + mIndex++; + mCountingSemaphore.release(); + } + + @Override + protected void onInsertComplete(int token, Object cookie, Uri uri) { + Log.d(TAG, "onInsertComplete tid=" + Thread.currentThread().getId()); + Log.d(TAG, "mWork.length=" + mWork.length + " mIndex=" + mIndex); + + assertEquals(mWork[mIndex].op, Operation.EVENT_ARG_INSERT); + assertEquals(mWork[mIndex].token, token); + assertEquals(mWork[mIndex].result, uri); + + mIndex++; + mCountingSemaphore.release(); + } + + @Override + protected void onUpdateComplete(int token, Object cookie, int result) { + Log.d(TAG, "onUpdateComplete tid=" + Thread.currentThread().getId()); + Log.d(TAG, "mWork.length=" + mWork.length + " mIndex=" + mIndex); + + assertEquals(mWork[mIndex].op, Operation.EVENT_ARG_UPDATE); + assertEquals(mWork[mIndex].token, token); + assertEquals(mWork[mIndex].result, result); + + mIndex++; + mCountingSemaphore.release(); + } + + @Override + protected void onDeleteComplete(int token, Object cookie, int result) { + Log.d(TAG, "onDeleteComplete tid=" + Thread.currentThread().getId()); + Log.d(TAG, "mWork.length=" + mWork.length + " mIndex=" + mIndex); + + assertEquals(mWork[mIndex].op, Operation.EVENT_ARG_DELETE); + assertEquals(mWork[mIndex].token, token); + assertEquals(mWork[mIndex].result, result); + + mIndex++; + mCountingSemaphore.release(); + } + + @Override + protected void onBatchComplete(int token, Object cookie, ContentProviderResult[] results) { + Log.d(TAG, "onBatchComplete tid=" + Thread.currentThread().getId()); + Log.d(TAG, "mWork.length=" + mWork.length + " mIndex=" + mIndex); + + assertEquals(mWork[mIndex].op, Operation.EVENT_ARG_BATCH); + assertEquals(mWork[mIndex].token, token); + + ContentProviderResult[] expected = (ContentProviderResult[]) mWork[mIndex].result; + assertEquals(expected.length, results.length); + for (int i = 0; i < expected.length; ++i) { + assertEquals(expected[i].count, results[i].count); + assertEquals(expected[i].uri, results[i].uri); + } + + mIndex++; + mCountingSemaphore.release(); + } + + public int waitForCompletion(long timeoutMills) { + Log.d(TAG, "waitForCompletion tid=" + Thread.currentThread().getId()); + int count = 0; + try { + while (count < mWork.length) { + if (!mCountingSemaphore.tryAcquire(timeoutMills, TimeUnit.MILLISECONDS)) { + break; + } + count++; + } + } catch (InterruptedException e) { + } + return count; + } + } + + /** + * This gets called by AsyncQueryServiceHelper to read or write the data. It + * also verifies the data against the data passed in the constructor + */ + class TestProvider extends ContentProvider { + OperationInfo[] mWork; + + int index = 0; + + public TestProvider(OperationInfo[] work) { + mWork = work; + } + + @Override + public final Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String orderBy) { + Log.d(TAG, "Provider query index=" + index); + assertEquals(mWork[index].op, Operation.EVENT_ARG_QUERY); + assertEquals(mWork[index].uri, uri); + assertEquals(mWork[index].projection, projection); + assertEquals(mWork[index].selection, selection); + assertEquals(mWork[index].selectionArgs, selectionArgs); + assertEquals(mWork[index].orderBy, orderBy); + return (Cursor) mWork[index++].result; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + Log.d(TAG, "Provider insert index=" + index); + assertEquals(mWork[index].op, Operation.EVENT_ARG_INSERT); + assertEquals(mWork[index].uri, uri); + assertEquals(mWork[index].values, values); + return (Uri) mWork[index++].result; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + Log.d(TAG, "Provider update index=" + index); + assertEquals(mWork[index].op, Operation.EVENT_ARG_UPDATE); + assertEquals(mWork[index].uri, uri); + assertEquals(mWork[index].values, values); + assertEquals(mWork[index].selection, selection); + assertEquals(mWork[index].selectionArgs, selectionArgs); + return (Integer) mWork[index++].result; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + Log.d(TAG, "Provider delete index=" + index); + assertEquals(mWork[index].op, Operation.EVENT_ARG_DELETE); + assertEquals(mWork[index].uri, uri); + assertEquals(mWork[index].selection, selection); + assertEquals(mWork[index].selectionArgs, selectionArgs); + return (Integer) mWork[index++].result; + } + + @Override + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) { + Log.d(TAG, "Provider applyBatch index=" + index); + assertEquals(mWork[index].op, Operation.EVENT_ARG_BATCH); + assertEquals(mWork[index].cpo, operations); + return (ContentProviderResult[]) mWork[index++].result; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public boolean onCreate() { + return false; + } + } +} |