CallerInfo.java revision 6fe795ecd35c4d49822d349424fc71b660577dfc
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.Context; 20import android.database.Cursor; 21import android.graphics.drawable.Drawable; 22import android.net.Uri; 23import android.provider.ContactsContract.PhoneLookup; 24import android.provider.ContactsContract.CommonDataKinds.Phone; 25import static android.provider.ContactsContract.RawContacts; 26import android.text.TextUtils; 27import android.telephony.TelephonyManager; 28import android.telephony.PhoneNumberUtils; 29import android.util.Log; 30 31 32/** 33 * Looks up caller information for the given phone number. 34 * 35 * {@hide} 36 */ 37public class CallerInfo { 38 private static final String TAG = "CallerInfo"; 39 private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); 40 41 public static final String UNKNOWN_NUMBER = "-1"; 42 public static final String PRIVATE_NUMBER = "-2"; 43 public static final String PAYPHONE_NUMBER = "-3"; 44 45 /** 46 * Please note that, any one of these member variables can be null, 47 * and any accesses to them should be prepared to handle such a case. 48 * 49 * Also, it is implied that phoneNumber is more often populated than 50 * name is, (think of calls being dialed/received using numbers where 51 * names are not known to the device), so phoneNumber should serve as 52 * a dependable fallback when name is unavailable. 53 * 54 * One other detail here is that this CallerInfo object reflects 55 * information found on a connection, it is an OUTPUT that serves 56 * mainly to display information to the user. In no way is this object 57 * used as input to make a connection, so we can choose to display 58 * whatever human-readable text makes sense to the user for a 59 * connection. This is especially relevant for the phone number field, 60 * since it is the one field that is most likely exposed to the user. 61 * 62 * As an example: 63 * 1. User dials "911" 64 * 2. Device recognizes that this is an emergency number 65 * 3. We use the "Emergency Number" string instead of "911" in the 66 * phoneNumber field. 67 * 68 * What we're really doing here is treating phoneNumber as an essential 69 * field here, NOT name. We're NOT always guaranteed to have a name 70 * for a connection, but the number should be displayable. 71 */ 72 public String name; 73 public String phoneNumber; 74 75 public String cnapName; 76 public int numberPresentation; 77 public int namePresentation; 78 public boolean contactExists; 79 80 public String phoneLabel; 81 /* Split up the phoneLabel into number type and label name */ 82 public int numberType; 83 public String numberLabel; 84 85 public int photoResource; 86 public long person_id; 87 public boolean needUpdate; 88 public Uri contactRefUri; 89 90 // fields to hold individual contact preference data, 91 // including the send to voicemail flag and the ringtone 92 // uri reference. 93 public Uri contactRingtoneUri; 94 public boolean shouldSendToVoicemail; 95 96 /** 97 * Drawable representing the caller image. This is essentially 98 * a cache for the image data tied into the connection / 99 * callerinfo object. The isCachedPhotoCurrent flag indicates 100 * if the image data needs to be reloaded. 101 */ 102 public Drawable cachedPhoto; 103 public boolean isCachedPhotoCurrent; 104 105 private boolean mIsEmergency; 106 private boolean mIsVoiceMail; 107 108 public CallerInfo() { 109 // TODO: Move all the basic initialization here? 110 mIsEmergency = false; 111 mIsVoiceMail = false; 112 } 113 114 /** 115 * getCallerInfo given a Cursor. 116 * @param context the context used to retrieve string constants 117 * @param contactRef the URI to attach to this CallerInfo object 118 * @param cursor the first object in the cursor is used to build the CallerInfo object. 119 * @return the CallerInfo which contains the caller id for the given 120 * number. The returned CallerInfo is null if no number is supplied. 121 */ 122 public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor) { 123 CallerInfo info = new CallerInfo(); 124 info.photoResource = 0; 125 info.phoneLabel = null; 126 info.numberType = 0; 127 info.numberLabel = null; 128 info.cachedPhoto = null; 129 info.isCachedPhotoCurrent = false; 130 info.contactExists = false; 131 132 if (VDBG) Log.v(TAG, "construct callerInfo from cursor"); 133 134 if (cursor != null) { 135 if (cursor.moveToFirst()) { 136 // TODO: photo_id is always available but not taken 137 // care of here. Maybe we should store it in the 138 // CallerInfo object as well. 139 140 int columnIndex; 141 142 // Look for the name 143 columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME); 144 if (columnIndex != -1) { 145 info.name = cursor.getString(columnIndex); 146 } 147 148 // Look for the number 149 columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER); 150 if (columnIndex != -1) { 151 info.phoneNumber = cursor.getString(columnIndex); 152 } 153 154 // Look for the label/type combo 155 columnIndex = cursor.getColumnIndex(PhoneLookup.LABEL); 156 if (columnIndex != -1) { 157 int typeColumnIndex = cursor.getColumnIndex(PhoneLookup.TYPE); 158 if (typeColumnIndex != -1) { 159 info.numberType = cursor.getInt(typeColumnIndex); 160 info.numberLabel = cursor.getString(columnIndex); 161 info.phoneLabel = Phone.getDisplayLabel(context, 162 info.numberType, info.numberLabel) 163 .toString(); 164 } 165 } 166 167 // Look for the person ID. 168 169 // TODO: This is pretty ugly now, see bug 2269240 for 170 // more details. The column to use depends upon the type of URL, 171 // for content://com.android.contacts/data/phones the "contact_id" 172 // column is used. For content/com.andriod.contacts/phone_lookup" 173 // the "_ID" column is used. If it is neither we leave columnIndex 174 // at -1 and no person ID will be available. 175 176 columnIndex = -1; 177 String url = contactRef.toString(); 178 if (url.startsWith("content://com.android.contacts/data/phones")) { 179 if (VDBG) Log.v(TAG, 180 "URL path starts with 'data/phones' using RawContacts.CONTACT_ID"); 181 columnIndex = cursor.getColumnIndex(RawContacts.CONTACT_ID); 182 } else if (url.startsWith("content://com.android.contacts/phone_lookup")) { 183 if (VDBG) Log.v(TAG, 184 "URL path starts with 'phone_lookup' using PhoneLookup._ID"); 185 columnIndex = cursor.getColumnIndex(PhoneLookup._ID); 186 } else { 187 Log.e(TAG, "Bad contact URL '" + url + "'"); 188 } 189 190 if (columnIndex != -1) { 191 info.person_id = cursor.getLong(columnIndex); 192 } else { 193 Log.e(TAG, "person_id column missing for " + contactRef); 194 } 195 196 // look for the custom ringtone, create from the string stored 197 // in the database. 198 columnIndex = cursor.getColumnIndex(PhoneLookup.CUSTOM_RINGTONE); 199 if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) { 200 info.contactRingtoneUri = Uri.parse(cursor.getString(columnIndex)); 201 } else { 202 info.contactRingtoneUri = null; 203 } 204 205 // look for the send to voicemail flag, set it to true only 206 // under certain circumstances. 207 columnIndex = cursor.getColumnIndex(PhoneLookup.SEND_TO_VOICEMAIL); 208 info.shouldSendToVoicemail = (columnIndex != -1) && 209 ((cursor.getInt(columnIndex)) == 1); 210 info.contactExists = true; 211 } 212 cursor.close(); 213 } 214 215 info.needUpdate = false; 216 info.name = normalize(info.name); 217 info.contactRefUri = contactRef; 218 219 return info; 220 } 221 222 /** 223 * getCallerInfo given a URI, look up in the call-log database 224 * for the uri unique key. 225 * @param context the context used to get the ContentResolver 226 * @param contactRef the URI used to lookup caller id 227 * @return the CallerInfo which contains the caller id for the given 228 * number. The returned CallerInfo is null if no number is supplied. 229 */ 230 public static CallerInfo getCallerInfo(Context context, Uri contactRef) { 231 232 return getCallerInfo(context, contactRef, 233 context.getContentResolver().query(contactRef, null, null, null, null)); 234 } 235 236 /** 237 * getCallerInfo given a phone number, look up in the call-log database 238 * for the matching caller id info. 239 * @param context the context used to get the ContentResolver 240 * @param number the phone number used to lookup caller id 241 * @return the CallerInfo which contains the caller id for the given 242 * number. The returned CallerInfo is null if no number is supplied. If 243 * a matching number is not found, then a generic caller info is returned, 244 * with all relevant fields empty or null. 245 */ 246 public static CallerInfo getCallerInfo(Context context, String number) { 247 if (TextUtils.isEmpty(number)) { 248 return null; 249 } 250 251 // Change the callerInfo number ONLY if it is an emergency number 252 // or if it is the voicemail number. If it is either, take a 253 // shortcut and skip the query. 254 if (PhoneNumberUtils.isEmergencyNumber(number)) { 255 return new CallerInfo().markAsEmergency(context); 256 } else if (PhoneNumberUtils.isVoiceMailNumber(number)) { 257 return new CallerInfo().markAsVoiceMail(); 258 } 259 260 Uri contactUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)); 261 262 CallerInfo info = getCallerInfo(context, contactUri); 263 info = doSecondaryLookupIfNecessary(context, number, info); 264 265 // if no query results were returned with a viable number, 266 // fill in the original number value we used to query with. 267 if (TextUtils.isEmpty(info.phoneNumber)) { 268 info.phoneNumber = number; 269 } 270 271 return info; 272 } 273 274 /** 275 * Performs another lookup if previous lookup fails and it's a SIP call 276 * and the peer's username is all numeric. Look up the username as it 277 * could be a PSTN number in the contact database. 278 * 279 * @param context the query context 280 * @param number the original phone number, could be a SIP URI 281 * @param previousResult the result of previous lookup 282 * @return previousResult if it's not the case 283 */ 284 static CallerInfo doSecondaryLookupIfNecessary(Context context, 285 String number, CallerInfo previousResult) { 286 if (!previousResult.contactExists 287 && PhoneNumberUtils.isUriNumber(number)) { 288 String username = number.substring(0, number.indexOf('@')); 289 if (PhoneNumberUtils.isGlobalPhoneNumber(username)) { 290 previousResult = getCallerInfo(context, 291 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, 292 Uri.encode(username))); 293 } 294 } 295 return previousResult; 296 } 297 298 /** 299 * getCallerId: a convenience method to get the caller id for a given 300 * number. 301 * 302 * @param context the context used to get the ContentResolver. 303 * @param number a phone number. 304 * @return if the number belongs to a contact, the contact's name is 305 * returned; otherwise, the number itself is returned. 306 * 307 * TODO NOTE: This MAY need to refer to the Asynchronous Query API 308 * [startQuery()], instead of getCallerInfo, but since it looks like 309 * it is only being used by the provider calls in the messaging app: 310 * 1. android.provider.Telephony.Mms.getDisplayAddress() 311 * 2. android.provider.Telephony.Sms.getDisplayAddress() 312 * We may not need to make the change. 313 */ 314 public static String getCallerId(Context context, String number) { 315 CallerInfo info = getCallerInfo(context, number); 316 String callerID = null; 317 318 if (info != null) { 319 String name = info.name; 320 321 if (!TextUtils.isEmpty(name)) { 322 callerID = name; 323 } else { 324 callerID = number; 325 } 326 } 327 328 return callerID; 329 } 330 331 // Accessors 332 333 /** 334 * @return true if the caller info is an emergency number. 335 */ 336 public boolean isEmergencyNumber() { 337 return mIsEmergency; 338 } 339 340 /** 341 * @return true if the caller info is a voicemail number. 342 */ 343 public boolean isVoiceMailNumber() { 344 return mIsVoiceMail; 345 } 346 347 /** 348 * Mark this CallerInfo as an emergency call. 349 * @param context To lookup the localized 'Emergency Number' string. 350 * @return this instance. 351 */ 352 // TODO: Note we're setting the phone number here (refer to 353 // javadoc comments at the top of CallerInfo class) to a localized 354 // string 'Emergency Number'. This is pretty bad because we are 355 // making UI work here instead of just packaging the data. We 356 // should set the phone number to the dialed number and name to 357 // 'Emergency Number' and let the UI make the decision about what 358 // should be displayed. 359 /* package */ CallerInfo markAsEmergency(Context context) { 360 phoneNumber = context.getString( 361 com.android.internal.R.string.emergency_call_dialog_number_for_display); 362 photoResource = com.android.internal.R.drawable.picture_emergency; 363 mIsEmergency = true; 364 return this; 365 } 366 367 368 /** 369 * Mark this CallerInfo as a voicemail call. The voicemail label 370 * is obtained from the telephony manager. Caller must hold the 371 * READ_PHONE_STATE permission otherwise the phoneNumber will be 372 * set to null. 373 * @return this instance. 374 */ 375 // TODO: As in the emergency number handling, we end up writing a 376 // string in the phone number field. 377 /* package */ CallerInfo markAsVoiceMail() { 378 mIsVoiceMail = true; 379 380 try { 381 String voiceMailLabel = TelephonyManager.getDefault().getVoiceMailAlphaTag(); 382 383 phoneNumber = voiceMailLabel; 384 } catch (SecurityException se) { 385 // Should never happen: if this process does not have 386 // permission to retrieve VM tag, it should not have 387 // permission to retrieve VM number and would not call 388 // this method. 389 // Leave phoneNumber untouched. 390 Log.e(TAG, "Cannot access VoiceMail.", se); 391 } 392 // TODO: There is no voicemail picture? 393 // FIXME: FIND ANOTHER ICON 394 // photoResource = android.R.drawable.badge_voicemail; 395 return this; 396 } 397 398 private static String normalize(String s) { 399 if (s == null || s.length() > 0) { 400 return s; 401 } else { 402 return null; 403 } 404 } 405 406 /** 407 * @return a string debug representation of this instance. 408 */ 409 public String toString() { 410 return new StringBuilder(384) 411 .append("\nname: " + /*name*/ "nnnnnn") 412 .append("\nphoneNumber: " + /*phoneNumber*/ "xxxxxxx") 413 .append("\ncnapName: " + cnapName) 414 .append("\nnumberPresentation: " + numberPresentation) 415 .append("\nnamePresentation: " + namePresentation) 416 .append("\ncontactExits: " + contactExists) 417 .append("\nphoneLabel: " + phoneLabel) 418 .append("\nnumberType: " + numberType) 419 .append("\nnumberLabel: " + numberLabel) 420 .append("\nphotoResource: " + photoResource) 421 .append("\nperson_id: " + person_id) 422 .append("\nneedUpdate: " + needUpdate) 423 .append("\ncontactRefUri: " + /*contactRefUri*/ "xxxxxxx") 424 .append("\ncontactRingtoneUri: " + /*contactRefUri*/ "xxxxxxx") 425 .append("\nshouldSendToVoicemail: " + shouldSendToVoicemail) 426 .append("\ncachedPhoto: " + cachedPhoto) 427 .append("\nisCachedPhotoCurrent: " + isCachedPhotoCurrent) 428 .append("\nemergency: " + mIsEmergency) 429 .append("\nvoicemail " + mIsVoiceMail) 430 .append("\ncontactExists " + contactExists) 431 .toString(); 432 } 433} 434