• 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.content.AsyncQueryHandler;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.database.SQLException;
25 import android.net.Uri;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.os.Message;
29 import android.os.Trace;
30 import android.provider.ContactsContract;
31 import android.provider.ContactsContract.Directory;
32 import android.support.annotation.MainThread;
33 import android.support.annotation.RequiresPermission;
34 import android.support.annotation.WorkerThread;
35 import android.text.TextUtils;
36 import com.android.dialer.phonenumbercache.CachedNumberLookupService;
37 import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
38 import com.android.dialer.phonenumbercache.ContactInfoHelper;
39 import com.android.dialer.phonenumbercache.PhoneNumberCache;
40 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
41 import com.android.dialer.strictmode.StrictModeUtils;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 
47 /**
48  * Helper class to make it easier to run asynchronous caller-id lookup queries.
49  *
50  * @see CallerInfo
51  */
52 public class CallerInfoAsyncQuery {
53 
54   /** Interface for a CallerInfoAsyncQueryHandler result return. */
55   interface OnQueryCompleteListener {
56 
57     /** Called when the query is complete. */
58     @MainThread
onQueryComplete(int token, Object cookie, CallerInfo ci)59     void onQueryComplete(int token, Object cookie, CallerInfo ci);
60 
61     /** Called when data is loaded. Must be called in worker thread. */
62     @WorkerThread
onDataLoaded(int token, Object cookie, CallerInfo ci)63     void onDataLoaded(int token, Object cookie, CallerInfo ci);
64   }
65 
66   private static final boolean DBG = false;
67   private static final String LOG_TAG = "CallerInfoAsyncQuery";
68 
69   private static final int EVENT_NEW_QUERY = 1;
70   private static final int EVENT_ADD_LISTENER = 2;
71   private static final int EVENT_EMERGENCY_NUMBER = 3;
72   private static final int EVENT_VOICEMAIL_NUMBER = 4;
73   // If the CallerInfo query finds no contacts, should we use the
74   // PhoneNumberOfflineGeocoder to look up a "geo description"?
75   // (TODO: This could become a flag in config.xml if it ever needs to be
76   // configured on a per-product basis.)
77   private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
78   /* Directory lookup related code - START */
79   private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID};
80 
81   /** Private constructor for factory methods. */
CallerInfoAsyncQuery()82   private CallerInfoAsyncQuery() {}
83 
84   @RequiresPermission(Manifest.permission.READ_CONTACTS)
startQuery( final int token, final Context context, final CallerInfo info, final OnQueryCompleteListener listener, final Object cookie)85   static void startQuery(
86       final int token,
87       final Context context,
88       final CallerInfo info,
89       final OnQueryCompleteListener listener,
90       final Object cookie) {
91     Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####");
92     Log.d(LOG_TAG, "- number: " + info.phoneNumber);
93     Log.d(LOG_TAG, "- cookie: " + cookie);
94 
95     OnQueryCompleteListener contactsProviderQueryCompleteListener =
96         new OnQueryCompleteListener() {
97           @Override
98           public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
99             Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete");
100             // If there are no other directory queries, make sure that the listener is
101             // notified of this result.  see a bug
102             if ((ci != null && ci.contactExists)
103                 || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) {
104               if (listener != null && ci != null) {
105                 listener.onQueryComplete(token, cookie, ci);
106               }
107             }
108           }
109 
110           @Override
111           public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
112             Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onDataLoaded");
113             listener.onDataLoaded(token, cookie, ci);
114           }
115         };
116     startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, cookie);
117   }
118 
119   // Private methods
startDefaultDirectoryQuery( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)120   private static void startDefaultDirectoryQuery(
121       int token,
122       Context context,
123       CallerInfo info,
124       OnQueryCompleteListener listener,
125       Object cookie) {
126     // Construct the URI object and query params, and start the query.
127     Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber);
128     startQueryInternal(token, context, info, listener, cookie, uri);
129   }
130 
131   /**
132    * Factory method to start the query based on a CallerInfo object.
133    *
134    * <p>Note: if the number contains an "@" character we treat it as a SIP address, and look it up
135    * directly in the Data table rather than using the PhoneLookup table. TODO: But eventually we
136    * should expose two separate methods, one for numbers and one for SIP addresses, and then have
137    * PhoneUtils.startGetCallerInfo() decide which one to call based on the phone type of the
138    * incoming connection.
139    */
startQueryInternal( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie, Uri contactRef)140   private static void startQueryInternal(
141       int token,
142       Context context,
143       CallerInfo info,
144       OnQueryCompleteListener listener,
145       Object cookie,
146       Uri contactRef) {
147     if (DBG) {
148       Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
149     }
150 
151     if ((context == null) || (contactRef == null)) {
152       throw new QueryPoolException("Bad context or query uri.");
153     }
154     CallerInfoAsyncQueryHandler handler = new CallerInfoAsyncQueryHandler(context, contactRef);
155 
156     //create cookieWrapper, start query
157     CookieWrapper cw = new CookieWrapper();
158     cw.listener = listener;
159     cw.cookie = cookie;
160     cw.number = info.phoneNumber;
161     cw.countryIso = info.countryIso;
162 
163     // check to see if these are recognized numbers, and use shortcuts if we can.
164     if (PhoneNumberHelper.isLocalEmergencyNumber(context, info.phoneNumber)) {
165       cw.event = EVENT_EMERGENCY_NUMBER;
166     } else if (info.isVoiceMailNumber()) {
167       cw.event = EVENT_VOICEMAIL_NUMBER;
168     } else {
169       cw.event = EVENT_NEW_QUERY;
170     }
171 
172     String[] proejection = CallerInfo.getDefaultPhoneLookupProjection();
173     handler.startQuery(
174         token,
175         cw, // cookie
176         contactRef, // uri
177         proejection, // projection
178         null, // selection
179         null, // selectionArgs
180         null); // orderBy
181   }
182 
183   // Return value indicates if listener was notified.
startOtherDirectoriesQuery( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)184   private static boolean startOtherDirectoriesQuery(
185       int token,
186       Context context,
187       CallerInfo info,
188       OnQueryCompleteListener listener,
189       Object cookie) {
190     Trace.beginSection("CallerInfoAsyncQuery.startOtherDirectoriesQuery");
191     long[] directoryIds = StrictModeUtils.bypass(() -> getDirectoryIds(context));
192     int size = directoryIds.length;
193     if (size == 0) {
194       Trace.endSection();
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(a bug): 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     Trace.endSection();
215     return true;
216   }
217 
getDirectoryIds(Context context)218   private static long[] getDirectoryIds(Context context) {
219     ArrayList<Long> results = new ArrayList<>();
220 
221     Uri uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise");
222 
223     ContentResolver cr = context.getContentResolver();
224     Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null);
225     addDirectoryIdsFromCursor(cursor, results);
226 
227     long[] result = new long[results.size()];
228     for (int i = 0; i < results.size(); i++) {
229       result[i] = results.get(i);
230     }
231     return result;
232   }
233 
addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results)234   private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results) {
235     if (cursor != null) {
236       int idIndex = cursor.getColumnIndex(Directory._ID);
237       while (cursor.moveToNext()) {
238         long id = cursor.getLong(idIndex);
239         if (Directory.isRemoteDirectoryId(id)) {
240           results.add(id);
241         }
242       }
243       cursor.close();
244     }
245   }
246 
sanitizeUriToString(Uri uri)247   private static String sanitizeUriToString(Uri uri) {
248     if (uri != null) {
249       String uriString = uri.toString();
250       int indexOfLastSlash = uriString.lastIndexOf('/');
251       if (indexOfLastSlash > 0) {
252         return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
253       } else {
254         return uriString;
255       }
256     } else {
257       return "";
258     }
259   }
260 
261   /** Wrap the cookie from the WorkerArgs with additional information needed by our classes. */
262   private static final class CookieWrapper {
263 
264     public OnQueryCompleteListener listener;
265     public Object cookie;
266     public int event;
267     public String number;
268     public String countryIso;
269   }
270   /* Directory lookup related code - END */
271 
272   /** Simple exception used to communicate problems with the query pool. */
273   private static class QueryPoolException extends SQLException {
274 
QueryPoolException(String error)275     QueryPoolException(String error) {
276       super(error);
277     }
278   }
279 
280   private static final class DirectoryQueryCompleteListenerFactory {
281 
282     private final OnQueryCompleteListener listener;
283     private final Context context;
284     // Make sure listener to be called once and only once
285     private int count;
286     private boolean isListenerCalled;
287 
DirectoryQueryCompleteListenerFactory( Context context, int size, OnQueryCompleteListener listener)288     DirectoryQueryCompleteListenerFactory(
289         Context context, int size, OnQueryCompleteListener listener) {
290       count = size;
291       this.listener = listener;
292       isListenerCalled = false;
293       this.context = context;
294     }
295 
onDirectoryQueryComplete( int token, Object cookie, CallerInfo ci, long directoryId)296     private void onDirectoryQueryComplete(
297         int token, Object cookie, CallerInfo ci, long directoryId) {
298       boolean shouldCallListener = false;
299       synchronized (this) {
300         count = count - 1;
301         if (!isListenerCalled && (ci.contactExists || count == 0)) {
302           isListenerCalled = true;
303           shouldCallListener = true;
304         }
305       }
306 
307       // Don't call callback in synchronized block because mListener.onQueryComplete may
308       // take long time to complete
309       if (shouldCallListener && listener != null) {
310         addCallerInfoIntoCache(ci, directoryId);
311         listener.onQueryComplete(token, cookie, ci);
312       }
313     }
314 
addCallerInfoIntoCache(CallerInfo ci, long directoryId)315     private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) {
316       CachedNumberLookupService cachedNumberLookupService =
317           PhoneNumberCache.get(context).getCachedNumberLookupService();
318       if (ci.contactExists && cachedNumberLookupService != null) {
319         // 1. Cache caller info
320         CachedContactInfo cachedContactInfo =
321             CallerInfoUtils.buildCachedContactInfo(cachedNumberLookupService, ci);
322         String directoryLabel = context.getString(R.string.directory_search_label);
323         cachedContactInfo.setDirectorySource(directoryLabel, directoryId);
324         cachedNumberLookupService.addContact(context, cachedContactInfo);
325 
326         // 2. Cache photo
327         if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) {
328           try (InputStream in =
329               context.getContentResolver().openInputStream(ci.contactDisplayPhotoUri)) {
330             if (in != null) {
331               cachedNumberLookupService.addPhoto(context, ci.normalizedNumber, in);
332             }
333           } catch (IOException e) {
334             Log.e(LOG_TAG, "failed to fetch directory contact photo", e);
335           }
336         }
337       }
338     }
339 
newListener(long directoryId)340     OnQueryCompleteListener newListener(long directoryId) {
341       return new DirectoryQueryCompleteListener(directoryId);
342     }
343 
344     private class DirectoryQueryCompleteListener implements OnQueryCompleteListener {
345 
346       private final long directoryId;
347 
DirectoryQueryCompleteListener(long directoryId)348       DirectoryQueryCompleteListener(long directoryId) {
349         this.directoryId = directoryId;
350       }
351 
352       @Override
onDataLoaded(int token, Object cookie, CallerInfo ci)353       public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
354         Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onDataLoaded");
355         listener.onDataLoaded(token, cookie, ci);
356       }
357 
358       @Override
onQueryComplete(int token, Object cookie, CallerInfo ci)359       public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
360         Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onQueryComplete");
361         onDirectoryQueryComplete(token, cookie, ci, directoryId);
362       }
363     }
364   }
365 
366   /** Our own implementation of the AsyncQueryHandler. */
367   private static class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
368 
369     /**
370      * The information relevant to each CallerInfo query. Each query may have multiple listeners, so
371      * each AsyncCursorInfo is associated with 2 or more CookieWrapper objects in the queue (one
372      * with a new query event, and one with a end event, with 0 or more additional listeners in
373      * between).
374      */
375     private Context queryContext;
376 
377     private Uri queryUri;
378     private CallerInfo callerInfo;
379 
380     /** Asynchronous query handler class for the contact / callerinfo object. */
CallerInfoAsyncQueryHandler(Context context, Uri contactRef)381     private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) {
382       super(context.getContentResolver());
383       this.queryContext = context;
384       this.queryUri = contactRef;
385     }
386 
387     @Override
startQuery( int token, Object cookie, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy)388     public void startQuery(
389         int token,
390         Object cookie,
391         Uri uri,
392         String[] projection,
393         String selection,
394         String[] selectionArgs,
395         String orderBy) {
396       if (DBG) {
397         // Show stack trace with the arguments.
398         Log.d(
399             LOG_TAG,
400             "InCall: startQuery: url="
401                 + uri
402                 + " projection=["
403                 + Arrays.toString(projection)
404                 + "]"
405                 + " selection="
406                 + selection
407                 + " "
408                 + " args=["
409                 + Arrays.toString(selectionArgs)
410                 + "]",
411             new RuntimeException("STACKTRACE"));
412       }
413       super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
414     }
415 
416     @Override
createHandler(Looper looper)417     protected Handler createHandler(Looper looper) {
418       return new CallerInfoWorkerHandler(looper);
419     }
420 
421     /**
422      * Overrides onQueryComplete from AsyncQueryHandler.
423      *
424      * <p>This method takes into account the state of this class; we construct the CallerInfo object
425      * only once for each set of listeners. When the query thread has done its work and calls this
426      * method, we inform the remaining listeners in the queue, until we're out of listeners. Once we
427      * get the message indicating that we should expect no new listeners for this CallerInfo object,
428      * we release the AsyncCursorInfo back into the pool.
429      */
430     @Override
onQueryComplete(int token, Object cookie, Cursor cursor)431     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
432       Log.d(this, "##### onQueryComplete() #####   query complete for token: " + token);
433 
434       CookieWrapper cw = (CookieWrapper) cookie;
435 
436       if (cw.listener != null) {
437         Log.d(
438             this,
439             "notifying listener: "
440                 + cw.listener.getClass().toString()
441                 + " for token: "
442                 + token
443                 + callerInfo);
444         cw.listener.onQueryComplete(token, cw.cookie, callerInfo);
445       }
446       queryContext = null;
447       queryUri = null;
448       callerInfo = null;
449     }
450 
updateData(int token, Object cookie, Cursor cursor)451     void updateData(int token, Object cookie, Cursor cursor) {
452       try {
453         Log.d(this, "##### updateData() #####  for token: " + token);
454 
455         //get the cookie and notify the listener.
456         CookieWrapper cw = (CookieWrapper) cookie;
457         if (cw == null) {
458           // Normally, this should never be the case for calls originating
459           // from within this code.
460           // However, if there is any code that calls this method, we should
461           // check the parameters to make sure they're viable.
462           Log.d(this, "Cookie is null, ignoring onQueryComplete() request.");
463           return;
464         }
465 
466         // check the token and if needed, create the callerinfo object.
467         if (callerInfo == null) {
468           if ((queryContext == null) || (queryUri == null)) {
469             throw new QueryPoolException(
470                 "Bad context or query uri, or CallerInfoAsyncQuery already released.");
471           }
472 
473           // adjust the callerInfo data as needed, and only if it was set from the
474           // initial query request.
475           // Change the callerInfo number ONLY if it is an emergency number or the
476           // voicemail number, and adjust other data (including photoResource)
477           // accordingly.
478           if (cw.event == EVENT_EMERGENCY_NUMBER) {
479             // Note we're setting the phone number here (refer to javadoc
480             // comments at the top of CallerInfo class).
481             callerInfo = new CallerInfo().markAsEmergency(queryContext);
482           } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
483             callerInfo = new CallerInfo().markAsVoiceMail(queryContext);
484           } else {
485             callerInfo = CallerInfo.getCallerInfo(queryContext, queryUri, cursor);
486             Log.d(this, "==> Got mCallerInfo: " + callerInfo);
487 
488             CallerInfo newCallerInfo =
489                 CallerInfo.doSecondaryLookupIfNecessary(queryContext, cw.number, callerInfo);
490             if (newCallerInfo != callerInfo) {
491               callerInfo = newCallerInfo;
492               Log.d(this, "#####async contact look up with numeric username" + callerInfo);
493             }
494             callerInfo.countryIso = cw.countryIso;
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(callerInfo.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                 callerInfo.updateGeoDescription(queryContext, cw.number);
515               }
516             }
517 
518             // Use the number entered by the user for display.
519             if (!TextUtils.isEmpty(cw.number)) {
520               callerInfo.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, callerInfo);
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 = queryContext.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