summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml3
-rw-r--r--src/com/android/calendar/AgendaWindowAdapter.java1
-rw-r--r--src/com/android/calendar/AlertActivity.java18
-rw-r--r--src/com/android/calendar/AsyncQueryService.java426
-rw-r--r--src/com/android/calendar/AsyncQueryServiceHelper.java371
-rw-r--r--src/com/android/calendar/EditEvent.java25
-rw-r--r--src/com/android/calendar/SelectCalendarsAdapter.java13
-rw-r--r--src/com/android/calendar/Utils.java3
-rw-r--r--tests/src/com/android/calendar/AsyncQueryServiceTest.java628
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;
+ }
+ }
+}