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