1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.incallui; 18 19 import android.Manifest; 20 import android.content.AsyncQueryHandler; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.database.Cursor; 24 import android.database.SQLException; 25 import android.net.Uri; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.os.Message; 29 import android.os.Trace; 30 import android.provider.ContactsContract; 31 import android.provider.ContactsContract.Directory; 32 import android.support.annotation.MainThread; 33 import android.support.annotation.RequiresPermission; 34 import android.support.annotation.WorkerThread; 35 import android.text.TextUtils; 36 import com.android.dialer.phonenumbercache.CachedNumberLookupService; 37 import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo; 38 import com.android.dialer.phonenumbercache.ContactInfoHelper; 39 import com.android.dialer.phonenumbercache.PhoneNumberCache; 40 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 41 import com.android.dialer.strictmode.StrictModeUtils; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.util.ArrayList; 45 import java.util.Arrays; 46 47 /** 48 * Helper class to make it easier to run asynchronous caller-id lookup queries. 49 * 50 * @see CallerInfo 51 */ 52 public class CallerInfoAsyncQuery { 53 54 /** Interface for a CallerInfoAsyncQueryHandler result return. */ 55 interface OnQueryCompleteListener { 56 57 /** Called when the query is complete. */ 58 @MainThread onQueryComplete(int token, Object cookie, CallerInfo ci)59 void onQueryComplete(int token, Object cookie, CallerInfo ci); 60 61 /** Called when data is loaded. Must be called in worker thread. */ 62 @WorkerThread onDataLoaded(int token, Object cookie, CallerInfo ci)63 void onDataLoaded(int token, Object cookie, CallerInfo ci); 64 } 65 66 private static final boolean DBG = false; 67 private static final String LOG_TAG = "CallerInfoAsyncQuery"; 68 69 private static final int EVENT_NEW_QUERY = 1; 70 private static final int EVENT_ADD_LISTENER = 2; 71 private static final int EVENT_EMERGENCY_NUMBER = 3; 72 private static final int EVENT_VOICEMAIL_NUMBER = 4; 73 // If the CallerInfo query finds no contacts, should we use the 74 // PhoneNumberOfflineGeocoder to look up a "geo description"? 75 // (TODO: This could become a flag in config.xml if it ever needs to be 76 // configured on a per-product basis.) 77 private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true; 78 /* Directory lookup related code - START */ 79 private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID}; 80 81 /** Private constructor for factory methods. */ CallerInfoAsyncQuery()82 private CallerInfoAsyncQuery() {} 83 84 @RequiresPermission(Manifest.permission.READ_CONTACTS) startQuery( final int token, final Context context, final CallerInfo info, final OnQueryCompleteListener listener, final Object cookie)85 static void startQuery( 86 final int token, 87 final Context context, 88 final CallerInfo info, 89 final OnQueryCompleteListener listener, 90 final Object cookie) { 91 Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####"); 92 Log.d(LOG_TAG, "- number: " + info.phoneNumber); 93 Log.d(LOG_TAG, "- cookie: " + cookie); 94 95 OnQueryCompleteListener contactsProviderQueryCompleteListener = 96 new OnQueryCompleteListener() { 97 @Override 98 public void onQueryComplete(int token, Object cookie, CallerInfo ci) { 99 Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onQueryComplete"); 100 // If there are no other directory queries, make sure that the listener is 101 // notified of this result. see a bug 102 if ((ci != null && ci.contactExists) 103 || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) { 104 if (listener != null && ci != null) { 105 listener.onQueryComplete(token, cookie, ci); 106 } 107 } 108 } 109 110 @Override 111 public void onDataLoaded(int token, Object cookie, CallerInfo ci) { 112 Log.d(LOG_TAG, "contactsProviderQueryCompleteListener onDataLoaded"); 113 listener.onDataLoaded(token, cookie, ci); 114 } 115 }; 116 startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, cookie); 117 } 118 119 // Private methods startDefaultDirectoryQuery( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)120 private static void startDefaultDirectoryQuery( 121 int token, 122 Context context, 123 CallerInfo info, 124 OnQueryCompleteListener listener, 125 Object cookie) { 126 // Construct the URI object and query params, and start the query. 127 Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber); 128 startQueryInternal(token, context, info, listener, cookie, uri); 129 } 130 131 /** 132 * Factory method to start the query based on a CallerInfo object. 133 * 134 * <p>Note: if the number contains an "@" character we treat it as a SIP address, and look it up 135 * directly in the Data table rather than using the PhoneLookup table. TODO: But eventually we 136 * should expose two separate methods, one for numbers and one for SIP addresses, and then have 137 * PhoneUtils.startGetCallerInfo() decide which one to call based on the phone type of the 138 * incoming connection. 139 */ startQueryInternal( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie, Uri contactRef)140 private static void startQueryInternal( 141 int token, 142 Context context, 143 CallerInfo info, 144 OnQueryCompleteListener listener, 145 Object cookie, 146 Uri contactRef) { 147 if (DBG) { 148 Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef)); 149 } 150 151 if ((context == null) || (contactRef == null)) { 152 throw new QueryPoolException("Bad context or query uri."); 153 } 154 CallerInfoAsyncQueryHandler handler = new CallerInfoAsyncQueryHandler(context, contactRef); 155 156 //create cookieWrapper, start query 157 CookieWrapper cw = new CookieWrapper(); 158 cw.listener = listener; 159 cw.cookie = cookie; 160 cw.number = info.phoneNumber; 161 cw.countryIso = info.countryIso; 162 163 // check to see if these are recognized numbers, and use shortcuts if we can. 164 if (PhoneNumberHelper.isLocalEmergencyNumber(context, info.phoneNumber)) { 165 cw.event = EVENT_EMERGENCY_NUMBER; 166 } else if (info.isVoiceMailNumber()) { 167 cw.event = EVENT_VOICEMAIL_NUMBER; 168 } else { 169 cw.event = EVENT_NEW_QUERY; 170 } 171 172 String[] proejection = CallerInfo.getDefaultPhoneLookupProjection(); 173 handler.startQuery( 174 token, 175 cw, // cookie 176 contactRef, // uri 177 proejection, // projection 178 null, // selection 179 null, // selectionArgs 180 null); // orderBy 181 } 182 183 // Return value indicates if listener was notified. startOtherDirectoriesQuery( int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)184 private static boolean startOtherDirectoriesQuery( 185 int token, 186 Context context, 187 CallerInfo info, 188 OnQueryCompleteListener listener, 189 Object cookie) { 190 Trace.beginSection("CallerInfoAsyncQuery.startOtherDirectoriesQuery"); 191 long[] directoryIds = StrictModeUtils.bypass(() -> getDirectoryIds(context)); 192 int size = directoryIds.length; 193 if (size == 0) { 194 Trace.endSection(); 195 return false; 196 } 197 198 DirectoryQueryCompleteListenerFactory listenerFactory = 199 new DirectoryQueryCompleteListenerFactory(context, size, listener); 200 201 // The current implementation of multiple async query runs in single handler thread 202 // in AsyncQueryHandler. 203 // intermediateListener.onQueryComplete is also called from the same caller thread. 204 // TODO(a bug): use thread pool instead of single thread. 205 for (int i = 0; i < size; i++) { 206 long directoryId = directoryIds[i]; 207 Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId); 208 if (DBG) { 209 Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri); 210 } 211 OnQueryCompleteListener intermediateListener = listenerFactory.newListener(directoryId); 212 startQueryInternal(token, context, info, intermediateListener, cookie, uri); 213 } 214 Trace.endSection(); 215 return true; 216 } 217 getDirectoryIds(Context context)218 private static long[] getDirectoryIds(Context context) { 219 ArrayList<Long> results = new ArrayList<>(); 220 221 Uri uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise"); 222 223 ContentResolver cr = context.getContentResolver(); 224 Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null); 225 addDirectoryIdsFromCursor(cursor, results); 226 227 long[] result = new long[results.size()]; 228 for (int i = 0; i < results.size(); i++) { 229 result[i] = results.get(i); 230 } 231 return result; 232 } 233 addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results)234 private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results) { 235 if (cursor != null) { 236 int idIndex = cursor.getColumnIndex(Directory._ID); 237 while (cursor.moveToNext()) { 238 long id = cursor.getLong(idIndex); 239 if (Directory.isRemoteDirectoryId(id)) { 240 results.add(id); 241 } 242 } 243 cursor.close(); 244 } 245 } 246 sanitizeUriToString(Uri uri)247 private static String sanitizeUriToString(Uri uri) { 248 if (uri != null) { 249 String uriString = uri.toString(); 250 int indexOfLastSlash = uriString.lastIndexOf('/'); 251 if (indexOfLastSlash > 0) { 252 return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx"; 253 } else { 254 return uriString; 255 } 256 } else { 257 return ""; 258 } 259 } 260 261 /** Wrap the cookie from the WorkerArgs with additional information needed by our classes. */ 262 private static final class CookieWrapper { 263 264 public OnQueryCompleteListener listener; 265 public Object cookie; 266 public int event; 267 public String number; 268 public String countryIso; 269 } 270 /* Directory lookup related code - END */ 271 272 /** Simple exception used to communicate problems with the query pool. */ 273 private static class QueryPoolException extends SQLException { 274 QueryPoolException(String error)275 QueryPoolException(String error) { 276 super(error); 277 } 278 } 279 280 private static final class DirectoryQueryCompleteListenerFactory { 281 282 private final OnQueryCompleteListener listener; 283 private final Context context; 284 // Make sure listener to be called once and only once 285 private int count; 286 private boolean isListenerCalled; 287 DirectoryQueryCompleteListenerFactory( Context context, int size, OnQueryCompleteListener listener)288 DirectoryQueryCompleteListenerFactory( 289 Context context, int size, OnQueryCompleteListener listener) { 290 count = size; 291 this.listener = listener; 292 isListenerCalled = false; 293 this.context = context; 294 } 295 onDirectoryQueryComplete( int token, Object cookie, CallerInfo ci, long directoryId)296 private void onDirectoryQueryComplete( 297 int token, Object cookie, CallerInfo ci, long directoryId) { 298 boolean shouldCallListener = false; 299 synchronized (this) { 300 count = count - 1; 301 if (!isListenerCalled && (ci.contactExists || count == 0)) { 302 isListenerCalled = true; 303 shouldCallListener = true; 304 } 305 } 306 307 // Don't call callback in synchronized block because mListener.onQueryComplete may 308 // take long time to complete 309 if (shouldCallListener && listener != null) { 310 addCallerInfoIntoCache(ci, directoryId); 311 listener.onQueryComplete(token, cookie, ci); 312 } 313 } 314 addCallerInfoIntoCache(CallerInfo ci, long directoryId)315 private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) { 316 CachedNumberLookupService cachedNumberLookupService = 317 PhoneNumberCache.get(context).getCachedNumberLookupService(); 318 if (ci.contactExists && cachedNumberLookupService != null) { 319 // 1. Cache caller info 320 CachedContactInfo cachedContactInfo = 321 CallerInfoUtils.buildCachedContactInfo(cachedNumberLookupService, ci); 322 String directoryLabel = context.getString(R.string.directory_search_label); 323 cachedContactInfo.setDirectorySource(directoryLabel, directoryId); 324 cachedNumberLookupService.addContact(context, cachedContactInfo); 325 326 // 2. Cache photo 327 if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) { 328 try (InputStream in = 329 context.getContentResolver().openInputStream(ci.contactDisplayPhotoUri)) { 330 if (in != null) { 331 cachedNumberLookupService.addPhoto(context, ci.normalizedNumber, in); 332 } 333 } catch (IOException e) { 334 Log.e(LOG_TAG, "failed to fetch directory contact photo", e); 335 } 336 } 337 } 338 } 339 newListener(long directoryId)340 OnQueryCompleteListener newListener(long directoryId) { 341 return new DirectoryQueryCompleteListener(directoryId); 342 } 343 344 private class DirectoryQueryCompleteListener implements OnQueryCompleteListener { 345 346 private final long directoryId; 347 DirectoryQueryCompleteListener(long directoryId)348 DirectoryQueryCompleteListener(long directoryId) { 349 this.directoryId = directoryId; 350 } 351 352 @Override onDataLoaded(int token, Object cookie, CallerInfo ci)353 public void onDataLoaded(int token, Object cookie, CallerInfo ci) { 354 Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onDataLoaded"); 355 listener.onDataLoaded(token, cookie, ci); 356 } 357 358 @Override onQueryComplete(int token, Object cookie, CallerInfo ci)359 public void onQueryComplete(int token, Object cookie, CallerInfo ci) { 360 Log.d(LOG_TAG, "DirectoryQueryCompleteListener.onQueryComplete"); 361 onDirectoryQueryComplete(token, cookie, ci, directoryId); 362 } 363 } 364 } 365 366 /** Our own implementation of the AsyncQueryHandler. */ 367 private static class CallerInfoAsyncQueryHandler extends AsyncQueryHandler { 368 369 /** 370 * The information relevant to each CallerInfo query. Each query may have multiple listeners, so 371 * each AsyncCursorInfo is associated with 2 or more CookieWrapper objects in the queue (one 372 * with a new query event, and one with a end event, with 0 or more additional listeners in 373 * between). 374 */ 375 private Context queryContext; 376 377 private Uri queryUri; 378 private CallerInfo callerInfo; 379 380 /** Asynchronous query handler class for the contact / callerinfo object. */ CallerInfoAsyncQueryHandler(Context context, Uri contactRef)381 private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) { 382 super(context.getContentResolver()); 383 this.queryContext = context; 384 this.queryUri = contactRef; 385 } 386 387 @Override startQuery( int token, Object cookie, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy)388 public void startQuery( 389 int token, 390 Object cookie, 391 Uri uri, 392 String[] projection, 393 String selection, 394 String[] selectionArgs, 395 String orderBy) { 396 if (DBG) { 397 // Show stack trace with the arguments. 398 Log.d( 399 LOG_TAG, 400 "InCall: startQuery: url=" 401 + uri 402 + " projection=[" 403 + Arrays.toString(projection) 404 + "]" 405 + " selection=" 406 + selection 407 + " " 408 + " args=[" 409 + Arrays.toString(selectionArgs) 410 + "]", 411 new RuntimeException("STACKTRACE")); 412 } 413 super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy); 414 } 415 416 @Override createHandler(Looper looper)417 protected Handler createHandler(Looper looper) { 418 return new CallerInfoWorkerHandler(looper); 419 } 420 421 /** 422 * Overrides onQueryComplete from AsyncQueryHandler. 423 * 424 * <p>This method takes into account the state of this class; we construct the CallerInfo object 425 * only once for each set of listeners. When the query thread has done its work and calls this 426 * method, we inform the remaining listeners in the queue, until we're out of listeners. Once we 427 * get the message indicating that we should expect no new listeners for this CallerInfo object, 428 * we release the AsyncCursorInfo back into the pool. 429 */ 430 @Override onQueryComplete(int token, Object cookie, Cursor cursor)431 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 432 Log.d(this, "##### onQueryComplete() ##### query complete for token: " + token); 433 434 CookieWrapper cw = (CookieWrapper) cookie; 435 436 if (cw.listener != null) { 437 Log.d( 438 this, 439 "notifying listener: " 440 + cw.listener.getClass().toString() 441 + " for token: " 442 + token 443 + callerInfo); 444 cw.listener.onQueryComplete(token, cw.cookie, callerInfo); 445 } 446 queryContext = null; 447 queryUri = null; 448 callerInfo = null; 449 } 450 updateData(int token, Object cookie, Cursor cursor)451 void updateData(int token, Object cookie, Cursor cursor) { 452 try { 453 Log.d(this, "##### updateData() ##### for token: " + token); 454 455 //get the cookie and notify the listener. 456 CookieWrapper cw = (CookieWrapper) cookie; 457 if (cw == null) { 458 // Normally, this should never be the case for calls originating 459 // from within this code. 460 // However, if there is any code that calls this method, we should 461 // check the parameters to make sure they're viable. 462 Log.d(this, "Cookie is null, ignoring onQueryComplete() request."); 463 return; 464 } 465 466 // check the token and if needed, create the callerinfo object. 467 if (callerInfo == null) { 468 if ((queryContext == null) || (queryUri == null)) { 469 throw new QueryPoolException( 470 "Bad context or query uri, or CallerInfoAsyncQuery already released."); 471 } 472 473 // adjust the callerInfo data as needed, and only if it was set from the 474 // initial query request. 475 // Change the callerInfo number ONLY if it is an emergency number or the 476 // voicemail number, and adjust other data (including photoResource) 477 // accordingly. 478 if (cw.event == EVENT_EMERGENCY_NUMBER) { 479 // Note we're setting the phone number here (refer to javadoc 480 // comments at the top of CallerInfo class). 481 callerInfo = new CallerInfo().markAsEmergency(queryContext); 482 } else if (cw.event == EVENT_VOICEMAIL_NUMBER) { 483 callerInfo = new CallerInfo().markAsVoiceMail(queryContext); 484 } else { 485 callerInfo = CallerInfo.getCallerInfo(queryContext, queryUri, cursor); 486 Log.d(this, "==> Got mCallerInfo: " + callerInfo); 487 488 CallerInfo newCallerInfo = 489 CallerInfo.doSecondaryLookupIfNecessary(queryContext, cw.number, callerInfo); 490 if (newCallerInfo != callerInfo) { 491 callerInfo = newCallerInfo; 492 Log.d(this, "#####async contact look up with numeric username" + callerInfo); 493 } 494 callerInfo.countryIso = cw.countryIso; 495 496 // Final step: look up the geocoded description. 497 if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) { 498 // Note we do this only if we *don't* have a valid name (i.e. if 499 // no contacts matched the phone number of the incoming call), 500 // since that's the only case where the incoming-call UI cares 501 // about this field. 502 // 503 // (TODO: But if we ever want the UI to show the geoDescription 504 // even when we *do* match a contact, we'll need to either call 505 // updateGeoDescription() unconditionally here, or possibly add a 506 // new parameter to CallerInfoAsyncQuery.startQuery() to force 507 // the geoDescription field to be populated.) 508 509 if (TextUtils.isEmpty(callerInfo.name)) { 510 // Actually when no contacts match the incoming phone number, 511 // the CallerInfo object is totally blank here (i.e. no name 512 // *or* phoneNumber). So we need to pass in cw.number as 513 // a fallback number. 514 callerInfo.updateGeoDescription(queryContext, cw.number); 515 } 516 } 517 518 // Use the number entered by the user for display. 519 if (!TextUtils.isEmpty(cw.number)) { 520 callerInfo.phoneNumber = cw.number; 521 } 522 } 523 524 Log.d(this, "constructing CallerInfo object for token: " + token); 525 526 if (cw.listener != null) { 527 cw.listener.onDataLoaded(token, cw.cookie, callerInfo); 528 } 529 } 530 531 } finally { 532 // The cursor may have been closed in CallerInfo.getCallerInfo() 533 if (cursor != null && !cursor.isClosed()) { 534 cursor.close(); 535 } 536 } 537 } 538 539 /** 540 * Our own query worker thread. 541 * 542 * <p>This thread handles the messages enqueued in the looper. The normal sequence of events is 543 * that a new query shows up in the looper queue, followed by 0 or more add listener requests, 544 * and then an end request. Of course, these requests can be interlaced with requests from other 545 * tokens, but is irrelevant to this handler since the handler has no state. 546 * 547 * <p>Note that we depend on the queue to keep things in order; in other words, the looper queue 548 * must be FIFO with respect to input from the synchronous startQuery calls and output to this 549 * handleMessage call. 550 * 551 * <p>This use of the queue is required because CallerInfo objects may be accessed multiple 552 * times before the query is complete. All accesses (listeners) must be queued up and informed 553 * in order when the query is complete. 554 */ 555 class CallerInfoWorkerHandler extends WorkerHandler { 556 CallerInfoWorkerHandler(Looper looper)557 CallerInfoWorkerHandler(Looper looper) { 558 super(looper); 559 } 560 561 @Override handleMessage(Message msg)562 public void handleMessage(Message msg) { 563 WorkerArgs args = (WorkerArgs) msg.obj; 564 CookieWrapper cw = (CookieWrapper) args.cookie; 565 566 if (cw == null) { 567 // Normally, this should never be the case for calls originating 568 // from within this code. 569 // However, if there is any code that this Handler calls (such as in 570 // super.handleMessage) that DOES place unexpected messages on the 571 // queue, then we need pass these messages on. 572 Log.d( 573 this, 574 "Unexpected command (CookieWrapper is null): " 575 + msg.what 576 + " ignored by CallerInfoWorkerHandler, passing onto parent."); 577 578 super.handleMessage(msg); 579 } else { 580 Log.d( 581 this, 582 "Processing event: " 583 + cw.event 584 + " token (arg1): " 585 + msg.arg1 586 + " command: " 587 + msg.what 588 + " query URI: " 589 + sanitizeUriToString(args.uri)); 590 591 switch (cw.event) { 592 case EVENT_NEW_QUERY: 593 final ContentResolver resolver = queryContext.getContentResolver(); 594 595 // This should never happen. 596 if (resolver == null) { 597 Log.e(this, "Content Resolver is null!"); 598 return; 599 } 600 // start the sql command. 601 Cursor cursor; 602 try { 603 cursor = 604 resolver.query( 605 args.uri, 606 args.projection, 607 args.selection, 608 args.selectionArgs, 609 args.orderBy); 610 // Calling getCount() causes the cursor window to be filled, 611 // which will make the first access on the main thread a lot faster. 612 if (cursor != null) { 613 cursor.getCount(); 614 } 615 } catch (Exception e) { 616 Log.e(this, "Exception thrown during handling EVENT_ARG_QUERY", e); 617 cursor = null; 618 } 619 620 args.result = cursor; 621 updateData(msg.arg1, cw, cursor); 622 break; 623 624 // shortcuts to avoid query for recognized numbers. 625 case EVENT_EMERGENCY_NUMBER: 626 case EVENT_VOICEMAIL_NUMBER: 627 case EVENT_ADD_LISTENER: 628 updateData(msg.arg1, cw, (Cursor) args.result); 629 break; 630 default: // fall out 631 } 632 Message reply = args.handler.obtainMessage(msg.what); 633 reply.obj = args; 634 reply.arg1 = msg.arg1; 635 636 reply.sendToTarget(); 637 } 638 } 639 } 640 } 641 } 642