1 /* 2 * Copyright (C) 2009 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.contacts; 18 19 20 import com.android.contacts.model.ContactsSource; 21 import com.android.contacts.util.Constants; 22 23 import android.content.ContentResolver; 24 import android.content.ContentUris; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.database.Cursor; 29 import android.graphics.Bitmap; 30 import android.graphics.BitmapFactory; 31 import android.graphics.drawable.Drawable; 32 import android.net.Uri; 33 import android.provider.ContactsContract.Contacts; 34 import android.provider.ContactsContract.Data; 35 import android.provider.ContactsContract.RawContacts; 36 import android.provider.ContactsContract.CommonDataKinds.Email; 37 import android.provider.ContactsContract.CommonDataKinds.Im; 38 import android.provider.ContactsContract.CommonDataKinds.Organization; 39 import android.provider.ContactsContract.CommonDataKinds.Phone; 40 import android.provider.ContactsContract.CommonDataKinds.Photo; 41 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 42 import android.telephony.PhoneNumberUtils; 43 import android.text.TextUtils; 44 import android.view.LayoutInflater; 45 import android.view.View; 46 import android.view.ViewGroup; 47 import android.widget.ImageView; 48 import android.widget.TextView; 49 50 import java.util.ArrayList; 51 52 public class ContactsUtils { 53 private static final String TAG = "ContactsUtils"; 54 private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT); 55 /** 56 * Build the display title for the {@link Data#CONTENT_URI} entry in the 57 * provided cursor, assuming the given mimeType. 58 */ 59 public static final CharSequence getDisplayLabel(Context context, 60 String mimeType, Cursor cursor) { 61 // Try finding the type and label for this mimetype 62 int colType; 63 int colLabel; 64 65 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) 66 || Constants.MIME_SMS_ADDRESS.equals(mimeType)) { 67 // Reset to phone mimetype so we generate a label for SMS case 68 mimeType = Phone.CONTENT_ITEM_TYPE; 69 colType = cursor.getColumnIndex(Phone.TYPE); 70 colLabel = cursor.getColumnIndex(Phone.LABEL); 71 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) { 72 colType = cursor.getColumnIndex(Email.TYPE); 73 colLabel = cursor.getColumnIndex(Email.LABEL); 74 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) { 75 colType = cursor.getColumnIndex(StructuredPostal.TYPE); 76 colLabel = cursor.getColumnIndex(StructuredPostal.LABEL); 77 } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) { 78 colType = cursor.getColumnIndex(Organization.TYPE); 79 colLabel = cursor.getColumnIndex(Organization.LABEL); 80 } else { 81 return null; 82 } 83 84 final int type = cursor.getInt(colType); 85 final CharSequence label = cursor.getString(colLabel); 86 87 return getDisplayLabel(context, mimeType, type, label); 88 } 89 90 public static final CharSequence getDisplayLabel(Context context, String mimetype, int type, 91 CharSequence label) { 92 CharSequence display = ""; 93 final int customType; 94 final int defaultType; 95 final int arrayResId; 96 97 if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) { 98 defaultType = Phone.TYPE_HOME; 99 customType = Phone.TYPE_CUSTOM; 100 arrayResId = com.android.internal.R.array.phoneTypes; 101 } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) { 102 defaultType = Email.TYPE_HOME; 103 customType = Email.TYPE_CUSTOM; 104 arrayResId = com.android.internal.R.array.emailAddressTypes; 105 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimetype)) { 106 defaultType = StructuredPostal.TYPE_HOME; 107 customType = StructuredPostal.TYPE_CUSTOM; 108 arrayResId = com.android.internal.R.array.postalAddressTypes; 109 } else if (Organization.CONTENT_ITEM_TYPE.equals(mimetype)) { 110 defaultType = Organization.TYPE_WORK; 111 customType = Organization.TYPE_CUSTOM; 112 arrayResId = com.android.internal.R.array.organizationTypes; 113 } else { 114 // Can't return display label for given mimetype. 115 return display; 116 } 117 118 if (type != customType) { 119 CharSequence[] labels = context.getResources().getTextArray(arrayResId); 120 try { 121 display = labels[type - 1]; 122 } catch (ArrayIndexOutOfBoundsException e) { 123 display = labels[defaultType - 1]; 124 } 125 } else { 126 if (!TextUtils.isEmpty(label)) { 127 display = label; 128 } 129 } 130 return display; 131 } 132 133 /** 134 * Opens an InputStream for the person's photo and returns the photo as a Bitmap. 135 * If the person's photo isn't present returns null. 136 * 137 * @param aggCursor the Cursor pointing to the data record containing the photo. 138 * @param bitmapColumnIndex the column index where the photo Uri is stored. 139 * @param options the decoding options, can be set to null 140 * @return the photo Bitmap 141 */ 142 public static Bitmap loadContactPhoto(Cursor cursor, int bitmapColumnIndex, 143 BitmapFactory.Options options) { 144 if (cursor == null) { 145 return null; 146 } 147 148 byte[] data = cursor.getBlob(bitmapColumnIndex); 149 return BitmapFactory.decodeByteArray(data, 0, data.length, options); 150 } 151 152 /** 153 * Loads a placeholder photo. 154 * 155 * @param placeholderImageResource the resource to use for the placeholder image 156 * @param context the Context 157 * @param options the decoding options, can be set to null 158 * @return the placeholder Bitmap. 159 */ 160 public static Bitmap loadPlaceholderPhoto(int placeholderImageResource, Context context, 161 BitmapFactory.Options options) { 162 if (placeholderImageResource == 0) { 163 return null; 164 } 165 return BitmapFactory.decodeResource(context.getResources(), 166 placeholderImageResource, options); 167 } 168 169 public static Bitmap loadContactPhoto(Context context, long photoId, 170 BitmapFactory.Options options) { 171 Cursor photoCursor = null; 172 Bitmap photoBm = null; 173 174 try { 175 photoCursor = context.getContentResolver().query( 176 ContentUris.withAppendedId(Data.CONTENT_URI, photoId), 177 new String[] { Photo.PHOTO }, 178 null, null, null); 179 180 if (photoCursor.moveToFirst() && !photoCursor.isNull(0)) { 181 byte[] photoData = photoCursor.getBlob(0); 182 photoBm = BitmapFactory.decodeByteArray(photoData, 0, 183 photoData.length, options); 184 } 185 } finally { 186 if (photoCursor != null) { 187 photoCursor.close(); 188 } 189 } 190 191 return photoBm; 192 } 193 194 // TODO find a proper place for the canonical version of these 195 public interface ProviderNames { 196 String YAHOO = "Yahoo"; 197 String GTALK = "GTalk"; 198 String MSN = "MSN"; 199 String ICQ = "ICQ"; 200 String AIM = "AIM"; 201 String XMPP = "XMPP"; 202 String JABBER = "JABBER"; 203 String SKYPE = "SKYPE"; 204 String QQ = "QQ"; 205 } 206 207 /** 208 * This looks up the provider name defined in 209 * ProviderNames from the predefined IM protocol id. 210 * This is used for interacting with the IM application. 211 * 212 * @param protocol the protocol ID 213 * @return the provider name the IM app uses for the given protocol, or null if no 214 * provider is defined for the given protocol 215 * @hide 216 */ 217 public static String lookupProviderNameFromId(int protocol) { 218 switch (protocol) { 219 case Im.PROTOCOL_GOOGLE_TALK: 220 return ProviderNames.GTALK; 221 case Im.PROTOCOL_AIM: 222 return ProviderNames.AIM; 223 case Im.PROTOCOL_MSN: 224 return ProviderNames.MSN; 225 case Im.PROTOCOL_YAHOO: 226 return ProviderNames.YAHOO; 227 case Im.PROTOCOL_ICQ: 228 return ProviderNames.ICQ; 229 case Im.PROTOCOL_JABBER: 230 return ProviderNames.JABBER; 231 case Im.PROTOCOL_SKYPE: 232 return ProviderNames.SKYPE; 233 case Im.PROTOCOL_QQ: 234 return ProviderNames.QQ; 235 } 236 return null; 237 } 238 239 /** 240 * Build {@link Intent} to launch an action for the given {@link Im} or 241 * {@link Email} row. Returns null when missing protocol or data. 242 */ 243 public static Intent buildImIntent(ContentValues values) { 244 final boolean isEmail = Email.CONTENT_ITEM_TYPE.equals(values.getAsString(Data.MIMETYPE)); 245 246 if (!isEmail && !isProtocolValid(values)) { 247 return null; 248 } 249 250 final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : values.getAsInteger(Im.PROTOCOL); 251 252 String host = values.getAsString(Im.CUSTOM_PROTOCOL); 253 String data = values.getAsString(isEmail ? Email.DATA : Im.DATA); 254 if (protocol != Im.PROTOCOL_CUSTOM) { 255 // Try bringing in a well-known host for specific protocols 256 host = ContactsUtils.lookupProviderNameFromId(protocol); 257 } 258 259 if (!TextUtils.isEmpty(host) && !TextUtils.isEmpty(data)) { 260 final String authority = host.toLowerCase(); 261 final Uri imUri = new Uri.Builder().scheme(Constants.SCHEME_IMTO).authority( 262 authority).appendPath(data).build(); 263 return new Intent(Intent.ACTION_SENDTO, imUri); 264 } else { 265 return null; 266 } 267 } 268 269 private static boolean isProtocolValid(ContentValues values) { 270 String protocolString = values.getAsString(Im.PROTOCOL); 271 if (protocolString == null) { 272 return false; 273 } 274 try { 275 Integer.valueOf(protocolString); 276 } catch (NumberFormatException e) { 277 return false; 278 } 279 return true; 280 } 281 282 public static long queryForContactId(ContentResolver cr, long rawContactId) { 283 Cursor contactIdCursor = null; 284 long contactId = -1; 285 try { 286 contactIdCursor = cr.query(RawContacts.CONTENT_URI, 287 new String[] {RawContacts.CONTACT_ID}, 288 RawContacts._ID + "=" + rawContactId, null, null); 289 if (contactIdCursor != null && contactIdCursor.moveToFirst()) { 290 contactId = contactIdCursor.getLong(0); 291 } 292 } finally { 293 if (contactIdCursor != null) { 294 contactIdCursor.close(); 295 } 296 } 297 return contactId; 298 } 299 300 public static String querySuperPrimaryPhone(ContentResolver cr, long contactId) { 301 Cursor c = null; 302 String phone = null; 303 try { 304 Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 305 Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY); 306 307 c = cr.query(dataUri, 308 new String[] {Phone.NUMBER}, 309 Data.MIMETYPE + "=" + Phone.MIMETYPE + 310 " AND " + Data.IS_SUPER_PRIMARY + "=1", 311 null, null); 312 if (c != null && c.moveToFirst()) { 313 // Just return the first one. 314 phone = c.getString(0); 315 } 316 } finally { 317 if (c != null) { 318 c.close(); 319 } 320 } 321 return phone; 322 } 323 324 public static long queryForRawContactId(ContentResolver cr, long contactId) { 325 Cursor rawContactIdCursor = null; 326 long rawContactId = -1; 327 try { 328 rawContactIdCursor = cr.query(RawContacts.CONTENT_URI, 329 new String[] {RawContacts._ID}, 330 RawContacts.CONTACT_ID + "=" + contactId, null, null); 331 if (rawContactIdCursor != null && rawContactIdCursor.moveToFirst()) { 332 // Just return the first one. 333 rawContactId = rawContactIdCursor.getLong(0); 334 } 335 } finally { 336 if (rawContactIdCursor != null) { 337 rawContactIdCursor.close(); 338 } 339 } 340 return rawContactId; 341 } 342 343 public static ArrayList<Long> queryForAllRawContactIds(ContentResolver cr, long contactId) { 344 Cursor rawContactIdCursor = null; 345 ArrayList<Long> rawContactIds = new ArrayList<Long>(); 346 try { 347 rawContactIdCursor = cr.query(RawContacts.CONTENT_URI, 348 new String[] {RawContacts._ID}, 349 RawContacts.CONTACT_ID + "=" + contactId, null, null); 350 if (rawContactIdCursor != null) { 351 while (rawContactIdCursor.moveToNext()) { 352 rawContactIds.add(rawContactIdCursor.getLong(0)); 353 } 354 } 355 } finally { 356 if (rawContactIdCursor != null) { 357 rawContactIdCursor.close(); 358 } 359 } 360 return rawContactIds; 361 } 362 363 364 /** 365 * Utility for creating a standard tab indicator view. 366 * 367 * @param parent The parent ViewGroup to attach the new view to. 368 * @param label The label to display in the tab indicator. If null, not label will be displayed. 369 * @param icon The icon to display. If null, no icon will be displayed. 370 * @return The tab indicator View. 371 */ 372 public static View createTabIndicatorView(ViewGroup parent, CharSequence label, Drawable icon) { 373 final LayoutInflater inflater = (LayoutInflater)parent.getContext().getSystemService( 374 Context.LAYOUT_INFLATER_SERVICE); 375 final View tabIndicator = inflater.inflate(R.layout.tab_indicator, parent, false); 376 tabIndicator.getBackground().setDither(true); 377 378 final TextView tv = (TextView) tabIndicator.findViewById(R.id.tab_title); 379 tv.setText(label); 380 381 final ImageView iconView = (ImageView) tabIndicator.findViewById(R.id.tab_icon); 382 iconView.setImageDrawable(icon); 383 384 return tabIndicator; 385 } 386 387 /** 388 * Utility for creating a standard tab indicator view. 389 * 390 * @param parent The parent ViewGroup to attach the new view to. 391 * @param source The {@link ContactsSource} to build the tab view from. 392 * @return The tab indicator View. 393 */ 394 public static View createTabIndicatorView(ViewGroup parent, ContactsSource source) { 395 Drawable icon = null; 396 if (source != null) { 397 icon = source.getDisplayIcon(parent.getContext()); 398 } 399 return createTabIndicatorView(parent, null, icon); 400 } 401 402 /** 403 * Kick off an intent to initiate a call. 404 * 405 * @param phoneNumber must not be null. 406 * @throws NullPointerException when the given argument is null. 407 */ 408 public static void initiateCall(Context context, CharSequence phoneNumber) { 409 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 410 Uri.fromParts("tel", phoneNumber.toString(), null)); 411 context.startActivity(intent); 412 } 413 414 /** 415 * Kick off an intent to initiate an Sms/Mms message. 416 * 417 * @param phoneNumber must not be null. 418 * @throws NullPointerException when the given argument is null. 419 */ 420 public static void initiateSms(Context context, CharSequence phoneNumber) { 421 Intent intent = new Intent(Intent.ACTION_SENDTO, 422 Uri.fromParts("sms", phoneNumber.toString(), null)); 423 context.startActivity(intent); 424 } 425 426 /** 427 * Test if the given {@link CharSequence} contains any graphic characters, 428 * first checking {@link TextUtils#isEmpty(CharSequence)} to handle null. 429 */ 430 public static boolean isGraphic(CharSequence str) { 431 return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str); 432 } 433 434 /** 435 * Returns true if two objects are considered equal. Two null references are equal here. 436 */ 437 public static boolean areObjectsEqual(Object a, Object b) { 438 return a == b || (a != null && a.equals(b)); 439 } 440 441 /** 442 * Returns true if two data with mimetypes which represent values in contact entries are 443 * considered equal for collapsing in the GUI. For caller-id, use 444 * {@link PhoneNumberUtils#compare(Context, String, String)} instead 445 */ 446 public static final boolean shouldCollapse(Context context, CharSequence mimetype1, 447 CharSequence data1, CharSequence mimetype2, CharSequence data2) { 448 if (TextUtils.equals(Phone.CONTENT_ITEM_TYPE, mimetype1) 449 && TextUtils.equals(Phone.CONTENT_ITEM_TYPE, mimetype2)) { 450 if (data1 == data2) { 451 return true; 452 } 453 if (data1 == null || data2 == null) { 454 return false; 455 } 456 457 // If the number contains semicolons, PhoneNumberUtils.compare 458 // only checks the substring before that (which is fine for caller-id usually) 459 // but not for collapsing numbers. so we check each segment indidually to be more strict 460 // TODO: This should be replaced once we have a more robust phonenumber-library 461 String[] dataParts1 = data1.toString().split(WAIT_SYMBOL_AS_STRING); 462 String[] dataParts2 = data2.toString().split(WAIT_SYMBOL_AS_STRING); 463 if (dataParts1.length != dataParts2.length) { 464 return false; 465 } 466 for (int i = 0; i < dataParts1.length; i++) { 467 if (!PhoneNumberUtils.compare(context, dataParts1[i], dataParts2[i])) { 468 return false; 469 } 470 } 471 472 return true; 473 } else { 474 if (mimetype1 == mimetype2 && data1 == data2) { 475 return true; 476 } 477 return TextUtils.equals(mimetype1, mimetype2) && TextUtils.equals(data1, data2); 478 } 479 } 480 481 /** 482 * Returns true if two {@link Intent}s are both null, or have the same action. 483 */ 484 public static final boolean areIntentActionEqual(Intent a, Intent b) { 485 if (a == b) { 486 return true; 487 } 488 if (a == null || b == null) { 489 return false; 490 } 491 return TextUtils.equals(a.getAction(), b.getAction()); 492 } 493 } 494