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