• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 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 
17 package com.android.incallui;
18 
19 import android.Manifest;
20 import android.annotation.TargetApi;
21 import android.content.AsyncQueryHandler;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.database.Cursor;
25 import android.database.SQLException;
26 import android.net.Uri;
27 import android.os.Build.VERSION;
28 import android.os.Build.VERSION_CODES;
29 import android.os.Handler;
30 import android.os.Looper;
31 import android.os.Message;
32 import android.os.Trace;
33 import android.provider.ContactsContract;
34 import android.provider.ContactsContract.Directory;
35 import android.support.annotation.MainThread;
36 import android.support.annotation.RequiresPermission;
37 import android.support.annotation.WorkerThread;
38 import android.telephony.PhoneNumberUtils;
39 import android.text.TextUtils;
40 import com.android.dialer.common.cp2.DirectoryCompat;
41 import com.android.dialer.phonenumbercache.CachedNumberLookupService;
42 import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
43 import com.android.dialer.phonenumbercache.ContactInfoHelper;
44 import com.android.dialer.phonenumbercache.PhoneNumberCache;
45 import com.android.dialer.strictmode.StrictModeUtils;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 
51 /**
52  * Helper class to make it easier to run asynchronous caller-id lookup queries.
53  *
54  * @see CallerInfo
55  */
56 @TargetApi(VERSION_CODES.M)
57 public class CallerInfoAsyncQuery {
58 
59   /** Interface for a CallerInfoAsyncQueryHandler result return. */
60   interface OnQueryCompleteListener {
61 
62     /** Called when the query is complete. */
63     @MainThread
onQueryComplete(int token, Object cookie, CallerInfo ci)64     void onQueryComplete(int token, Object cookie, CallerInfo ci);
65 
66     /** Called when data is loaded. Must be called in worker thread. */
67     @WorkerThread
onDataLoaded(int token, Object cookie, CallerInfo ci)68     void onDataLoaded(int token, Object cookie, CallerInfo ci);
69   }
70 
71   private static final boolean DBG = false;
72   private static final String LOG_TAG = "CallerInfoAsyncQuery";
73 
74   private static final int EVENT_NEW_QUERY = 1;
75   private static final int EVENT_ADD_LISTENER = 2;
76   private static final int EVENT_EMERGENCY_NUMBER = 3;
77   private static final int EVENT_VOICEMAIL_NUMBER = 4;
78   // If the CallerInfo query finds no contacts, should we use the
79   // PhoneNumberOfflineGeocoder to look up a "geo description"?
80   // (TODO: This could become a flag in config.xml if it ever needs to be
81   // configured on a per-product basis.)
82   private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
83   /* Directory lookup related code - START */
84   private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID};
85 
86   /** Private constructor for factory methods. */
CallerInfoAsyncQuery()87   private CallerInfoAsyncQuery() {}
88 
89   @RequiresPermission(Manifest.permission.READ_CONTACTS)
startQuery( final int token, final Context context, final CallerInfo info, final OnQueryCompleteListener listener, final Object cookie)90   static void startQuery(
91       final int token,
92       final Context context,
93       final CallerInfo info,
94       final OnQueryCompleteListener listener,
95       final Object cookie) {
96     Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####");
97     Log.d(LOG_TAG, "- number: " + info.phoneNumber);
98     Log.d(LOG_TAG, "- cookie: " + cookie);
99 
100     OnQueryCompleteListener contactsProviderQueryCompleteListener =
101         new OnQueryCompleteListener() {
102           @Override
103           public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
104             Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete");
105             // If there are no other directory queries, make sure that the listener is
106             // notified of this result.  see a bug
107             if ((ci != null && ci.contactExists)
108                 || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) {
109               if (listener != null && ci != null) {
110                 listener.onQueryComplete(token, cookie, ci);
111               }
112             }
113           }
114 
115           @Override
116           public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
117             Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onDataLoaded");
118             listener.onDataLoaded(token, cookie, ci);
119           }
120         };
121     startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, cookie);
122   }
123 
124   // Private methods
startDefaultDirectoryQuery( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)125   private static void startDefaultDirectoryQuery(
126       int token,
127       Context context,
128       CallerInfo info,
129       OnQueryCompleteListener listener,
130       Object cookie) {
131     // Construct the URI object and query params, and start the query.
132     Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber);
133     startQueryInternal(token, context, info, listener, cookie, uri);
134   }
135 
136   /**
137    * Factory method to start the query based on a CallerInfo object.
138    *
139    * <p>Note: if the number contains an "@" character we treat it as a SIP address, and look it up
140    * directly in the Data table rather than using the PhoneLookup table. TODO: But eventually we
141    * should expose two separate methods, one for numbers and one for SIP addresses, and then have
142    * PhoneUtils.startGetCallerInfo() decide which one to call based on the phone type of the
143    * incoming connection.
144    */
startQueryInternal( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie, Uri contactRef)145   private static void startQueryInternal(
146       int token,
147       Context context,
148       CallerInfo info,
149       OnQueryCompleteListener listener,
150       Object cookie,
151       Uri contactRef) {
152     if (DBG) {
153       Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
154     }
155 
156     if ((context == null) || (contactRef == null)) {
157       throw new QueryPoolException("Bad context or query uri.");
158     }
159     CallerInfoAsyncQueryHandler handler = new CallerInfoAsyncQueryHandler(context, contactRef);
160 
161     //create cookieWrapper, start query
162     CookieWrapper cw = new CookieWrapper();
163     cw.listener = listener;
164     cw.cookie = cookie;
165     cw.number = info.phoneNumber;
166     cw.countryIso = info.countryIso;
167 
168     // check to see if these are recognized numbers, and use shortcuts if we can.
169     if (PhoneNumberUtils.isLocalEmergencyNumber(context, info.phoneNumber)) {
170       cw.event = EVENT_EMERGENCY_NUMBER;
171     } else if (info.isVoiceMailNumber()) {
172       cw.event = EVENT_VOICEMAIL_NUMBER;
173     } else {
174       cw.event = EVENT_NEW_QUERY;
175     }
176 
177     String[] proejection = CallerInfo.getDefaultPhoneLookupProjection(contactRef);
178     handler.startQuery(
179         token,
180         cw, // cookie
181         contactRef, // uri
182         proejection, // projection
183         null, // selection
184         null, // selectionArgs
185         null); // orderBy
186   }
187 
188   // Return value indicates if listener was notified.
startOtherDirectoriesQuery( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)189   private static boolean startOtherDirectoriesQuery(
190       int token,
191       Context context,
192       CallerInfo info,
193       OnQueryCompleteListener listener,
194       Object cookie) {
195     Trace.beginSection("CallerInfoAsyncQuery.startOtherDirectoriesQuery");
196     long[] directoryIds = StrictModeUtils.bypass(() -> getDirectoryIds(context));
197     int size = directoryIds.length;
198     if (size == 0) {
199       Trace.endSection();
200       return false;
201     }
202 
203     DirectoryQueryCompleteListenerFactory listenerFactory =
204         new DirectoryQueryCompleteListenerFactory(context, size, listener);
205 
206     // The current implementation of multiple async query runs in single handler thread
207     // in AsyncQueryHandler.
208     // intermediateListener.onQueryComplete is also called from the same caller thread.
209     // TODO(a bug): use thread pool instead of single thread.
210     for (int i = 0; i < size; i++) {
211       long directoryId = directoryIds[i];
212       Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId);
213       if (DBG) {
214         Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri);
215       }
216       OnQueryCompleteListener intermediateListener = listenerFactory.newListener(directoryId);
217       startQueryInternal(token, context, info, intermediateListener, cookie, uri);
218     }
219     Trace.endSection();
220     return true;
221   }
222 
getDirectoryIds(Context context)223   private static long[] getDirectoryIds(Context context) {
224     ArrayList<Long> results = new ArrayList<>();
225 
226     Uri uri = Directory.CONTENT_URI;
227     if (VERSION.SDK_INT >= VERSION_CODES.N) {
228       uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise");
229     }
230 
231     ContentResolver cr = context.getContentResolver();
232     Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null);
233     addDirectoryIdsFromCursor(cursor, results);
234 
235     long[] result = new long[results.size()];
236     for (int i = 0; i < results.size(); i++) {
237       result[i] = results.get(i);
238     }
239     return result;
240   }
241 
addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results)242   private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results) {
243     if (cursor != null) {
244       int idIndex = cursor.getColumnIndex(Directory._ID);
245       while (cursor.moveToNext()) {
246         long id = cursor.getLong(idIndex);
247         if (DirectoryCompat.isRemoteDirectoryId(id)) {
248           results.add(id);
249         }
250       }
251       cursor.close();
252     }
253   }
254 
sanitizeUriToString(Uri uri)255   private static String sanitizeUriToString(Uri uri) {
256     if (uri != null) {
257       String uriString = uri.toString();
258       int indexOfLastSlash = uriString.lastIndexOf('/');
259       if (indexOfLastSlash > 0) {
260         return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
261       } else {
262         return uriString;
263       }
264     } else {
265       return "";
266     }
267   }
268 
269   /** Wrap the cookie from the WorkerArgs with additional information needed by our classes. */
270   private static final class CookieWrapper {
271 
272     public OnQueryCompleteListener listener;
273     public Object cookie;
274     public int event;
275     public String number;
276     public String countryIso;
277   }
278   /* Directory lookup related code - END */
279 
280   /** Simple exception used to communicate problems with the query pool. */
281   private static class QueryPoolException extends SQLException {
282 
QueryPoolException(String error)283     QueryPoolException(String error) {
284       super(error);
285     }
286   }
287 
288   private static final class DirectoryQueryCompleteListenerFactory {
289 
290     private final OnQueryCompleteListener listener;
291     private final Context context;
292     // Make sure listener to be called once and only once
293     private int count;
294     private boolean isListenerCalled;
295 
DirectoryQueryCompleteListenerFactory( Context context, int size, OnQueryCompleteListener listener)296     DirectoryQueryCompleteListenerFactory(
297         Context context, int size, OnQueryCompleteListener listener) {
298       count = size;
299       this.listener = listener;
300       isListenerCalled = false;
301       this.context = context;
302     }
303 
onDirectoryQueryComplete( int token, Object cookie, CallerInfo ci, long directoryId)304     private void onDirectoryQueryComplete(
305         int token, Object cookie, CallerInfo ci, long directoryId) {
306       boolean shouldCallListener = false;
307       synchronized (this) {
308         count = count - 1;
309         if (!isListenerCalled && (ci.contactExists || count == 0)) {
310           isListenerCalled = true;
311           shouldCallListener = true;
312         }
313       }
314 
315       // Don't call callback in synchronized block because mListener.onQueryComplete may
316       // take long time to complete
317       if (shouldCallListener && listener != null) {
318         addCallerInfoIntoCache(ci, directoryId);
319         listener.onQueryComplete(token, cookie, ci);
320       }
321     }
322 
addCallerInfoIntoCache(CallerInfo ci, long directoryId)323     private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) {
324       CachedNumberLookupService cachedNumberLookupService =
325           PhoneNumberCache.get(context).getCachedNumberLookupService();
326       if (ci.contactExists && cachedNumberLookupService != null) {
327         // 1. Cache caller info
328         CachedContactInfo cachedContactInfo =
329             CallerInfoUtils.buildCachedContactInfo(cachedNumberLookupService, ci);
330         String directoryLabel = context.getString(R.string.directory_search_label);
331         cachedContactInfo.setDirectorySource(directoryLabel, directoryId);
332         cachedNumberLookupService.addContact(context, cachedContactInfo);
333 
334         // 2. Cache photo
335         if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) {
336           try (InputStream in =
337               context.getContentResolver().openInputStream(ci.contactDisplayPhotoUri)) {
338             if (in != null) {
339               cachedNumberLookupService.addPhoto(context, ci.normalizedNumber, in);
340             }
341           } catch (IOException e) {
342             Log.e(LOG_TAG, "failed to fetch directory contact photo", e);
343           }
344         }
345       }
346     }
347 
newListener(long directoryId)348     OnQueryCompleteListener newListener(long directoryId) {
349       return new DirectoryQueryCompleteListener(directoryId);
350     }
351 
352     private class DirectoryQueryCompleteListener implements OnQueryCompleteListener {
353 
354       private final long directoryId;
355 
DirectoryQueryCompleteListener(long directoryId)356       DirectoryQueryCompleteListener(long directoryId) {
357         this.directoryId = directoryId;
358       }
359 
360       @Override
onDataLoaded(int token, Object cookie, CallerInfo ci)361       public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
362         Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onDataLoaded");
363         listener.onDataLoaded(token, cookie, ci);
364       }
365 
366       @Override
onQueryComplete(int token, Object cookie, CallerInfo ci)367       public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
368         Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onQueryComplete");
369         onDirectoryQueryComplete(token, cookie, ci, directoryId);
370       }
371     }
372   }
373 
374   /** Our own implementation of the AsyncQueryHandler. */
375   private static class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
376 
377     /**
378      * The information relevant to each CallerInfo query. Each query may have multiple listeners, so
379      * each AsyncCursorInfo is associated with 2 or more CookieWrapper objects in the queue (one
380      * with a new query event, and one with a end event, with 0 or more additional listeners in
381      * between).
382      */
383     private Context queryContext;
384 
385     private Uri queryUri;
386     private CallerInfo callerInfo;
387 
388     /** Asynchronous query handler class for the contact / callerinfo object. */
CallerInfoAsyncQueryHandler(Context context, Uri contactRef)389     private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) {
390       super(context.getContentResolver());
391       this.queryContext = context;
392       this.queryUri = contactRef;
393     }
394 
395     @Override
startQuery( int token, Object cookie, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy)396     public void startQuery(
397         int token,
398         Object cookie,
399         Uri uri,
400         String[] projection,
401         String selection,
402         String[] selectionArgs,
403         String orderBy) {
404       if (DBG) {
405         // Show stack trace with the arguments.
406         Log.d(
407             LOG_TAG,
408             "InCall: startQuery: url="
409                 + uri
410                 + " projection=["
411                 + Arrays.toString(projection)
412                 + "]"
413                 + " selection="
414                 + selection
415                 + " "
416                 + " args=["
417                 + Arrays.toString(selectionArgs)
418                 + "]",
419             new RuntimeException("STACKTRACE"));
420       }
421       super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
422     }
423 
424     @Override
createHandler(Looper looper)425     protected Handler createHandler(Looper looper) {
426       return new CallerInfoWorkerHandler(looper);
427     }
428 
429     /**
430      * Overrides onQueryComplete from AsyncQueryHandler.
431      *
432      * <p>This method takes into account the state of this class; we construct the CallerInfo object
433      * only once for each set of listeners. When the query thread has done its work and calls this
434      * method, we inform the remaining listeners in the queue, until we're out of listeners. Once we
435      * get the message indicating that we should expect no new listeners for this CallerInfo object,
436      * we release the AsyncCursorInfo back into the pool.
437      */
438     @Override
onQueryComplete(int token, Object cookie, Cursor cursor)439     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
440       Log.d(this, "##### onQueryComplete() #####   query complete for token: " + token);
441 
442       CookieWrapper cw = (CookieWrapper) cookie;
443 
444       if (cw.listener != null) {
445         Log.d(
446             this,
447             "notifying listener: "
448                 + cw.listener.getClass().toString()
449                 + " for token: "
450                 + token
451                 + callerInfo);
452         cw.listener.onQueryComplete(token, cw.cookie, callerInfo);
453       }
454       queryContext = null;
455       queryUri = null;
456       callerInfo = null;
457     }
458 
updateData(int token, Object cookie, Cursor cursor)459     void updateData(int token, Object cookie, Cursor cursor) {
460       try {
461         Log.d(this, "##### updateData() #####  for token: " + token);
462 
463         //get the cookie and notify the listener.
464         CookieWrapper cw = (CookieWrapper) cookie;
465         if (cw == null) {
466           // Normally, this should never be the case for calls originating
467           // from within this code.
468           // However, if there is any code that calls this method, we should
469           // check the parameters to make sure they're viable.
470           Log.d(this, "Cookie is null, ignoring onQueryComplete() request.");
471           return;
472         }
473 
474         // check the token and if needed, create the callerinfo object.
475         if (callerInfo == null) {
476           if ((queryContext == null) || (queryUri == null)) {
477             throw new QueryPoolException(
478                 "Bad context or query uri, or CallerInfoAsyncQuery already released.");
479           }
480 
481           // adjust the callerInfo data as needed, and only if it was set from the
482           // initial query request.
483           // Change the callerInfo number ONLY if it is an emergency number or the
484           // voicemail number, and adjust other data (including photoResource)
485           // accordingly.
486           if (cw.event == EVENT_EMERGENCY_NUMBER) {
487             // Note we're setting the phone number here (refer to javadoc
488             // comments at the top of CallerInfo class).
489             callerInfo = new CallerInfo().markAsEmergency(queryContext);
490           } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
491             callerInfo = new CallerInfo().markAsVoiceMail(queryContext);
492           } else {
493             callerInfo = CallerInfo.getCallerInfo(queryContext, queryUri, cursor);
494             Log.d(this, "==> Got mCallerInfo: " + callerInfo);
495 
496             CallerInfo newCallerInfo =
497                 CallerInfo.doSecondaryLookupIfNecessary(queryContext, cw.number, callerInfo);
498             if (newCallerInfo != callerInfo) {
499               callerInfo = newCallerInfo;
500               Log.d(this, "#####async contact look up with numeric username" + callerInfo);
501             }
502             callerInfo.countryIso = cw.countryIso;
503 
504             // Final step: look up the geocoded description.
505             if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
506               // Note we do this only if we *don't* have a valid name (i.e. if
507               // no contacts matched the phone number of the incoming call),
508               // since that's the only case where the incoming-call UI cares
509               // about this field.
510               //
511               // (TODO: But if we ever want the UI to show the geoDescription
512               // even when we *do* match a contact, we'll need to either call
513               // updateGeoDescription() unconditionally here, or possibly add a
514               // new parameter to CallerInfoAsyncQuery.startQuery() to force
515               // the geoDescription field to be populated.)
516 
517               if (TextUtils.isEmpty(callerInfo.name)) {
518                 // Actually when no contacts match the incoming phone number,
519                 // the CallerInfo object is totally blank here (i.e. no name
520                 // *or* phoneNumber).  So we need to pass in cw.number as
521                 // a fallback number.
522                 callerInfo.updateGeoDescription(queryContext, cw.number);
523               }
524             }
525 
526             // Use the number entered by the user for display.
527             if (!TextUtils.isEmpty(cw.number)) {
528               callerInfo.phoneNumber = cw.number;
529             }
530           }
531 
532           Log.d(this, "constructing CallerInfo object for token: " + token);
533 
534           if (cw.listener != null) {
535             cw.listener.onDataLoaded(token, cw.cookie, callerInfo);
536           }
537         }
538 
539       } finally {
540         // The cursor may have been closed in CallerInfo.getCallerInfo()
541         if (cursor != null && !cursor.isClosed()) {
542           cursor.close();
543         }
544       }
545     }
546 
547     /**
548      * Our own query worker thread.
549      *
550      * <p>This thread handles the messages enqueued in the looper. The normal sequence of events is
551      * that a new query shows up in the looper queue, followed by 0 or more add listener requests,
552      * and then an end request. Of course, these requests can be interlaced with requests from other
553      * tokens, but is irrelevant to this handler since the handler has no state.
554      *
555      * <p>Note that we depend on the queue to keep things in order; in other words, the looper queue
556      * must be FIFO with respect to input from the synchronous startQuery calls and output to this
557      * handleMessage call.
558      *
559      * <p>This use of the queue is required because CallerInfo objects may be accessed multiple
560      * times before the query is complete. All accesses (listeners) must be queued up and informed
561      * in order when the query is complete.
562      */
563     class CallerInfoWorkerHandler extends WorkerHandler {
564 
CallerInfoWorkerHandler(Looper looper)565       CallerInfoWorkerHandler(Looper looper) {
566         super(looper);
567       }
568 
569       @Override
handleMessage(Message msg)570       public void handleMessage(Message msg) {
571         WorkerArgs args = (WorkerArgs) msg.obj;
572         CookieWrapper cw = (CookieWrapper) args.cookie;
573 
574         if (cw == null) {
575           // Normally, this should never be the case for calls originating
576           // from within this code.
577           // However, if there is any code that this Handler calls (such as in
578           // super.handleMessage) that DOES place unexpected messages on the
579           // queue, then we need pass these messages on.
580           Log.d(
581               this,
582               "Unexpected command (CookieWrapper is null): "
583                   + msg.what
584                   + " ignored by CallerInfoWorkerHandler, passing onto parent.");
585 
586           super.handleMessage(msg);
587         } else {
588           Log.d(
589               this,
590               "Processing event: "
591                   + cw.event
592                   + " token (arg1): "
593                   + msg.arg1
594                   + " command: "
595                   + msg.what
596                   + " query URI: "
597                   + sanitizeUriToString(args.uri));
598 
599           switch (cw.event) {
600             case EVENT_NEW_QUERY:
601               final ContentResolver resolver = queryContext.getContentResolver();
602 
603               // This should never happen.
604               if (resolver == null) {
605                 Log.e(this, "Content Resolver is null!");
606                 return;
607               }
608               // start the sql command.
609               Cursor cursor;
610               try {
611                 cursor =
612                     resolver.query(
613                         args.uri,
614                         args.projection,
615                         args.selection,
616                         args.selectionArgs,
617                         args.orderBy);
618                 // Calling getCount() causes the cursor window to be filled,
619                 // which will make the first access on the main thread a lot faster.
620                 if (cursor != null) {
621                   cursor.getCount();
622                 }
623               } catch (Exception e) {
624                 Log.e(this, "Exception thrown during handling EVENT_ARG_QUERY", e);
625                 cursor = null;
626               }
627 
628               args.result = cursor;
629               updateData(msg.arg1, cw, cursor);
630               break;
631 
632               // shortcuts to avoid query for recognized numbers.
633             case EVENT_EMERGENCY_NUMBER:
634             case EVENT_VOICEMAIL_NUMBER:
635             case EVENT_ADD_LISTENER:
636               updateData(msg.arg1, cw, (Cursor) args.result);
637               break;
638             default: // fall out
639           }
640           Message reply = args.handler.obtainMessage(msg.what);
641           reply.obj = args;
642           reply.arg1 = msg.arg1;
643 
644           reply.sendToTarget();
645         }
646       }
647     }
648   }
649 }
650