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