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 onQueryComplete(int token, Object cookie, CallerInfo ci)64 void onQueryComplete(int token, Object cookie, CallerInfo ci); 65 66 /** Called when data is loaded. Must be called in worker thread. */ 67 @WorkerThread onDataLoaded(int token, Object cookie, CallerInfo ci)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. */ CallerInfoAsyncQuery()87 private CallerInfoAsyncQuery() {} 88 89 @RequiresPermission(Manifest.permission.READ_CONTACTS) startQuery( final int token, final Context context, final CallerInfo info, final OnQueryCompleteListener listener, final Object cookie)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 startDefaultDirectoryQuery( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)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 */ startQueryInternal( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie, Uri contactRef)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. startOtherDirectoriesQuery( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)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 getDirectoryIds(Context context)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 addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results)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 sanitizeUriToString(Uri uri)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 QueryPoolException(String error)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 DirectoryQueryCompleteListenerFactory( Context context, int size, OnQueryCompleteListener listener)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 onDirectoryQueryComplete( int token, Object cookie, CallerInfo ci, long directoryId)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 addCallerInfoIntoCache(CallerInfo ci, long directoryId)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 newListener(long directoryId)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 DirectoryQueryCompleteListener(long directoryId)356 DirectoryQueryCompleteListener(long directoryId) { 357 this.directoryId = directoryId; 358 } 359 360 @Override onDataLoaded(int token, Object cookie, CallerInfo ci)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 onQueryComplete(int token, Object cookie, CallerInfo ci)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. */ CallerInfoAsyncQueryHandler(Context context, Uri contactRef)389 private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) { 390 super(context.getContentResolver()); 391 this.queryContext = context; 392 this.queryUri = contactRef; 393 } 394 395 @Override startQuery( int token, Object cookie, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy)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 createHandler(Looper looper)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 onQueryComplete(int token, Object cookie, Cursor cursor)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 updateData(int token, Object cookie, Cursor cursor)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 CallerInfoWorkerHandler(Looper looper)565 CallerInfoWorkerHandler(Looper looper) { 566 super(looper); 567 } 568 569 @Override handleMessage(Message msg)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