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