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