CallerInfoAsyncQuery.java revision 9066cfe9886ac131c34d59ed0e2d287b0e3c0087
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 17package com.android.internal.telephony; 18 19import android.content.AsyncQueryHandler; 20import android.content.Context; 21import android.database.Cursor; 22import android.database.SQLException; 23import android.net.Uri; 24import android.os.Handler; 25import android.os.Looper; 26import android.os.Message; 27import android.provider.Contacts; 28import android.telephony.PhoneNumberUtils; 29import android.telephony.TelephonyManager; 30import android.text.TextUtils; 31import android.util.Log; 32 33/** 34 * ASYNCHRONOUS QUERY API 35 */ 36 37public class CallerInfoAsyncQuery { 38 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 // Don't keep checking VM if it's going to throw an exception for this proc. 51 private static boolean sSkipVmCheck = false; 52 53 /** 54 * Interface for a CallerInfoAsyncQueryHandler result return. 55 */ 56 public interface OnQueryCompleteListener { 57 /** 58 * Called when the query is complete. 59 */ 60 public void onQueryComplete(int token, Object cookie, CallerInfo ci); 61 } 62 63 64 /** 65 * Wrap the cookie from the WorkerArgs with additional information needed by our 66 * classes. 67 */ 68 private static final class CookieWrapper { 69 public OnQueryCompleteListener listener; 70 public Object cookie; 71 public int event; 72 public String number; 73 } 74 75 76 /** 77 * Simple exception used to communicate problems with the query pool. 78 */ 79 public static class QueryPoolException extends SQLException { 80 public QueryPoolException(String error) { 81 super(error); 82 } 83 } 84 85 /** 86 * Our own implementation of the AsyncQueryHandler. 87 */ 88 private class CallerInfoAsyncQueryHandler extends AsyncQueryHandler { 89 90 /** 91 * The information relevant to each CallerInfo query. Each query may have multiple 92 * listeners, so each AsyncCursorInfo is associated with 2 or more CookieWrapper 93 * objects in the queue (one with a new query event, and one with a end event, with 94 * 0 or more additional listeners in between). 95 */ 96 private Context mQueryContext; 97 private Uri mQueryUri; 98 private CallerInfo mCallerInfo; 99 100 /** 101 * Our own query worker thread. 102 * 103 * This thread handles the messages enqueued in the looper. The normal sequence 104 * of events is that a new query shows up in the looper queue, followed by 0 or 105 * more add listener requests, and then an end request. Of course, these requests 106 * can be interlaced with requests from other tokens, but is irrelevant to this 107 * handler since the handler has no state. 108 * 109 * Note that we depend on the queue to keep things in order; in other words, the 110 * looper queue must be FIFO with respect to input from the synchronous startQuery 111 * calls and output to this handleMessage call. 112 * 113 * This use of the queue is required because CallerInfo objects may be accessed 114 * multiple times before the query is complete. All accesses (listeners) must be 115 * queued up and informed in order when the query is complete. 116 */ 117 protected class CallerInfoWorkerHandler extends WorkerHandler { 118 public CallerInfoWorkerHandler(Looper looper) { 119 super(looper); 120 } 121 122 @Override 123 public void handleMessage(Message msg) { 124 WorkerArgs args = (WorkerArgs) msg.obj; 125 CookieWrapper cw = (CookieWrapper) args.cookie; 126 127 if (cw == null) { 128 // Normally, this should never be the case for calls originating 129 // from within this code. 130 // However, if there is any code that this Handler calls (such as in 131 // super.handleMessage) that DOES place unexpected messages on the 132 // queue, then we need pass these messages on. 133 if (DBG) log("Unexpected command (CookieWrapper is null): " + msg.what + 134 " ignored by CallerInfoWorkerHandler, passing onto parent."); 135 136 super.handleMessage(msg); 137 } else { 138 139 if (DBG) log("Processing event: " + cw.event + " token (arg1): " + msg.arg1 + 140 " command: " + msg.what + " query URI: " + args.uri); 141 142 switch (cw.event) { 143 case EVENT_NEW_QUERY: 144 //start the sql command. 145 super.handleMessage(msg); 146 break; 147 148 // shortcuts to avoid query for recognized numbers. 149 case EVENT_EMERGENCY_NUMBER: 150 case EVENT_VOICEMAIL_NUMBER: 151 152 case EVENT_ADD_LISTENER: 153 case EVENT_END_OF_QUEUE: 154 // query was already completed, so just send the reply. 155 // passing the original token value back to the caller 156 // on top of the event values in arg1. 157 Message reply = args.handler.obtainMessage(msg.what); 158 reply.obj = args; 159 reply.arg1 = msg.arg1; 160 161 reply.sendToTarget(); 162 163 break; 164 default: 165 } 166 } 167 } 168 } 169 170 171 /** 172 * Asynchronous query handler class for the contact / callerinfo object. 173 */ 174 private CallerInfoAsyncQueryHandler(Context context) { 175 super(context.getContentResolver()); 176 } 177 178 @Override 179 protected Handler createHandler(Looper looper) { 180 return new CallerInfoWorkerHandler(looper); 181 } 182 183 /** 184 * Overrides onQueryComplete from AsyncQueryHandler. 185 * 186 * This method takes into account the state of this class; we construct the CallerInfo 187 * object only once for each set of listeners. When the query thread has done its work 188 * and calls this method, we inform the remaining listeners in the queue, until we're 189 * out of listeners. Once we get the message indicating that we should expect no new 190 * listeners for this CallerInfo object, we release the AsyncCursorInfo back into the 191 * pool. 192 */ 193 @Override 194 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 195 if (DBG) log("query complete for token: " + token); 196 197 //get the cookie and notify the listener. 198 CookieWrapper cw = (CookieWrapper) cookie; 199 if (cw == null) { 200 // Normally, this should never be the case for calls originating 201 // from within this code. 202 // However, if there is any code that calls this method, we should 203 // check the parameters to make sure they're viable. 204 if (DBG) log("Cookie is null, ignoring onQueryComplete() request."); 205 return; 206 } 207 208 if (cw.event == EVENT_END_OF_QUEUE) { 209 release(); 210 return; 211 } 212 213 // check the token and if needed, create the callerinfo object. 214 if (mCallerInfo == null) { 215 if ((mQueryContext == null) || (mQueryUri == null)) { 216 throw new QueryPoolException 217 ("Bad context or query uri, or CallerInfoAsyncQuery already released."); 218 } 219 220 // adjust the callerInfo data as needed, and only if it was set from the 221 // initial query request. 222 // Change the callerInfo number ONLY if it is an emergency number or the 223 // voicemail number, and adjust other data (including photoResource) 224 // accordingly. 225 if (cw.event == EVENT_EMERGENCY_NUMBER) { 226 mCallerInfo = new CallerInfo(); 227 // Note we're setting the phone number here (refer to javadoc 228 // comments at the top of CallerInfo class). 229 mCallerInfo.phoneNumber = mQueryContext.getString(com.android.internal 230 .R.string.emergency_call_dialog_number_for_display); 231 mCallerInfo.photoResource = com.android.internal.R.drawable.picture_emergency; 232 233 } else if (cw.event == EVENT_VOICEMAIL_NUMBER) { 234 mCallerInfo = new CallerInfo(); 235 try { 236 // Note we're setting the phone number here (refer to javadoc 237 // comments at the top of CallerInfo class). 238 mCallerInfo.phoneNumber = 239 TelephonyManager.getDefault().getVoiceMailAlphaTag(); 240 } catch (SecurityException ex) { 241 // Should never happen: if this process does not have 242 // permission to retrieve VM tag, it should not have 243 // permission to retrieve VM number and would not generate 244 // an EVENT_VOICEMAIL_NUMBER. But if it happens, don't crash. 245 } 246 } else { 247 mCallerInfo = CallerInfo.getCallerInfo(mQueryContext, mQueryUri, cursor); 248 // Use the number entered by the user for display. 249 if (!TextUtils.isEmpty(cw.number)) { 250 mCallerInfo.phoneNumber = PhoneNumberUtils.formatNumber(cw.number); 251 } 252 } 253 254 if (DBG) log("constructing CallerInfo object for token: " + token); 255 256 //notify that we can clean up the queue after this. 257 CookieWrapper endMarker = new CookieWrapper(); 258 endMarker.event = EVENT_END_OF_QUEUE; 259 startQuery (token, endMarker, null, null, null, null, null); 260 } 261 262 //notify the listener that the query is complete. 263 if (cw.listener != null) { 264 if (DBG) log("notifying listener: " + cw.listener.getClass().toString() + 265 " for token: " + token); 266 cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo); 267 } 268 } 269 } 270 271 /** 272 * Private constructor for factory methods. 273 */ 274 private CallerInfoAsyncQuery() { 275 } 276 277 278 /** 279 * Factory method to start query with a Uri query spec 280 */ 281 public static CallerInfoAsyncQuery startQuery(int token, Context context, Uri contactRef, 282 OnQueryCompleteListener listener, Object cookie) { 283 284 CallerInfoAsyncQuery c = new CallerInfoAsyncQuery(); 285 c.allocate(context, contactRef); 286 287 if (DBG) log("starting query for URI: " + contactRef + " handler: " + c.toString()); 288 289 //create cookieWrapper, start query 290 CookieWrapper cw = new CookieWrapper(); 291 cw.listener = listener; 292 cw.cookie = cookie; 293 cw.event = EVENT_NEW_QUERY; 294 295 c.mHandler.startQuery (token, cw, contactRef, null, null, null, null); 296 297 return c; 298 } 299 300 /** 301 * Factory method to start query with a number 302 */ 303 public static CallerInfoAsyncQuery startQuery(int token, Context context, String number, 304 OnQueryCompleteListener listener, Object cookie) { 305 //contruct the URI object and start Query. 306 Uri contactRef = Uri.withAppendedPath(Contacts.Phones.CONTENT_FILTER_URL, number); 307 308 CallerInfoAsyncQuery c = new CallerInfoAsyncQuery(); 309 c.allocate(context, contactRef); 310 311 if (DBG) log("starting query for number: " + number + " handler: " + c.toString()); 312 313 //create cookieWrapper, start query 314 CookieWrapper cw = new CookieWrapper(); 315 cw.listener = listener; 316 cw.cookie = cookie; 317 cw.number = number; 318 319 // check to see if these are recognized numbers, and use shortcuts if we can. 320 if (PhoneNumberUtils.isEmergencyNumber(number)) { 321 cw.event = EVENT_EMERGENCY_NUMBER; 322 } else { 323 String vmNumber = null; 324 if (!sSkipVmCheck){ 325 try { 326 vmNumber = TelephonyManager.getDefault().getVoiceMailNumber(); 327 } catch (SecurityException ex) { 328 // Don't crash if this process doesn't have permission to 329 // retrieve VM number. It's still allowed to look up caller info. 330 // But don't try it again. 331 sSkipVmCheck = true; 332 } 333 } 334 if (PhoneNumberUtils.compare(number, vmNumber)) { 335 cw.event = EVENT_VOICEMAIL_NUMBER; 336 } else { 337 cw.event = EVENT_NEW_QUERY; 338 } 339 } 340 341 c.mHandler.startQuery (token, cw, contactRef, null, null, null, null); 342 343 return c; 344 } 345 346 /** 347 * Method to add listeners to a currently running query 348 */ 349 public void addQueryListener(int token, OnQueryCompleteListener listener, Object cookie) { 350 351 if (DBG) log("adding listener to query: " + mHandler.mQueryUri + " handler: " + 352 mHandler.toString()); 353 354 //create cookieWrapper, add query request to end of queue. 355 CookieWrapper cw = new CookieWrapper(); 356 cw.listener = listener; 357 cw.cookie = cookie; 358 cw.event = EVENT_ADD_LISTENER; 359 360 mHandler.startQuery (token, cw, null, null, null, null, null); 361 } 362 363 /** 364 * Method to create a new CallerInfoAsyncQueryHandler object, ensuring correct 365 * state of context and uri. 366 */ 367 private void allocate (Context context, Uri contactRef) { 368 if ((context == null) || (contactRef == null)){ 369 throw new QueryPoolException("Bad context or query uri."); 370 } 371 mHandler = new CallerInfoAsyncQueryHandler(context); 372 mHandler.mQueryContext = context; 373 mHandler.mQueryUri = contactRef; 374 } 375 376 /** 377 * Releases the relevant data. 378 */ 379 private void release () { 380 mHandler.mQueryContext = null; 381 mHandler.mQueryUri = null; 382 mHandler.mCallerInfo = null; 383 mHandler = null; 384 } 385 386 /** 387 * static logging method 388 */ 389 private static void log(String msg) { 390 Log.d(LOG_TAG, msg); 391 } 392} 393 394