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.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
     62     void onQueryComplete(int token, Object cookie, CallerInfo ci);
     63 
     64     /** Called when data is loaded. Must be called in worker thread. */
     65     @WorkerThread
     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. */
     85   private CallerInfoAsyncQuery() {}
     86 
     87   @RequiresPermission(Manifest.permission.READ_CONTACTS)
     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
    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    */
    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.
    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 
    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 
    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 
    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 
    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 
    289     DirectoryQueryCompleteListenerFactory(
    290         Context context, int size, OnQueryCompleteListener listener) {
    291       mCount = size;
    292       mListener = listener;
    293       mIsListenerCalled = false;
    294       mContext = context;
    295     }
    296 
    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 
    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 
    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 
    349       DirectoryQueryCompleteListener(long directoryId) {
    350         mDirectoryId = directoryId;
    351       }
    352 
    353       @Override
    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
    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. */
    382     private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) {
    383       super(context.getContentResolver());
    384       this.mQueryContext = context;
    385       this.mQueryUri = contactRef;
    386     }
    387 
    388     @Override
    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
    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
    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 
    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 
    557       CallerInfoWorkerHandler(Looper looper) {
    558         super(looper);
    559       }
    560 
    561       @Override
    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