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.model; 18 19 import com.android.contacts.ContactsUtils; 20 import com.android.contacts.model.ContactsSource.DataKind; 21 import com.android.contacts.model.ContactsSource.EditField; 22 import com.android.contacts.model.ContactsSource.EditType; 23 import com.android.contacts.model.EntityDelta.ValuesDelta; 24 import com.google.android.collect.Lists; 25 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.database.Cursor; 29 import android.os.Bundle; 30 import android.provider.ContactsContract.Data; 31 import android.provider.ContactsContract.Intents; 32 import android.provider.ContactsContract.RawContacts; 33 import android.provider.ContactsContract.CommonDataKinds.BaseTypes; 34 import android.provider.ContactsContract.CommonDataKinds.Email; 35 import android.provider.ContactsContract.CommonDataKinds.Im; 36 import android.provider.ContactsContract.CommonDataKinds.Note; 37 import android.provider.ContactsContract.CommonDataKinds.Organization; 38 import android.provider.ContactsContract.CommonDataKinds.Phone; 39 import android.provider.ContactsContract.CommonDataKinds.Photo; 40 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 41 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 42 import android.provider.ContactsContract.Intents.Insert; 43 import android.text.TextUtils; 44 import android.util.Log; 45 import android.util.SparseIntArray; 46 47 import java.util.ArrayList; 48 import java.util.Iterator; 49 import java.util.List; 50 51 /** 52 * Helper methods for modifying an {@link EntityDelta}, such as inserting 53 * new rows, or enforcing {@link ContactsSource}. 54 */ 55 public class EntityModifier { 56 private static final String TAG = "EntityModifier"; 57 58 /** 59 * For the given {@link EntityDelta}, determine if the given 60 * {@link DataKind} could be inserted under specific 61 * {@link ContactsSource}. 62 */ 63 public static boolean canInsert(EntityDelta state, DataKind kind) { 64 // Insert possible when have valid types and under overall maximum 65 final int visibleCount = state.getMimeEntriesCount(kind.mimeType, true); 66 final boolean validTypes = hasValidTypes(state, kind); 67 final boolean validOverall = (kind.typeOverallMax == -1) 68 || (visibleCount < kind.typeOverallMax); 69 return (validTypes && validOverall); 70 } 71 72 public static boolean hasValidTypes(EntityDelta state, DataKind kind) { 73 if (EntityModifier.hasEditTypes(kind)) { 74 return (getValidTypes(state, kind).size() > 0); 75 } else { 76 return true; 77 } 78 } 79 80 /** 81 * Ensure that at least one of the given {@link DataKind} exists in the 82 * given {@link EntityDelta} state, and try creating one if none exist. 83 */ 84 public static void ensureKindExists(EntityDelta state, ContactsSource source, String mimeType) { 85 final DataKind kind = source.getKindForMimetype(mimeType); 86 final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0; 87 88 if (!hasChild && kind != null) { 89 // Create child when none exists and valid kind 90 final ValuesDelta child = insertChild(state, kind); 91 if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { 92 child.setFromTemplate(true); 93 } 94 } 95 } 96 97 /** 98 * For the given {@link EntityDelta} and {@link DataKind}, return the 99 * list possible {@link EditType} options available based on 100 * {@link ContactsSource}. 101 */ 102 public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind) { 103 return getValidTypes(state, kind, null, true, null); 104 } 105 106 /** 107 * For the given {@link EntityDelta} and {@link DataKind}, return the 108 * list possible {@link EditType} options available based on 109 * {@link ContactsSource}. 110 * 111 * @param forceInclude Always include this {@link EditType} in the returned 112 * list, even when an otherwise-invalid choice. This is useful 113 * when showing a dialog that includes the current type. 114 */ 115 public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind, 116 EditType forceInclude) { 117 return getValidTypes(state, kind, forceInclude, true, null); 118 } 119 120 /** 121 * For the given {@link EntityDelta} and {@link DataKind}, return the 122 * list possible {@link EditType} options available based on 123 * {@link ContactsSource}. 124 * 125 * @param forceInclude Always include this {@link EditType} in the returned 126 * list, even when an otherwise-invalid choice. This is useful 127 * when showing a dialog that includes the current type. 128 * @param includeSecondary If true, include any valid types marked as 129 * {@link EditType#secondary}. 130 * @param typeCount When provided, will be used for the frequency count of 131 * each {@link EditType}, otherwise built using 132 * {@link #getTypeFrequencies(EntityDelta, DataKind)}. 133 */ 134 private static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind, 135 EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) { 136 final ArrayList<EditType> validTypes = Lists.newArrayList(); 137 138 // Bail early if no types provided 139 if (!hasEditTypes(kind)) return validTypes; 140 141 if (typeCount == null) { 142 // Build frequency counts if not provided 143 typeCount = getTypeFrequencies(state, kind); 144 } 145 146 // Build list of valid types 147 final int overallCount = typeCount.get(FREQUENCY_TOTAL); 148 for (EditType type : kind.typeList) { 149 final boolean validOverall = (kind.typeOverallMax == -1 ? true 150 : overallCount < kind.typeOverallMax); 151 final boolean validSpecific = (type.specificMax == -1 ? true : typeCount 152 .get(type.rawValue) < type.specificMax); 153 final boolean validSecondary = (includeSecondary ? true : !type.secondary); 154 final boolean forcedInclude = type.equals(forceInclude); 155 if (forcedInclude || (validOverall && validSpecific && validSecondary)) { 156 // Type is valid when no limit, under limit, or forced include 157 validTypes.add(type); 158 } 159 } 160 161 return validTypes; 162 } 163 164 private static final int FREQUENCY_TOTAL = Integer.MIN_VALUE; 165 166 /** 167 * Count up the frequency that each {@link EditType} appears in the given 168 * {@link EntityDelta}. The returned {@link SparseIntArray} maps from 169 * {@link EditType#rawValue} to counts, with the total overall count stored 170 * as {@link #FREQUENCY_TOTAL}. 171 */ 172 private static SparseIntArray getTypeFrequencies(EntityDelta state, DataKind kind) { 173 final SparseIntArray typeCount = new SparseIntArray(); 174 175 // Find all entries for this kind, bailing early if none found 176 final List<ValuesDelta> mimeEntries = state.getMimeEntries(kind.mimeType); 177 if (mimeEntries == null) return typeCount; 178 179 int totalCount = 0; 180 for (ValuesDelta entry : mimeEntries) { 181 // Only count visible entries 182 if (!entry.isVisible()) continue; 183 totalCount++; 184 185 final EditType type = getCurrentType(entry, kind); 186 if (type != null) { 187 final int count = typeCount.get(type.rawValue); 188 typeCount.put(type.rawValue, count + 1); 189 } 190 } 191 typeCount.put(FREQUENCY_TOTAL, totalCount); 192 return typeCount; 193 } 194 195 /** 196 * Check if the given {@link DataKind} has multiple types that should be 197 * displayed for users to pick. 198 */ 199 public static boolean hasEditTypes(DataKind kind) { 200 return kind.typeList != null && kind.typeList.size() > 0; 201 } 202 203 /** 204 * Find the {@link EditType} that describes the given 205 * {@link ValuesDelta} row, assuming the given {@link DataKind} dictates 206 * the possible types. 207 */ 208 public static EditType getCurrentType(ValuesDelta entry, DataKind kind) { 209 final Long rawValue = entry.getAsLong(kind.typeColumn); 210 if (rawValue == null) return null; 211 return getType(kind, rawValue.intValue()); 212 } 213 214 /** 215 * Find the {@link EditType} that describes the given {@link ContentValues} row, 216 * assuming the given {@link DataKind} dictates the possible types. 217 */ 218 public static EditType getCurrentType(ContentValues entry, DataKind kind) { 219 if (kind.typeColumn == null) return null; 220 final Integer rawValue = entry.getAsInteger(kind.typeColumn); 221 if (rawValue == null) return null; 222 return getType(kind, rawValue); 223 } 224 225 /** 226 * Find the {@link EditType} that describes the given {@link Cursor} row, 227 * assuming the given {@link DataKind} dictates the possible types. 228 */ 229 public static EditType getCurrentType(Cursor cursor, DataKind kind) { 230 if (kind.typeColumn == null) return null; 231 final int index = cursor.getColumnIndex(kind.typeColumn); 232 if (index == -1) return null; 233 final int rawValue = cursor.getInt(index); 234 return getType(kind, rawValue); 235 } 236 237 /** 238 * Find the {@link EditType} with the given {@link EditType#rawValue}. 239 */ 240 public static EditType getType(DataKind kind, int rawValue) { 241 for (EditType type : kind.typeList) { 242 if (type.rawValue == rawValue) { 243 return type; 244 } 245 } 246 return null; 247 } 248 249 /** 250 * Return the precedence for the the given {@link EditType#rawValue}, where 251 * lower numbers are higher precedence. 252 */ 253 public static int getTypePrecedence(DataKind kind, int rawValue) { 254 for (int i = 0; i < kind.typeList.size(); i++) { 255 final EditType type = kind.typeList.get(i); 256 if (type.rawValue == rawValue) { 257 return i; 258 } 259 } 260 return Integer.MAX_VALUE; 261 } 262 263 /** 264 * Find the best {@link EditType} for a potential insert. The "best" is the 265 * first primary type that doesn't already exist. When all valid types 266 * exist, we pick the last valid option. 267 */ 268 public static EditType getBestValidType(EntityDelta state, DataKind kind, 269 boolean includeSecondary, int exactValue) { 270 // Shortcut when no types 271 if (kind.typeColumn == null) return null; 272 273 // Find type counts and valid primary types, bail if none 274 final SparseIntArray typeCount = getTypeFrequencies(state, kind); 275 final ArrayList<EditType> validTypes = getValidTypes(state, kind, null, includeSecondary, 276 typeCount); 277 if (validTypes.size() == 0) return null; 278 279 // Keep track of the last valid type 280 final EditType lastType = validTypes.get(validTypes.size() - 1); 281 282 // Remove any types that already exist 283 Iterator<EditType> iterator = validTypes.iterator(); 284 while (iterator.hasNext()) { 285 final EditType type = iterator.next(); 286 final int count = typeCount.get(type.rawValue); 287 288 if (exactValue == type.rawValue) { 289 // Found exact value match 290 return type; 291 } 292 293 if (count > 0) { 294 // Type already appears, so don't consider 295 iterator.remove(); 296 } 297 } 298 299 // Use the best remaining, otherwise the last valid 300 if (validTypes.size() > 0) { 301 return validTypes.get(0); 302 } else { 303 return lastType; 304 } 305 } 306 307 /** 308 * Insert a new child of kind {@link DataKind} into the given 309 * {@link EntityDelta}. Tries using the best {@link EditType} found using 310 * {@link #getBestValidType(EntityDelta, DataKind, boolean, int)}. 311 */ 312 public static ValuesDelta insertChild(EntityDelta state, DataKind kind) { 313 // First try finding a valid primary 314 EditType bestType = getBestValidType(state, kind, false, Integer.MIN_VALUE); 315 if (bestType == null) { 316 // No valid primary found, so expand search to secondary 317 bestType = getBestValidType(state, kind, true, Integer.MIN_VALUE); 318 } 319 return insertChild(state, kind, bestType); 320 } 321 322 /** 323 * Insert a new child of kind {@link DataKind} into the given 324 * {@link EntityDelta}, marked with the given {@link EditType}. 325 */ 326 public static ValuesDelta insertChild(EntityDelta state, DataKind kind, EditType type) { 327 // Bail early if invalid kind 328 if (kind == null) return null; 329 final ContentValues after = new ContentValues(); 330 331 // Our parent CONTACT_ID is provided later 332 after.put(Data.MIMETYPE, kind.mimeType); 333 334 // Fill-in with any requested default values 335 if (kind.defaultValues != null) { 336 after.putAll(kind.defaultValues); 337 } 338 339 if (kind.typeColumn != null && type != null) { 340 // Set type, if provided 341 after.put(kind.typeColumn, type.rawValue); 342 } 343 344 final ValuesDelta child = ValuesDelta.fromAfter(after); 345 state.addEntry(child); 346 return child; 347 } 348 349 /** 350 * Processing to trim any empty {@link ValuesDelta} and {@link EntityDelta} 351 * from the given {@link EntitySet}, assuming the given {@link Sources} 352 * dictates the structure for various fields. This method ignores rows not 353 * described by the {@link ContactsSource}. 354 */ 355 public static void trimEmpty(EntitySet set, Sources sources) { 356 for (EntityDelta state : set) { 357 final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 358 final ContactsSource source = sources.getInflatedSource(accountType, 359 ContactsSource.LEVEL_MIMETYPES); 360 trimEmpty(state, source); 361 } 362 } 363 364 /** 365 * Processing to trim any empty {@link ValuesDelta} rows from the given 366 * {@link EntityDelta}, assuming the given {@link ContactsSource} dictates 367 * the structure for various fields. This method ignores rows not described 368 * by the {@link ContactsSource}. 369 */ 370 public static void trimEmpty(EntityDelta state, ContactsSource source) { 371 boolean hasValues = false; 372 373 // Walk through entries for each well-known kind 374 for (DataKind kind : source.getSortedDataKinds()) { 375 final String mimeType = kind.mimeType; 376 final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType); 377 if (entries == null) continue; 378 379 for (ValuesDelta entry : entries) { 380 // Skip any values that haven't been touched 381 final boolean touched = entry.isInsert() || entry.isUpdate(); 382 if (!touched) { 383 hasValues = true; 384 continue; 385 } 386 387 // Test and remove this row if empty and it isn't a photo from google 388 final boolean isGoogleSource = TextUtils.equals(GoogleSource.ACCOUNT_TYPE, 389 state.getValues().getAsString(RawContacts.ACCOUNT_TYPE)); 390 final boolean isPhoto = TextUtils.equals(Photo.CONTENT_ITEM_TYPE, kind.mimeType); 391 final boolean isGooglePhoto = isPhoto && isGoogleSource; 392 393 if (EntityModifier.isEmpty(entry, kind) && !isGooglePhoto) { 394 // TODO: remove this verbose logging 395 Log.w(TAG, "Trimming: " + entry.toString()); 396 entry.markDeleted(); 397 } else if (!entry.isFromTemplate()) { 398 hasValues = true; 399 } 400 } 401 } 402 if (!hasValues) { 403 // Trim overall entity if no children exist 404 state.markDeleted(); 405 } 406 } 407 408 /** 409 * Test if the given {@link ValuesDelta} would be considered "empty" in 410 * terms of {@link DataKind#fieldList}. 411 */ 412 public static boolean isEmpty(ValuesDelta values, DataKind kind) { 413 // No defined fields mean this row is always empty 414 if (kind.fieldList == null) return true; 415 416 boolean hasValues = false; 417 for (EditField field : kind.fieldList) { 418 // If any field has values, we're not empty 419 final String value = values.getAsString(field.column); 420 if (ContactsUtils.isGraphic(value)) { 421 hasValues = true; 422 } 423 } 424 425 return !hasValues; 426 } 427 428 /** 429 * Parse the given {@link Bundle} into the given {@link EntityDelta} state, 430 * assuming the extras defined through {@link Intents}. 431 */ 432 public static void parseExtras(Context context, ContactsSource source, EntityDelta state, 433 Bundle extras) { 434 if (extras == null || extras.size() == 0) { 435 // Bail early if no useful data 436 return; 437 } 438 439 { 440 // StructuredName 441 EntityModifier.ensureKindExists(state, source, StructuredName.CONTENT_ITEM_TYPE); 442 final ValuesDelta child = state.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); 443 444 final String name = extras.getString(Insert.NAME); 445 if (ContactsUtils.isGraphic(name)) { 446 child.put(StructuredName.GIVEN_NAME, name); 447 } 448 449 final String phoneticName = extras.getString(Insert.PHONETIC_NAME); 450 if (ContactsUtils.isGraphic(phoneticName)) { 451 child.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticName); 452 } 453 } 454 455 { 456 // StructuredPostal 457 final DataKind kind = source.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE); 458 parseExtras(state, kind, extras, Insert.POSTAL_TYPE, Insert.POSTAL, 459 StructuredPostal.STREET); 460 } 461 462 { 463 // Phone 464 final DataKind kind = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE); 465 parseExtras(state, kind, extras, Insert.PHONE_TYPE, Insert.PHONE, Phone.NUMBER); 466 parseExtras(state, kind, extras, Insert.SECONDARY_PHONE_TYPE, Insert.SECONDARY_PHONE, 467 Phone.NUMBER); 468 parseExtras(state, kind, extras, Insert.TERTIARY_PHONE_TYPE, Insert.TERTIARY_PHONE, 469 Phone.NUMBER); 470 } 471 472 { 473 // Email 474 final DataKind kind = source.getKindForMimetype(Email.CONTENT_ITEM_TYPE); 475 parseExtras(state, kind, extras, Insert.EMAIL_TYPE, Insert.EMAIL, Email.DATA); 476 parseExtras(state, kind, extras, Insert.SECONDARY_EMAIL_TYPE, Insert.SECONDARY_EMAIL, 477 Email.DATA); 478 parseExtras(state, kind, extras, Insert.TERTIARY_EMAIL_TYPE, Insert.TERTIARY_EMAIL, 479 Email.DATA); 480 } 481 482 { 483 // Im 484 final DataKind kind = source.getKindForMimetype(Im.CONTENT_ITEM_TYPE); 485 fixupLegacyImType(extras); 486 parseExtras(state, kind, extras, Insert.IM_PROTOCOL, Insert.IM_HANDLE, Im.DATA); 487 } 488 489 // Organization 490 final boolean hasOrg = extras.containsKey(Insert.COMPANY) 491 || extras.containsKey(Insert.JOB_TITLE); 492 final DataKind kindOrg = source.getKindForMimetype(Organization.CONTENT_ITEM_TYPE); 493 if (hasOrg && EntityModifier.canInsert(state, kindOrg)) { 494 final ValuesDelta child = EntityModifier.insertChild(state, kindOrg); 495 496 final String company = extras.getString(Insert.COMPANY); 497 if (ContactsUtils.isGraphic(company)) { 498 child.put(Organization.COMPANY, company); 499 } 500 501 final String title = extras.getString(Insert.JOB_TITLE); 502 if (ContactsUtils.isGraphic(title)) { 503 child.put(Organization.TITLE, title); 504 } 505 } 506 507 // Notes 508 final boolean hasNotes = extras.containsKey(Insert.NOTES); 509 final DataKind kindNotes = source.getKindForMimetype(Note.CONTENT_ITEM_TYPE); 510 if (hasNotes && EntityModifier.canInsert(state, kindNotes)) { 511 final ValuesDelta child = EntityModifier.insertChild(state, kindNotes); 512 513 final String notes = extras.getString(Insert.NOTES); 514 if (ContactsUtils.isGraphic(notes)) { 515 child.put(Note.NOTE, notes); 516 } 517 } 518 } 519 520 /** 521 * Attempt to parse legacy {@link Insert#IM_PROTOCOL} values, replacing them 522 * with updated values. 523 */ 524 private static void fixupLegacyImType(Bundle bundle) { 525 final String encodedString = bundle.getString(Insert.IM_PROTOCOL); 526 if (encodedString == null) return; 527 528 try { 529 final Object protocol = android.provider.Contacts.ContactMethods 530 .decodeImProtocol(encodedString); 531 if (protocol instanceof Integer) { 532 bundle.putInt(Insert.IM_PROTOCOL, (Integer)protocol); 533 } else { 534 bundle.putString(Insert.IM_PROTOCOL, (String)protocol); 535 } 536 } catch (IllegalArgumentException e) { 537 // Ignore exception when legacy parser fails 538 } 539 } 540 541 /** 542 * Parse a specific entry from the given {@link Bundle} and insert into the 543 * given {@link EntityDelta}. Silently skips the insert when missing value 544 * or no valid {@link EditType} found. 545 * 546 * @param typeExtra {@link Bundle} key that holds the incoming 547 * {@link EditType#rawValue} value. 548 * @param valueExtra {@link Bundle} key that holds the incoming value. 549 * @param valueColumn Column to write value into {@link ValuesDelta}. 550 */ 551 public static void parseExtras(EntityDelta state, DataKind kind, Bundle extras, 552 String typeExtra, String valueExtra, String valueColumn) { 553 final CharSequence value = extras.getCharSequence(valueExtra); 554 555 // Bail early if source doesn't handle this type 556 if (kind == null) return; 557 558 // Bail when can't insert type, or value missing 559 final boolean canInsert = EntityModifier.canInsert(state, kind); 560 final boolean validValue = (value != null && TextUtils.isGraphic(value)); 561 if (!validValue || !canInsert) return; 562 563 // Find exact type when requested, otherwise best available type 564 final boolean hasType = extras.containsKey(typeExtra); 565 final int typeValue = extras.getInt(typeExtra, hasType ? BaseTypes.TYPE_CUSTOM 566 : Integer.MIN_VALUE); 567 final EditType editType = EntityModifier.getBestValidType(state, kind, true, typeValue); 568 569 // Create data row and fill with value 570 final ValuesDelta child = EntityModifier.insertChild(state, kind, editType); 571 child.put(valueColumn, value.toString()); 572 573 if (editType != null && editType.customColumn != null) { 574 // Write down label when custom type picked 575 final String customType = extras.getString(typeExtra); 576 child.put(editType.customColumn, customType); 577 } 578 } 579 } 580