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