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