Home | History | Annotate | Download | only in incallui
      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
     64     void onQueryComplete(int token, Object cookie, CallerInfo ci);
     65 
     66     /** Called when data is loaded. Must be called in worker thread. */
     67     @WorkerThread
     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. */
     87   private CallerInfoAsyncQuery() {}
     88 
     89   @RequiresPermission(Manifest.permission.READ_CONTACTS)
     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
    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    */
    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.
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    356       DirectoryQueryCompleteListener(long directoryId) {
    357         this.directoryId = directoryId;
    358       }
    359 
    360       @Override
    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
    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. */
    389     private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) {
    390       super(context.getContentResolver());
    391       this.queryContext = context;
    392       this.queryUri = contactRef;
    393     }
    394 
    395     @Override
    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
    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
    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 
    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 
    565       CallerInfoWorkerHandler(Looper looper) {
    566         super(looper);
    567       }
    568 
    569       @Override
    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