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