1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calendar;
18
19import com.android.calendar.AsyncQueryServiceHelper.OperationInfo;
20
21import android.content.ContentProviderOperation;
22import android.content.ContentProviderResult;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.database.Cursor;
27import android.net.Uri;
28import android.os.Handler;
29import android.os.Message;
30import android.util.Log;
31
32import java.util.ArrayList;
33import java.util.concurrent.atomic.AtomicInteger;
34
35/**
36 * A helper class that executes {@link ContentResolver} calls in a background
37 * {@link android.app.Service}. This minimizes the chance of the call getting
38 * lost because the caller ({@link android.app.Activity}) is killed. It is
39 * designed for easy migration from {@link android.content.AsyncQueryHandler}
40 * which calls the {@link ContentResolver} in a background thread. This supports
41 * query/insert/update/delete and also batch mode i.e.
42 * {@link ContentProviderOperation}. It also supports delay execution and cancel
43 * which allows for time-limited undo. Note that there's one queue per
44 * application which serializes all the calls.
45 */
46public class AsyncQueryService extends Handler {
47    private static final String TAG = "AsyncQuery";
48    static final boolean localLOGV = false;
49
50    // Used for generating unique tokens for calls to this service
51    private static AtomicInteger mUniqueToken = new AtomicInteger(0);
52
53    private Context mContext;
54    private Handler mHandler = this; // can be overridden for testing
55
56    /**
57     * Data class which holds into info of the queued operation
58     */
59    public static class Operation {
60        static final int EVENT_ARG_QUERY = 1;
61        static final int EVENT_ARG_INSERT = 2;
62        static final int EVENT_ARG_UPDATE = 3;
63        static final int EVENT_ARG_DELETE = 4;
64        static final int EVENT_ARG_BATCH = 5;
65
66        /**
67         * unique identify for cancellation purpose
68         */
69        public int token;
70
71        /**
72         * One of the EVENT_ARG_ constants in the class describing the operation
73         */
74        public int op;
75
76        /**
77         * {@link SystemClock.elapsedRealtime()} based
78         */
79        public long scheduledExecutionTime;
80
81        protected static char opToChar(int op) {
82            switch (op) {
83                case Operation.EVENT_ARG_QUERY:
84                    return 'Q';
85                case Operation.EVENT_ARG_INSERT:
86                    return 'I';
87                case Operation.EVENT_ARG_UPDATE:
88                    return 'U';
89                case Operation.EVENT_ARG_DELETE:
90                    return 'D';
91                case Operation.EVENT_ARG_BATCH:
92                    return 'B';
93                default:
94                    return '?';
95            }
96        }
97
98        @Override
99        public String toString() {
100            StringBuilder builder = new StringBuilder();
101            builder.append("Operation [op=");
102            builder.append(op);
103            builder.append(", token=");
104            builder.append(token);
105            builder.append(", scheduledExecutionTime=");
106            builder.append(scheduledExecutionTime);
107            builder.append("]");
108            return builder.toString();
109        }
110    }
111
112    public AsyncQueryService(Context context) {
113        mContext = context;
114    }
115
116    /**
117     * returns a practically unique token for db operations
118     */
119    public final int getNextToken() {
120        return mUniqueToken.getAndIncrement();
121    }
122
123    /**
124     * Gets the last delayed operation. It is typically used for canceling.
125     *
126     * @return Operation object which contains of the last cancelable operation
127     */
128    public final Operation getLastCancelableOperation() {
129        return AsyncQueryServiceHelper.getLastCancelableOperation();
130    }
131
132    /**
133     * Attempts to cancel operation that has not already started. Note that
134     * there is no guarantee that the operation will be canceled. They still may
135     * result in a call to on[Query/Insert/Update/Delete/Batch]Complete after
136     * this call has completed.
137     *
138     * @param token The token representing the operation to be canceled. If
139     *            multiple operations have the same token they will all be
140     *            canceled.
141     */
142    public final int cancelOperation(int token) {
143        return AsyncQueryServiceHelper.cancelOperation(token);
144    }
145
146    /**
147     * This method begins an asynchronous query. When the query is done
148     * {@link #onQueryComplete} is called.
149     *
150     * @param token A token passed into {@link #onQueryComplete} to identify the
151     *            query.
152     * @param cookie An object that gets passed into {@link #onQueryComplete}
153     * @param uri The URI, using the content:// scheme, for the content to
154     *            retrieve.
155     * @param projection A list of which columns to return. Passing null will
156     *            return all columns, which is discouraged to prevent reading
157     *            data from storage that isn't going to be used.
158     * @param selection A filter declaring which rows to return, formatted as an
159     *            SQL WHERE clause (excluding the WHERE itself). Passing null
160     *            will return all rows for the given URI.
161     * @param selectionArgs You may include ?s in selection, which will be
162     *            replaced by the values from selectionArgs, in the order that
163     *            they appear in the selection. The values will be bound as
164     *            Strings.
165     * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause
166     *            (excluding the ORDER BY itself). Passing null will use the
167     *            default sort order, which may be unordered.
168     */
169    public void startQuery(int token, Object cookie, Uri uri, String[] projection,
170            String selection, String[] selectionArgs, String orderBy) {
171        OperationInfo info = new OperationInfo();
172        info.op = Operation.EVENT_ARG_QUERY;
173        info.resolver = mContext.getContentResolver();
174
175        info.handler = mHandler;
176        info.token = token;
177        info.cookie = cookie;
178        info.uri = uri;
179        info.projection = projection;
180        info.selection = selection;
181        info.selectionArgs = selectionArgs;
182        info.orderBy = orderBy;
183
184        AsyncQueryServiceHelper.queueOperation(mContext, info);
185    }
186
187    /**
188     * This method begins an asynchronous insert. When the insert operation is
189     * done {@link #onInsertComplete} is called.
190     *
191     * @param token A token passed into {@link #onInsertComplete} to identify
192     *            the insert operation.
193     * @param cookie An object that gets passed into {@link #onInsertComplete}
194     * @param uri the Uri passed to the insert operation.
195     * @param initialValues the ContentValues parameter passed to the insert
196     *            operation.
197     * @param delayMillis delay in executing the operation. This operation will
198     *            execute before the delayed time when another operation is
199     *            added. Useful for implementing single level undo.
200     */
201    public void startInsert(int token, Object cookie, Uri uri, ContentValues initialValues,
202            long delayMillis) {
203        OperationInfo info = new OperationInfo();
204        info.op = Operation.EVENT_ARG_INSERT;
205        info.resolver = mContext.getContentResolver();
206        info.handler = mHandler;
207
208        info.token = token;
209        info.cookie = cookie;
210        info.uri = uri;
211        info.values = initialValues;
212        info.delayMillis = delayMillis;
213
214        AsyncQueryServiceHelper.queueOperation(mContext, info);
215    }
216
217    /**
218     * This method begins an asynchronous update. When the update operation is
219     * done {@link #onUpdateComplete} is called.
220     *
221     * @param token A token passed into {@link #onUpdateComplete} to identify
222     *            the update operation.
223     * @param cookie An object that gets passed into {@link #onUpdateComplete}
224     * @param uri the Uri passed to the update operation.
225     * @param values the ContentValues parameter passed to the update operation.
226     * @param selection A filter declaring which rows to update, formatted as an
227     *            SQL WHERE clause (excluding the WHERE itself). Passing null
228     *            will update all rows for the given URI.
229     * @param selectionArgs You may include ?s in selection, which will be
230     *            replaced by the values from selectionArgs, in the order that
231     *            they appear in the selection. The values will be bound as
232     *            Strings.
233     * @param delayMillis delay in executing the operation. This operation will
234     *            execute before the delayed time when another operation is
235     *            added. Useful for implementing single level undo.
236     */
237    public void startUpdate(int token, Object cookie, Uri uri, ContentValues values,
238            String selection, String[] selectionArgs, long delayMillis) {
239        OperationInfo info = new OperationInfo();
240        info.op = Operation.EVENT_ARG_UPDATE;
241        info.resolver = mContext.getContentResolver();
242        info.handler = mHandler;
243
244        info.token = token;
245        info.cookie = cookie;
246        info.uri = uri;
247        info.values = values;
248        info.selection = selection;
249        info.selectionArgs = selectionArgs;
250        info.delayMillis = delayMillis;
251
252        AsyncQueryServiceHelper.queueOperation(mContext, info);
253    }
254
255    /**
256     * This method begins an asynchronous delete. When the delete operation is
257     * done {@link #onDeleteComplete} is called.
258     *
259     * @param token A token passed into {@link #onDeleteComplete} to identify
260     *            the delete operation.
261     * @param cookie An object that gets passed into {@link #onDeleteComplete}
262     * @param uri the Uri passed to the delete operation.
263     * @param selection A filter declaring which rows to delete, formatted as an
264     *            SQL WHERE clause (excluding the WHERE itself). Passing null
265     *            will delete all rows for the given URI.
266     * @param selectionArgs You may include ?s in selection, which will be
267     *            replaced by the values from selectionArgs, in the order that
268     *            they appear in the selection. The values will be bound as
269     *            Strings.
270     * @param delayMillis delay in executing the operation. This operation will
271     *            execute before the delayed time when another operation is
272     *            added. Useful for implementing single level undo.
273     */
274    public void startDelete(int token, Object cookie, Uri uri, String selection,
275            String[] selectionArgs, long delayMillis) {
276        OperationInfo info = new OperationInfo();
277        info.op = Operation.EVENT_ARG_DELETE;
278        info.resolver = mContext.getContentResolver();
279        info.handler = mHandler;
280
281        info.token = token;
282        info.cookie = cookie;
283        info.uri = uri;
284        info.selection = selection;
285        info.selectionArgs = selectionArgs;
286        info.delayMillis = delayMillis;
287
288        AsyncQueryServiceHelper.queueOperation(mContext, info);
289    }
290
291    /**
292     * This method begins an asynchronous {@link ContentProviderOperation}. When
293     * the operation is done {@link #onBatchComplete} is called.
294     *
295     * @param token A token passed into {@link #onDeleteComplete} to identify
296     *            the delete operation.
297     * @param cookie An object that gets passed into {@link #onDeleteComplete}
298     * @param authority the authority used for the
299     *            {@link ContentProviderOperation}.
300     * @param cpo the {@link ContentProviderOperation} to be executed.
301     * @param delayMillis delay in executing the operation. This operation will
302     *            execute before the delayed time when another operation is
303     *            added. Useful for implementing single level undo.
304     */
305    public void startBatch(int token, Object cookie, String authority,
306            ArrayList<ContentProviderOperation> cpo, long delayMillis) {
307        OperationInfo info = new OperationInfo();
308        info.op = Operation.EVENT_ARG_BATCH;
309        info.resolver = mContext.getContentResolver();
310        info.handler = mHandler;
311
312        info.token = token;
313        info.cookie = cookie;
314        info.authority = authority;
315        info.cpo = cpo;
316        info.delayMillis = delayMillis;
317
318        AsyncQueryServiceHelper.queueOperation(mContext, info);
319    }
320
321    /**
322     * Called when an asynchronous query is completed.
323     *
324     * @param token the token to identify the query, passed in from
325     *            {@link #startQuery}.
326     * @param cookie the cookie object passed in from {@link #startQuery}.
327     * @param cursor The cursor holding the results from the query.
328     */
329    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
330        if (localLOGV) {
331            Log.d(TAG, "########## default onQueryComplete");
332        }
333    }
334
335    /**
336     * Called when an asynchronous insert is completed.
337     *
338     * @param token the token to identify the query, passed in from
339     *            {@link #startInsert}.
340     * @param cookie the cookie object that's passed in from
341     *            {@link #startInsert}.
342     * @param uri the uri returned from the insert operation.
343     */
344    protected void onInsertComplete(int token, Object cookie, Uri uri) {
345        if (localLOGV) {
346            Log.d(TAG, "########## default onInsertComplete");
347        }
348    }
349
350    /**
351     * Called when an asynchronous update is completed.
352     *
353     * @param token the token to identify the query, passed in from
354     *            {@link #startUpdate}.
355     * @param cookie the cookie object that's passed in from
356     *            {@link #startUpdate}.
357     * @param result the result returned from the update operation
358     */
359    protected void onUpdateComplete(int token, Object cookie, int result) {
360        if (localLOGV) {
361            Log.d(TAG, "########## default onUpdateComplete");
362        }
363    }
364
365    /**
366     * Called when an asynchronous delete is completed.
367     *
368     * @param token the token to identify the query, passed in from
369     *            {@link #startDelete}.
370     * @param cookie the cookie object that's passed in from
371     *            {@link #startDelete}.
372     * @param result the result returned from the delete operation
373     */
374    protected void onDeleteComplete(int token, Object cookie, int result) {
375        if (localLOGV) {
376            Log.d(TAG, "########## default onDeleteComplete");
377        }
378    }
379
380    /**
381     * Called when an asynchronous {@link ContentProviderOperation} is
382     * completed.
383     *
384     * @param token the token to identify the query, passed in from
385     *            {@link #startDelete}.
386     * @param cookie the cookie object that's passed in from
387     *            {@link #startDelete}.
388     * @param results the result returned from executing the
389     *            {@link ContentProviderOperation}
390     */
391    protected void onBatchComplete(int token, Object cookie, ContentProviderResult[] results) {
392        if (localLOGV) {
393            Log.d(TAG, "########## default onBatchComplete");
394        }
395    }
396
397    @Override
398    public void handleMessage(Message msg) {
399        OperationInfo info = (OperationInfo) msg.obj;
400
401        int token = msg.what;
402        int op = msg.arg1;
403
404        if (localLOGV) {
405            Log.d(TAG, "AsyncQueryService.handleMessage: token=" + token + ", op=" + op
406                    + ", result=" + info.result);
407        }
408
409        // pass token back to caller on each callback.
410        switch (op) {
411            case Operation.EVENT_ARG_QUERY:
412                onQueryComplete(token, info.cookie, (Cursor) info.result);
413                break;
414
415            case Operation.EVENT_ARG_INSERT:
416                onInsertComplete(token, info.cookie, (Uri) info.result);
417                break;
418
419            case Operation.EVENT_ARG_UPDATE:
420                onUpdateComplete(token, info.cookie, (Integer) info.result);
421                break;
422
423            case Operation.EVENT_ARG_DELETE:
424                onDeleteComplete(token, info.cookie, (Integer) info.result);
425                break;
426
427            case Operation.EVENT_ARG_BATCH:
428                onBatchComplete(token, info.cookie, (ContentProviderResult[]) info.result);
429                break;
430        }
431    }
432
433//    @VisibleForTesting
434    protected void setTestHandler(Handler handler) {
435        mHandler = handler;
436    }
437}
438