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 */ onQueryComplete(int token, Object cookie, CallerInfo ci)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 { QueryPoolException(String error)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 startQuery(int token, Object cookie, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy)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 { CallerInfoWorkerHandler(Looper looper)153 public CallerInfoWorkerHandler(Looper looper) { 154 super(looper); 155 } 156 157 @Override handleMessage(Message msg)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 */ CallerInfoAsyncQueryHandler(Context context)209 private CallerInfoAsyncQueryHandler(Context context) { 210 super(context.getContentResolver()); 211 } 212 213 @Override createHandler(Looper looper)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 onQueryComplete(int token, Object cookie, Cursor cursor)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 */ CallerInfoAsyncQuery()336 private CallerInfoAsyncQuery() { 337 } 338 startQuery(final int token, final Context context, final CallerInfo info, final OnQueryCompleteListener listener, final Object cookie)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 startDefaultDirectoryQuery(int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)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 */ startQueryInternal(int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie, Uri contactRef)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. startOtherDirectoriesQuery(int token, Context context, CallerInfo info, OnQueryCompleteListener listener, Object cookie)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 getDirectoryIds(Context context)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 addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results)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 DirectoryQueryCompleteListener(long directoryId)496 DirectoryQueryCompleteListener(long directoryId) { 497 mDirectoryId = directoryId; 498 } 499 500 @Override onQueryComplete(int token, Object cookie, CallerInfo ci)501 public void onQueryComplete(int token, Object cookie, CallerInfo ci) { 502 onDirectoryQueryComplete(token, cookie, ci, mDirectoryId); 503 } 504 } 505 DirectoryQueryCompleteListenerFactory(Context context, int size, OnQueryCompleteListener listener)506 DirectoryQueryCompleteListenerFactory(Context context, int size, 507 OnQueryCompleteListener listener) { 508 mCount = size; 509 mListener = listener; 510 mIsListenerCalled = false; 511 mContext = context; 512 } 513 onDirectoryQueryComplete(int token, Object cookie, CallerInfo ci, long directoryId)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 addCallerInfoIntoCache(CallerInfo ci, long directoryId)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 newListener(long directoryId)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 */ allocate(Context context, Uri contactRef)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 */ release()579 private void release() { 580 mHandler.mQueryContext = null; 581 mHandler.mQueryUri = null; 582 mHandler.mCallerInfo = null; 583 mHandler = null; 584 } 585 sanitizeUriToString(Uri uri)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