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.AsyncQueryService.Operation;
20
21import android.app.IntentService;
22import android.content.ContentProviderOperation;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.content.OperationApplicationException;
28import android.database.Cursor;
29import android.net.Uri;
30import android.os.Handler;
31import android.os.Message;
32import android.os.RemoteException;
33import android.os.SystemClock;
34import android.util.Log;
35
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.Iterator;
39import java.util.PriorityQueue;
40import java.util.concurrent.Delayed;
41import java.util.concurrent.TimeUnit;
42
43public class AsyncQueryServiceHelper extends IntentService {
44    private static final String TAG = "AsyncQuery";
45
46    private static final PriorityQueue<OperationInfo> sWorkQueue =
47        new PriorityQueue<OperationInfo>();
48
49    protected Class<AsyncQueryService> mService = AsyncQueryService.class;
50
51    protected static class OperationInfo implements Delayed{
52        public int token; // Used for cancel
53        public int op;
54        public ContentResolver resolver;
55        public Uri uri;
56        public String authority;
57        public Handler handler;
58        public String[] projection;
59        public String selection;
60        public String[] selectionArgs;
61        public String orderBy;
62        public Object result;
63        public Object cookie;
64        public ContentValues values;
65        public ArrayList<ContentProviderOperation> cpo;
66
67        /**
68         * delayMillis is relative time e.g. 10,000 milliseconds
69         */
70        public long delayMillis;
71
72        /**
73         * scheduleTimeMillis is the time scheduled for this to be processed.
74         * e.g. SystemClock.elapsedRealtime() + 10,000 milliseconds Based on
75         * {@link android.os.SystemClock#elapsedRealtime }
76         */
77        private long mScheduledTimeMillis = 0;
78
79        // @VisibleForTesting
80        void calculateScheduledTime() {
81            mScheduledTimeMillis = SystemClock.elapsedRealtime() + delayMillis;
82        }
83
84        // @Override // Uncomment with Java6
85        public long getDelay(TimeUnit unit) {
86            return unit.convert(mScheduledTimeMillis - SystemClock.elapsedRealtime(),
87                    TimeUnit.MILLISECONDS);
88        }
89
90        // @Override // Uncomment with Java6
91        public int compareTo(Delayed another) {
92            OperationInfo anotherArgs = (OperationInfo) another;
93            if (this.mScheduledTimeMillis == anotherArgs.mScheduledTimeMillis) {
94                return 0;
95            } else if (this.mScheduledTimeMillis < anotherArgs.mScheduledTimeMillis) {
96                return -1;
97            } else {
98                return 1;
99            }
100        }
101
102        @Override
103        public String toString() {
104            StringBuilder builder = new StringBuilder();
105            builder.append("OperationInfo [\n\t token= ");
106            builder.append(token);
107            builder.append(",\n\t op= ");
108            builder.append(Operation.opToChar(op));
109            builder.append(",\n\t uri= ");
110            builder.append(uri);
111            builder.append(",\n\t authority= ");
112            builder.append(authority);
113            builder.append(",\n\t delayMillis= ");
114            builder.append(delayMillis);
115            builder.append(",\n\t mScheduledTimeMillis= ");
116            builder.append(mScheduledTimeMillis);
117            builder.append(",\n\t resolver= ");
118            builder.append(resolver);
119            builder.append(",\n\t handler= ");
120            builder.append(handler);
121            builder.append(",\n\t projection= ");
122            builder.append(Arrays.toString(projection));
123            builder.append(",\n\t selection= ");
124            builder.append(selection);
125            builder.append(",\n\t selectionArgs= ");
126            builder.append(Arrays.toString(selectionArgs));
127            builder.append(",\n\t orderBy= ");
128            builder.append(orderBy);
129            builder.append(",\n\t result= ");
130            builder.append(result);
131            builder.append(",\n\t cookie= ");
132            builder.append(cookie);
133            builder.append(",\n\t values= ");
134            builder.append(values);
135            builder.append(",\n\t cpo= ");
136            builder.append(cpo);
137            builder.append("\n]");
138            return builder.toString();
139        }
140
141        /**
142         * Compares an user-visible operation to this private OperationInfo
143         * object
144         *
145         * @param o operation to be compared
146         * @return true if logically equivalent
147         */
148        public boolean equivalent(Operation o) {
149            return o.token == this.token && o.op == this.op;
150        }
151    }
152
153    /**
154     * Queues the operation for execution
155     *
156     * @param context
157     * @param args OperationInfo object describing the operation
158     */
159    static public void queueOperation(Context context, OperationInfo args) {
160        // Set the schedule time for execution based on the desired delay.
161        args.calculateScheduledTime();
162
163        synchronized (sWorkQueue) {
164            sWorkQueue.add(args);
165            sWorkQueue.notify();
166        }
167
168        context.startService(new Intent(context, AsyncQueryServiceHelper.class));
169    }
170
171    /**
172     * Gets the last delayed operation. It is typically used for canceling.
173     *
174     * @return Operation object which contains of the last cancelable operation
175     */
176    static public Operation getLastCancelableOperation() {
177        long lastScheduleTime = Long.MIN_VALUE;
178        Operation op = null;
179
180        synchronized (sWorkQueue) {
181            // Unknown order even for a PriorityQueue
182            Iterator<OperationInfo> it = sWorkQueue.iterator();
183            while (it.hasNext()) {
184                OperationInfo info = it.next();
185                if (info.delayMillis > 0 && lastScheduleTime < info.mScheduledTimeMillis) {
186                    if (op == null) {
187                        op = new Operation();
188                    }
189
190                    op.token = info.token;
191                    op.op = info.op;
192                    op.scheduledExecutionTime = info.mScheduledTimeMillis;
193
194                    lastScheduleTime = info.mScheduledTimeMillis;
195                }
196            }
197        }
198
199        if (AsyncQueryService.localLOGV) {
200            Log.d(TAG, "getLastCancelableOperation -> Operation:" + Operation.opToChar(op.op)
201                    + " token:" + op.token);
202        }
203        return op;
204    }
205
206    /**
207     * Attempts to cancel operation that has not already started. Note that
208     * there is no guarantee that the operation will be canceled. They still may
209     * result in a call to on[Query/Insert/Update/Delete/Batch]Complete after
210     * this call has completed.
211     *
212     * @param token The token representing the operation to be canceled. If
213     *            multiple operations have the same token they will all be
214     *            canceled.
215     */
216    static public int cancelOperation(int token) {
217        int canceled = 0;
218        synchronized (sWorkQueue) {
219            Iterator<OperationInfo> it = sWorkQueue.iterator();
220            while (it.hasNext()) {
221                if (it.next().token == token) {
222                    it.remove();
223                    ++canceled;
224                }
225            }
226        }
227
228        if (AsyncQueryService.localLOGV) {
229            Log.d(TAG, "cancelOperation(" + token + ") -> " + canceled);
230        }
231        return canceled;
232    }
233
234    public AsyncQueryServiceHelper(String name) {
235        super(name);
236    }
237
238    public AsyncQueryServiceHelper() {
239        super("AsyncQueryServiceHelper");
240    }
241
242    @Override
243    protected void onHandleIntent(Intent intent) {
244        OperationInfo args;
245
246        if (AsyncQueryService.localLOGV) {
247            Log.d(TAG, "onHandleIntent: queue size=" + sWorkQueue.size());
248        }
249        synchronized (sWorkQueue) {
250            while (true) {
251                /*
252                 * This method can be called with no work because of
253                 * cancellations
254                 */
255                if (sWorkQueue.size() == 0) {
256                    return;
257                } else if (sWorkQueue.size() == 1) {
258                    OperationInfo first = sWorkQueue.peek();
259                    long waitTime = first.mScheduledTimeMillis - SystemClock.elapsedRealtime();
260                    if (waitTime > 0) {
261                        try {
262                            sWorkQueue.wait(waitTime);
263                        } catch (InterruptedException e) {
264                        }
265                    }
266                }
267
268                args = sWorkQueue.poll();
269                if (args != null) {
270                    // Got work to do. Break out of waiting loop
271                    break;
272                }
273            }
274        }
275
276        if (AsyncQueryService.localLOGV) {
277            Log.d(TAG, "onHandleIntent: " + args);
278        }
279
280        ContentResolver resolver = args.resolver;
281        if (resolver != null) {
282
283            switch (args.op) {
284                case Operation.EVENT_ARG_QUERY:
285                    Cursor cursor;
286                    try {
287                        cursor = resolver.query(args.uri, args.projection, args.selection,
288                                args.selectionArgs, args.orderBy);
289                        /*
290                         * Calling getCount() causes the cursor window to be
291                         * filled, which will make the first access on the main
292                         * thread a lot faster
293                         */
294                        if (cursor != null) {
295                            cursor.getCount();
296                        }
297                    } catch (Exception e) {
298                        Log.w(TAG, e.toString());
299                        cursor = null;
300                    }
301
302                    args.result = cursor;
303                    break;
304
305                case Operation.EVENT_ARG_INSERT:
306                    args.result = resolver.insert(args.uri, args.values);
307                    break;
308
309                case Operation.EVENT_ARG_UPDATE:
310                    args.result = resolver.update(args.uri, args.values, args.selection,
311                            args.selectionArgs);
312                    break;
313
314                case Operation.EVENT_ARG_DELETE:
315                    try {
316                        args.result = resolver.delete(args.uri, args.selection, args.selectionArgs);
317                    } catch (IllegalArgumentException e) {
318                        Log.w(TAG, "Delete failed.");
319                        Log.w(TAG, e.toString());
320                        args.result = 0;
321                    }
322
323                    break;
324
325                case Operation.EVENT_ARG_BATCH:
326                    try {
327                        args.result = resolver.applyBatch(args.authority, args.cpo);
328                    } catch (RemoteException e) {
329                        Log.e(TAG, e.toString());
330                        args.result = null;
331                    } catch (OperationApplicationException e) {
332                        Log.e(TAG, e.toString());
333                        args.result = null;
334                    }
335                    break;
336            }
337
338            /*
339             * passing the original token value back to the caller on top of the
340             * event values in arg1.
341             */
342            Message reply = args.handler.obtainMessage(args.token);
343            reply.obj = args;
344            reply.arg1 = args.op;
345
346            if (AsyncQueryService.localLOGV) {
347                Log.d(TAG, "onHandleIntent: op=" + Operation.opToChar(args.op) + ", token="
348                        + reply.what);
349            }
350
351            reply.sendToTarget();
352        }
353    }
354
355    @Override
356    public void onStart(Intent intent, int startId) {
357        if (AsyncQueryService.localLOGV) {
358            Log.d(TAG, "onStart startId=" + startId);
359        }
360        super.onStart(intent, startId);
361    }
362
363    @Override
364    public void onCreate() {
365        if (AsyncQueryService.localLOGV) {
366            Log.d(TAG, "onCreate");
367        }
368        super.onCreate();
369    }
370
371    @Override
372    public void onDestroy() {
373        if (AsyncQueryService.localLOGV) {
374            Log.d(TAG, "onDestroy");
375        }
376        super.onDestroy();
377    }
378}
379