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