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