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