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 android.telecom; 18 19 import android.app.ActivityManager; 20 import android.compat.annotation.UnsupportedAppUsage; 21 import android.content.AsyncQueryHandler; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.pm.PackageManager.NameNotFoundException; 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.os.SystemClock; 32 import android.os.UserHandle; 33 import android.os.UserManager; 34 import android.provider.ContactsContract.PhoneLookup; 35 import android.telephony.PhoneNumberUtils; 36 import android.telephony.SubscriptionManager; 37 import android.text.TextUtils; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * Helper class to make it easier to run asynchronous caller-id lookup queries. 44 * @see CallerInfo 45 * 46 * {@hide} 47 */ 48 public class CallerInfoAsyncQuery { 49 private static final boolean DBG = false; 50 private static final String LOG_TAG = "CallerInfoAsyncQuery"; 51 52 private static final int EVENT_NEW_QUERY = 1; 53 private static final int EVENT_ADD_LISTENER = 2; 54 private static final int EVENT_END_OF_QUEUE = 3; 55 private static final int EVENT_EMERGENCY_NUMBER = 4; 56 private static final int EVENT_VOICEMAIL_NUMBER = 5; 57 private static final int EVENT_GET_GEO_DESCRIPTION = 6; 58 59 private CallerInfoAsyncQueryHandler mHandler; 60 61 // If the CallerInfo query finds no contacts, should we use the 62 // PhoneNumberOfflineGeocoder to look up a "geo description"? 63 // (TODO: This could become a flag in config.xml if it ever needs to be 64 // configured on a per-product basis.) 65 private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true; 66 67 /** 68 * Interface for a CallerInfoAsyncQueryHandler result return. 69 */ 70 public interface OnQueryCompleteListener { 71 /** 72 * Called when the query is complete. 73 */ onQueryComplete(int token, Object cookie, CallerInfo ci)74 public void onQueryComplete(int token, Object cookie, CallerInfo ci); 75 } 76 77 78 /** 79 * Wrap the cookie from the WorkerArgs with additional information needed by our 80 * classes. 81 */ 82 private static final class CookieWrapper { 83 @UnsupportedAppUsage CookieWrapper()84 private CookieWrapper() { 85 } 86 87 public OnQueryCompleteListener listener; 88 public Object cookie; 89 public int event; 90 public String number; 91 public String geoDescription; 92 93 public int subId; 94 } 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 * @return {@link ContentResolver} for the "current" user. 108 */ getCurrentProfileContentResolver(Context context)109 static ContentResolver getCurrentProfileContentResolver(Context context) { 110 111 if (DBG) Log.d(LOG_TAG, "Trying to get current content resolver..."); 112 113 final int currentUser = ActivityManager.getCurrentUser(); 114 final int myUser = UserManager.get(context).getUserHandle(); 115 116 if (DBG) Log.d(LOG_TAG, "myUser=" + myUser + "currentUser=" + currentUser); 117 118 if (myUser != currentUser) { 119 final Context otherContext; 120 try { 121 otherContext = context.createPackageContextAsUser(context.getPackageName(), 122 /* flags =*/ 0, UserHandle.of(currentUser)); 123 return otherContext.getContentResolver(); 124 } catch (NameNotFoundException e) { 125 Log.e(LOG_TAG, e, "Can't find self package"); 126 // Fall back to the primary user. 127 } 128 } 129 return context.getContentResolver(); 130 } 131 132 /** 133 * Our own implementation of the AsyncQueryHandler. 134 */ 135 private class CallerInfoAsyncQueryHandler extends AsyncQueryHandler { 136 137 /* 138 * The information relevant to each CallerInfo query. Each query may have multiple 139 * listeners, so each AsyncCursorInfo is associated with 2 or more CookieWrapper 140 * objects in the queue (one with a new query event, and one with a end event, with 141 * 0 or more additional listeners in between). 142 */ 143 144 /** 145 * Context passed by the caller. 146 * 147 * NOTE: The actual context we use for query may *not* be this context; since we query 148 * against the "current" contacts provider. In the constructor we pass the "current" 149 * context resolver (obtained via {@link #getCurrentProfileContentResolver) and pass it 150 * to the super class. 151 */ 152 private Context mContext; 153 private Uri mQueryUri; 154 private CallerInfo mCallerInfo; 155 private List<Runnable> mPendingListenerCallbacks = new ArrayList<>(); 156 157 /** 158 * Our own query worker thread. 159 * 160 * This thread handles the messages enqueued in the looper. The normal sequence 161 * of events is that a new query shows up in the looper queue, followed by 0 or 162 * more add listener requests, and then an end request. Of course, these requests 163 * can be interlaced with requests from other tokens, but is irrelevant to this 164 * handler since the handler has no state. 165 * 166 * Note that we depend on the queue to keep things in order; in other words, the 167 * looper queue must be FIFO with respect to input from the synchronous startQuery 168 * calls and output to this handleMessage call. 169 * 170 * This use of the queue is required because CallerInfo objects may be accessed 171 * multiple times before the query is complete. All accesses (listeners) must be 172 * queued up and informed in order when the query is complete. 173 */ 174 protected class CallerInfoWorkerHandler extends WorkerHandler { CallerInfoWorkerHandler(Looper looper)175 public CallerInfoWorkerHandler(Looper looper) { 176 super(looper); 177 } 178 179 @Override handleMessage(Message msg)180 public void handleMessage(Message msg) { 181 WorkerArgs args = (WorkerArgs) msg.obj; 182 CookieWrapper cw = (CookieWrapper) args.cookie; 183 184 if (cw == null) { 185 // Normally, this should never be the case for calls originating 186 // from within this code. 187 // However, if there is any code that this Handler calls (such as in 188 // super.handleMessage) that DOES place unexpected messages on the 189 // queue, then we need pass these messages on. 190 Log.i(LOG_TAG, "Unexpected command (CookieWrapper is null): " + msg.what + 191 " ignored by CallerInfoWorkerHandler, passing onto parent."); 192 193 super.handleMessage(msg); 194 } else { 195 196 Log.d(LOG_TAG, "Processing event: " + cw.event + " token (arg1): " + msg.arg1 + 197 " command: " + msg.what + " query URI: " + sanitizeUriToString(args.uri)); 198 199 switch (cw.event) { 200 case EVENT_NEW_QUERY: 201 //start the sql command. 202 super.handleMessage(msg); 203 break; 204 205 // shortcuts to avoid query for recognized numbers. 206 case EVENT_EMERGENCY_NUMBER: 207 case EVENT_VOICEMAIL_NUMBER: 208 209 case EVENT_ADD_LISTENER: 210 case EVENT_END_OF_QUEUE: 211 // query was already completed, so just send the reply. 212 // passing the original token value back to the caller 213 // on top of the event values in arg1. 214 Message reply = args.handler.obtainMessage(msg.what); 215 reply.obj = args; 216 reply.arg1 = msg.arg1; 217 218 reply.sendToTarget(); 219 220 break; 221 case EVENT_GET_GEO_DESCRIPTION: 222 handleGeoDescription(msg); 223 break; 224 default: 225 } 226 } 227 } 228 handleGeoDescription(Message msg)229 private void handleGeoDescription(Message msg) { 230 WorkerArgs args = (WorkerArgs) msg.obj; 231 CookieWrapper cw = (CookieWrapper) args.cookie; 232 if (!TextUtils.isEmpty(cw.number) && cw.cookie != null && mContext != null) { 233 final long startTimeMillis = SystemClock.elapsedRealtime(); 234 cw.geoDescription = CallerInfo.getGeoDescription(mContext, cw.number); 235 final long duration = SystemClock.elapsedRealtime() - startTimeMillis; 236 if (duration > 500) { 237 if (DBG) Log.d(LOG_TAG, "[handleGeoDescription]" + 238 "Spends long time to retrieve Geo description: " + duration); 239 } 240 } 241 Message reply = args.handler.obtainMessage(msg.what); 242 reply.obj = args; 243 reply.arg1 = msg.arg1; 244 reply.sendToTarget(); 245 } 246 } 247 248 249 /** 250 * Asynchronous query handler class for the contact / callerinfo object. 251 */ CallerInfoAsyncQueryHandler(Context context)252 private CallerInfoAsyncQueryHandler(Context context) { 253 super(getCurrentProfileContentResolver(context)); 254 mContext = context; 255 } 256 257 @Override createHandler(Looper looper)258 protected Handler createHandler(Looper looper) { 259 return new CallerInfoWorkerHandler(looper); 260 } 261 262 /** 263 * Overrides onQueryComplete from AsyncQueryHandler. 264 * 265 * This method takes into account the state of this class; we construct the CallerInfo 266 * object only once for each set of listeners. When the query thread has done its work 267 * and calls this method, we inform the remaining listeners in the queue, until we're 268 * out of listeners. Once we get the message indicating that we should expect no new 269 * listeners for this CallerInfo object, we release the AsyncCursorInfo back into the 270 * pool. 271 */ 272 @Override onQueryComplete(int token, Object cookie, Cursor cursor)273 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 274 Log.d(LOG_TAG, "##### onQueryComplete() ##### query complete for token: " + token); 275 276 //get the cookie and notify the listener. 277 CookieWrapper cw = (CookieWrapper) cookie; 278 if (cw == null) { 279 // Normally, this should never be the case for calls originating 280 // from within this code. 281 // However, if there is any code that calls this method, we should 282 // check the parameters to make sure they're viable. 283 Log.i(LOG_TAG, "Cookie is null, ignoring onQueryComplete() request."); 284 if (cursor != null) { 285 cursor.close(); 286 } 287 return; 288 } 289 290 if (cw.event == EVENT_END_OF_QUEUE) { 291 for (Runnable r : mPendingListenerCallbacks) { 292 r.run(); 293 } 294 mPendingListenerCallbacks.clear(); 295 296 release(); 297 if (cursor != null) { 298 cursor.close(); 299 } 300 return; 301 } 302 303 // If the cw.event == EVENT_GET_GEO_DESCRIPTION, means it would not be the 1st 304 // time entering the onQueryComplete(), mCallerInfo should not be null. 305 if (cw.event == EVENT_GET_GEO_DESCRIPTION) { 306 if (mCallerInfo != null) { 307 mCallerInfo.geoDescription = cw.geoDescription; 308 } 309 // notify that we can clean up the queue after this. 310 CookieWrapper endMarker = new CookieWrapper(); 311 endMarker.event = EVENT_END_OF_QUEUE; 312 startQuery(token, endMarker, null, null, null, null, null); 313 } 314 315 // check the token and if needed, create the callerinfo object. 316 if (mCallerInfo == null) { 317 if ((mContext == null) || (mQueryUri == null)) { 318 throw new QueryPoolException 319 ("Bad context or query uri, or CallerInfoAsyncQuery already released."); 320 } 321 322 // adjust the callerInfo data as needed, and only if it was set from the 323 // initial query request. 324 // Change the callerInfo number ONLY if it is an emergency number or the 325 // voicemail number, and adjust other data (including photoResource) 326 // accordingly. 327 if (cw.event == EVENT_EMERGENCY_NUMBER) { 328 // Note we're setting the phone number here (refer to javadoc 329 // comments at the top of CallerInfo class). 330 mCallerInfo = new CallerInfo().markAsEmergency(mContext); 331 } else if (cw.event == EVENT_VOICEMAIL_NUMBER) { 332 mCallerInfo = new CallerInfo().markAsVoiceMail(mContext, cw.subId); 333 } else { 334 mCallerInfo = CallerInfo.getCallerInfo(mContext, mQueryUri, cursor); 335 if (DBG) Log.d(LOG_TAG, "==> Got mCallerInfo: " + mCallerInfo); 336 337 CallerInfo newCallerInfo = CallerInfo.doSecondaryLookupIfNecessary( 338 mContext, cw.number, mCallerInfo); 339 if (newCallerInfo != mCallerInfo) { 340 mCallerInfo = newCallerInfo; 341 if (DBG) Log.d(LOG_TAG, "#####async contact look up with numeric username" 342 + mCallerInfo); 343 } 344 345 // Use the number entered by the user for display. 346 if (!TextUtils.isEmpty(cw.number)) { 347 mCallerInfo.setPhoneNumber(PhoneNumberUtils.formatNumber(cw.number, 348 mCallerInfo.normalizedNumber, 349 CallerInfo.getCurrentCountryIso(mContext))); 350 } 351 352 // This condition refer to the google default code for geo. 353 // If the number exists in Contacts, the CallCard would never show 354 // the geo description, so it would be unnecessary to query it. 355 if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) { 356 if (TextUtils.isEmpty(mCallerInfo.getName())) { 357 if (DBG) Log.d(LOG_TAG, "start querying geo description"); 358 cw.event = EVENT_GET_GEO_DESCRIPTION; 359 startQuery(token, cw, null, null, null, null, null); 360 return; 361 } 362 } 363 } 364 365 if (DBG) Log.d(LOG_TAG, "constructing CallerInfo object for token: " + token); 366 367 //notify that we can clean up the queue after this. 368 CookieWrapper endMarker = new CookieWrapper(); 369 endMarker.event = EVENT_END_OF_QUEUE; 370 startQuery(token, endMarker, null, null, null, null, null); 371 } 372 373 //notify the listener that the query is complete. 374 if (cw.listener != null) { 375 mPendingListenerCallbacks.add(new Runnable() { 376 @Override 377 public void run() { 378 if (DBG) Log.d(LOG_TAG, "notifying listener: " 379 + cw.listener.getClass().toString() + " for token: " + token 380 + mCallerInfo); 381 cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo); 382 } 383 }); 384 } else { 385 Log.w(LOG_TAG, "There is no listener to notify for this query."); 386 } 387 388 if (cursor != null) { 389 cursor.close(); 390 } 391 } 392 } 393 394 /** 395 * Private constructor for factory methods. 396 */ CallerInfoAsyncQuery()397 private CallerInfoAsyncQuery() { 398 } 399 400 401 /** 402 * Factory method to start query with a Uri query spec 403 */ startQuery(int token, Context context, Uri contactRef, OnQueryCompleteListener listener, Object cookie)404 public static CallerInfoAsyncQuery startQuery(int token, Context context, Uri contactRef, 405 OnQueryCompleteListener listener, Object cookie) { 406 407 CallerInfoAsyncQuery c = new CallerInfoAsyncQuery(); 408 c.allocate(context, contactRef); 409 410 if (DBG) Log.d(LOG_TAG, "starting query for URI: " + contactRef + " handler: " + c.toString()); 411 412 //create cookieWrapper, start query 413 CookieWrapper cw = new CookieWrapper(); 414 cw.listener = listener; 415 cw.cookie = cookie; 416 cw.event = EVENT_NEW_QUERY; 417 418 c.mHandler.startQuery(token, cw, contactRef, null, null, null, null); 419 420 return c; 421 } 422 423 /** 424 * Factory method to start the query based on a number. 425 * 426 * Note: if the number contains an "@" character we treat it 427 * as a SIP address, and look it up directly in the Data table 428 * rather than using the PhoneLookup table. 429 * TODO: But eventually we should expose two separate methods, one for 430 * numbers and one for SIP addresses, and then have 431 * PhoneUtils.startGetCallerInfo() decide which one to call based on 432 * the phone type of the incoming connection. 433 */ startQuery(int token, Context context, String number, OnQueryCompleteListener listener, Object cookie)434 public static CallerInfoAsyncQuery startQuery(int token, Context context, String number, 435 OnQueryCompleteListener listener, Object cookie) { 436 437 int subId = SubscriptionManager.getDefaultSubscriptionId(); 438 return startQuery(token, context, number, listener, cookie, subId); 439 } 440 441 /** 442 * Factory method to start the query based on a number with specific subscription. 443 * 444 * Note: if the number contains an "@" character we treat it 445 * as a SIP address, and look it up directly in the Data table 446 * rather than using the PhoneLookup table. 447 * TODO: But eventually we should expose two separate methods, one for 448 * numbers and one for SIP addresses, and then have 449 * PhoneUtils.startGetCallerInfo() decide which one to call based on 450 * the phone type of the incoming connection. 451 */ startQuery(int token, Context context, String number, OnQueryCompleteListener listener, Object cookie, int subId)452 public static CallerInfoAsyncQuery startQuery(int token, Context context, String number, 453 OnQueryCompleteListener listener, Object cookie, int subId) { 454 455 if (DBG) { 456 Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startQuery()... #####"); 457 Log.d(LOG_TAG, "- number: " + /*number*/ "xxxxxxx"); 458 Log.d(LOG_TAG, "- cookie: " + cookie); 459 } 460 461 // Construct the URI object and query params, and start the query. 462 463 final Uri contactRef = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI.buildUpon() 464 .appendPath(number) 465 .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, 466 String.valueOf(PhoneNumberUtils.isUriNumber(number))) 467 .build(); 468 469 if (DBG) { 470 Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef)); 471 } 472 473 CallerInfoAsyncQuery c = new CallerInfoAsyncQuery(); 474 c.allocate(context, contactRef); 475 476 //create cookieWrapper, start query 477 CookieWrapper cw = new CookieWrapper(); 478 cw.listener = listener; 479 cw.cookie = cookie; 480 cw.number = number; 481 cw.subId = subId; 482 483 // check to see if these are recognized numbers, and use shortcuts if we can. 484 if (PhoneNumberUtils.isLocalEmergencyNumber(context, number)) { 485 cw.event = EVENT_EMERGENCY_NUMBER; 486 } else if (PhoneNumberUtils.isVoiceMailNumber(context, subId, number)) { 487 cw.event = EVENT_VOICEMAIL_NUMBER; 488 } else { 489 cw.event = EVENT_NEW_QUERY; 490 } 491 492 c.mHandler.startQuery(token, 493 cw, // cookie 494 contactRef, // uri 495 null, // projection 496 null, // selection 497 null, // selectionArgs 498 null); // orderBy 499 return c; 500 } 501 502 /** 503 * Method to add listeners to a currently running query 504 */ addQueryListener(int token, OnQueryCompleteListener listener, Object cookie)505 public void addQueryListener(int token, OnQueryCompleteListener listener, Object cookie) { 506 507 if (DBG) Log.d(LOG_TAG, "adding listener to query: " 508 + sanitizeUriToString(mHandler.mQueryUri) + " handler: " + mHandler.toString()); 509 510 //create cookieWrapper, add query request to end of queue. 511 CookieWrapper cw = new CookieWrapper(); 512 cw.listener = listener; 513 cw.cookie = cookie; 514 cw.event = EVENT_ADD_LISTENER; 515 516 mHandler.startQuery(token, cw, null, null, null, null, null); 517 } 518 519 /** 520 * Method to create a new CallerInfoAsyncQueryHandler object, ensuring correct 521 * state of context and uri. 522 */ allocate(Context context, Uri contactRef)523 private void allocate(Context context, Uri contactRef) { 524 if ((context == null) || (contactRef == null)){ 525 throw new QueryPoolException("Bad context or query uri."); 526 } 527 mHandler = new CallerInfoAsyncQueryHandler(context); 528 mHandler.mQueryUri = contactRef; 529 } 530 531 /** 532 * Releases the relevant data. 533 */ 534 @UnsupportedAppUsage release()535 private void release() { 536 mHandler.mContext = null; 537 mHandler.mQueryUri = null; 538 mHandler.mCallerInfo = null; 539 mHandler = null; 540 } 541 sanitizeUriToString(Uri uri)542 private static String sanitizeUriToString(Uri uri) { 543 if (uri != null) { 544 String uriString = uri.toString(); 545 int indexOfLastSlash = uriString.lastIndexOf('/'); 546 if (indexOfLastSlash > 0) { 547 return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx"; 548 } else { 549 return uriString; 550 } 551 } else { 552 return ""; 553 } 554 } 555 } 556