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.ContactsUtils; 20import com.android.contacts.editor.EventFieldEditorView; 21import com.android.contacts.util.NameConverter; 22import com.android.contacts.editor.PhoneticNameEditorView; 23import com.android.contacts.model.AccountType.EditField; 24import com.android.contacts.model.AccountType.EditType; 25import com.android.contacts.model.AccountType.EventEditType; 26import com.android.contacts.model.EntityDelta.ValuesDelta; 27import com.android.contacts.util.DateUtils; 28 29import android.content.ContentValues; 30import android.content.Context; 31import android.database.Cursor; 32import android.net.Uri; 33import android.os.Bundle; 34import android.provider.ContactsContract; 35import android.provider.ContactsContract.CommonDataKinds.BaseTypes; 36import android.provider.ContactsContract.CommonDataKinds.Email; 37import android.provider.ContactsContract.CommonDataKinds.Event; 38import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 39import android.provider.ContactsContract.CommonDataKinds.Im; 40import android.provider.ContactsContract.CommonDataKinds.Nickname; 41import android.provider.ContactsContract.CommonDataKinds.Note; 42import android.provider.ContactsContract.CommonDataKinds.Organization; 43import android.provider.ContactsContract.CommonDataKinds.Phone; 44import android.provider.ContactsContract.CommonDataKinds.Photo; 45import android.provider.ContactsContract.CommonDataKinds.Relation; 46import android.provider.ContactsContract.CommonDataKinds.SipAddress; 47import android.provider.ContactsContract.CommonDataKinds.StructuredName; 48import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 49import android.provider.ContactsContract.CommonDataKinds.Website; 50import android.provider.ContactsContract.Data; 51import android.provider.ContactsContract.Intents; 52import android.provider.ContactsContract.Intents.Insert; 53import android.provider.ContactsContract.RawContacts; 54import android.text.TextUtils; 55import android.util.Log; 56import android.util.SparseArray; 57import android.util.SparseIntArray; 58 59import java.text.ParsePosition; 60import java.util.ArrayList; 61import java.util.Arrays; 62import java.util.Calendar; 63import java.util.Date; 64import java.util.HashSet; 65import java.util.Iterator; 66import java.util.List; 67import java.util.Locale; 68import java.util.Set; 69 70/** 71 * Helper methods for modifying an {@link EntityDelta}, such as inserting 72 * new rows, or enforcing {@link AccountType}. 73 */ 74public class EntityModifier { 75 private static final String TAG = "EntityModifier"; 76 77 /** Set to true in order to view logs on entity operations */ 78 private static final boolean DEBUG = false; 79 80 /** 81 * For the given {@link EntityDelta}, determine if the given 82 * {@link DataKind} could be inserted under specific 83 * {@link AccountType}. 84 */ 85 public static boolean canInsert(EntityDelta state, DataKind kind) { 86 // Insert possible when have valid types and under overall maximum 87 final int visibleCount = state.getMimeEntriesCount(kind.mimeType, true); 88 final boolean validTypes = hasValidTypes(state, kind); 89 final boolean validOverall = (kind.typeOverallMax == -1) 90 || (visibleCount < kind.typeOverallMax); 91 return (validTypes && validOverall); 92 } 93 94 public static boolean hasValidTypes(EntityDelta state, DataKind kind) { 95 if (EntityModifier.hasEditTypes(kind)) { 96 return (getValidTypes(state, kind).size() > 0); 97 } else { 98 return true; 99 } 100 } 101 102 /** 103 * Ensure that at least one of the given {@link DataKind} exists in the 104 * given {@link EntityDelta} state, and try creating one if none exist. 105 * @return The child (either newly created or the first existing one), or null if the 106 * account doesn't support this {@link DataKind}. 107 */ 108 public static ValuesDelta ensureKindExists( 109 EntityDelta state, AccountType accountType, String mimeType) { 110 final DataKind kind = accountType.getKindForMimetype(mimeType); 111 final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0; 112 113 if (kind != null) { 114 if (hasChild) { 115 // Return the first entry. 116 return state.getMimeEntries(mimeType).get(0); 117 } else { 118 // Create child when none exists and valid kind 119 final ValuesDelta child = insertChild(state, kind); 120 if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) { 121 child.setFromTemplate(true); 122 } 123 return child; 124 } 125 } 126 return null; 127 } 128 129 /** 130 * For the given {@link EntityDelta} and {@link DataKind}, return the 131 * list possible {@link EditType} options available based on 132 * {@link AccountType}. 133 */ 134 public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind) { 135 return getValidTypes(state, kind, null, true, null); 136 } 137 138 /** 139 * For the given {@link EntityDelta} and {@link DataKind}, return the 140 * list possible {@link EditType} options available based on 141 * {@link AccountType}. 142 * 143 * @param forceInclude Always include this {@link EditType} in the returned 144 * list, even when an otherwise-invalid choice. This is useful 145 * when showing a dialog that includes the current type. 146 */ 147 public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind, 148 EditType forceInclude) { 149 return getValidTypes(state, kind, forceInclude, true, null); 150 } 151 152 /** 153 * For the given {@link EntityDelta} and {@link DataKind}, return the 154 * list possible {@link EditType} options available based on 155 * {@link AccountType}. 156 * 157 * @param forceInclude Always include this {@link EditType} in the returned 158 * list, even when an otherwise-invalid choice. This is useful 159 * when showing a dialog that includes the current type. 160 * @param includeSecondary If true, include any valid types marked as 161 * {@link EditType#secondary}. 162 * @param typeCount When provided, will be used for the frequency count of 163 * each {@link EditType}, otherwise built using 164 * {@link #getTypeFrequencies(EntityDelta, DataKind)}. 165 */ 166 private static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind, 167 EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) { 168 final ArrayList<EditType> validTypes = new ArrayList<EditType>(); 169 170 // Bail early if no types provided 171 if (!hasEditTypes(kind)) return validTypes; 172 173 if (typeCount == null) { 174 // Build frequency counts if not provided 175 typeCount = getTypeFrequencies(state, kind); 176 } 177 178 // Build list of valid types 179 final int overallCount = typeCount.get(FREQUENCY_TOTAL); 180 for (EditType type : kind.typeList) { 181 final boolean validOverall = (kind.typeOverallMax == -1 ? true 182 : overallCount < kind.typeOverallMax); 183 final boolean validSpecific = (type.specificMax == -1 ? true : typeCount 184 .get(type.rawValue) < type.specificMax); 185 final boolean validSecondary = (includeSecondary ? true : !type.secondary); 186 final boolean forcedInclude = type.equals(forceInclude); 187 if (forcedInclude || (validOverall && validSpecific && validSecondary)) { 188 // Type is valid when no limit, under limit, or forced include 189 validTypes.add(type); 190 } 191 } 192 193 return validTypes; 194 } 195 196 private static final int FREQUENCY_TOTAL = Integer.MIN_VALUE; 197 198 /** 199 * Count up the frequency that each {@link EditType} appears in the given 200 * {@link EntityDelta}. The returned {@link SparseIntArray} maps from 201 * {@link EditType#rawValue} to counts, with the total overall count stored 202 * as {@link #FREQUENCY_TOTAL}. 203 */ 204 private static SparseIntArray getTypeFrequencies(EntityDelta state, DataKind kind) { 205 final SparseIntArray typeCount = new SparseIntArray(); 206 207 // Find all entries for this kind, bailing early if none found 208 final List<ValuesDelta> mimeEntries = state.getMimeEntries(kind.mimeType); 209 if (mimeEntries == null) return typeCount; 210 211 int totalCount = 0; 212 for (ValuesDelta entry : mimeEntries) { 213 // Only count visible entries 214 if (!entry.isVisible()) continue; 215 totalCount++; 216 217 final EditType type = getCurrentType(entry, kind); 218 if (type != null) { 219 final int count = typeCount.get(type.rawValue); 220 typeCount.put(type.rawValue, count + 1); 221 } 222 } 223 typeCount.put(FREQUENCY_TOTAL, totalCount); 224 return typeCount; 225 } 226 227 /** 228 * Check if the given {@link DataKind} has multiple types that should be 229 * displayed for users to pick. 230 */ 231 public static boolean hasEditTypes(DataKind kind) { 232 return kind.typeList != null && kind.typeList.size() > 0; 233 } 234 235 /** 236 * Find the {@link EditType} that describes the given 237 * {@link ValuesDelta} row, assuming the given {@link DataKind} dictates 238 * the possible types. 239 */ 240 public static EditType getCurrentType(ValuesDelta entry, DataKind kind) { 241 final Long rawValue = entry.getAsLong(kind.typeColumn); 242 if (rawValue == null) return null; 243 return getType(kind, rawValue.intValue()); 244 } 245 246 /** 247 * Find the {@link EditType} that describes the given {@link ContentValues} row, 248 * assuming the given {@link DataKind} dictates the possible types. 249 */ 250 public static EditType getCurrentType(ContentValues entry, DataKind kind) { 251 if (kind.typeColumn == null) return null; 252 final Integer rawValue = entry.getAsInteger(kind.typeColumn); 253 if (rawValue == null) return null; 254 return getType(kind, rawValue); 255 } 256 257 /** 258 * Find the {@link EditType} that describes the given {@link Cursor} row, 259 * assuming the given {@link DataKind} dictates the possible types. 260 */ 261 public static EditType getCurrentType(Cursor cursor, DataKind kind) { 262 if (kind.typeColumn == null) return null; 263 final int index = cursor.getColumnIndex(kind.typeColumn); 264 if (index == -1) return null; 265 final int rawValue = cursor.getInt(index); 266 return getType(kind, rawValue); 267 } 268 269 /** 270 * Find the {@link EditType} with the given {@link EditType#rawValue}. 271 */ 272 public static EditType getType(DataKind kind, int rawValue) { 273 for (EditType type : kind.typeList) { 274 if (type.rawValue == rawValue) { 275 return type; 276 } 277 } 278 return null; 279 } 280 281 /** 282 * Return the precedence for the the given {@link EditType#rawValue}, where 283 * lower numbers are higher precedence. 284 */ 285 public static int getTypePrecedence(DataKind kind, int rawValue) { 286 for (int i = 0; i < kind.typeList.size(); i++) { 287 final EditType type = kind.typeList.get(i); 288 if (type.rawValue == rawValue) { 289 return i; 290 } 291 } 292 return Integer.MAX_VALUE; 293 } 294 295 /** 296 * Find the best {@link EditType} for a potential insert. The "best" is the 297 * first primary type that doesn't already exist. When all valid types 298 * exist, we pick the last valid option. 299 */ 300 public static EditType getBestValidType(EntityDelta state, DataKind kind, 301 boolean includeSecondary, int exactValue) { 302 // Shortcut when no types 303 if (kind.typeColumn == null) return null; 304 305 // Find type counts and valid primary types, bail if none 306 final SparseIntArray typeCount = getTypeFrequencies(state, kind); 307 final ArrayList<EditType> validTypes = getValidTypes(state, kind, null, includeSecondary, 308 typeCount); 309 if (validTypes.size() == 0) return null; 310 311 // Keep track of the last valid type 312 final EditType lastType = validTypes.get(validTypes.size() - 1); 313 314 // Remove any types that already exist 315 Iterator<EditType> iterator = validTypes.iterator(); 316 while (iterator.hasNext()) { 317 final EditType type = iterator.next(); 318 final int count = typeCount.get(type.rawValue); 319 320 if (exactValue == type.rawValue) { 321 // Found exact value match 322 return type; 323 } 324 325 if (count > 0) { 326 // Type already appears, so don't consider 327 iterator.remove(); 328 } 329 } 330 331 // Use the best remaining, otherwise the last valid 332 if (validTypes.size() > 0) { 333 return validTypes.get(0); 334 } else { 335 return lastType; 336 } 337 } 338 339 /** 340 * Insert a new child of kind {@link DataKind} into the given 341 * {@link EntityDelta}. Tries using the best {@link EditType} found using 342 * {@link #getBestValidType(EntityDelta, DataKind, boolean, int)}. 343 */ 344 public static ValuesDelta insertChild(EntityDelta state, DataKind kind) { 345 // First try finding a valid primary 346 EditType bestType = getBestValidType(state, kind, false, Integer.MIN_VALUE); 347 if (bestType == null) { 348 // No valid primary found, so expand search to secondary 349 bestType = getBestValidType(state, kind, true, Integer.MIN_VALUE); 350 } 351 return insertChild(state, kind, bestType); 352 } 353 354 /** 355 * Insert a new child of kind {@link DataKind} into the given 356 * {@link EntityDelta}, marked with the given {@link EditType}. 357 */ 358 public static ValuesDelta insertChild(EntityDelta state, DataKind kind, EditType type) { 359 // Bail early if invalid kind 360 if (kind == null) return null; 361 final ContentValues after = new ContentValues(); 362 363 // Our parent CONTACT_ID is provided later 364 after.put(Data.MIMETYPE, kind.mimeType); 365 366 // Fill-in with any requested default values 367 if (kind.defaultValues != null) { 368 after.putAll(kind.defaultValues); 369 } 370 371 if (kind.typeColumn != null && type != null) { 372 // Set type, if provided 373 after.put(kind.typeColumn, type.rawValue); 374 } 375 376 final ValuesDelta child = ValuesDelta.fromAfter(after); 377 state.addEntry(child); 378 return child; 379 } 380 381 /** 382 * Processing to trim any empty {@link ValuesDelta} and {@link EntityDelta} 383 * from the given {@link EntityDeltaList}, assuming the given {@link AccountTypeManager} 384 * dictates the structure for various fields. This method ignores rows not 385 * described by the {@link AccountType}. 386 */ 387 public static void trimEmpty(EntityDeltaList set, AccountTypeManager accountTypes) { 388 for (EntityDelta state : set) { 389 ValuesDelta values = state.getValues(); 390 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 391 final String dataSet = values.getAsString(RawContacts.DATA_SET); 392 final AccountType type = accountTypes.getAccountType(accountType, dataSet); 393 trimEmpty(state, type); 394 } 395 } 396 397 public static boolean hasChanges(EntityDeltaList set, AccountTypeManager accountTypes) { 398 if (set.isMarkedForSplitting() || set.isMarkedForJoining()) { 399 return true; 400 } 401 402 for (EntityDelta state : set) { 403 ValuesDelta values = state.getValues(); 404 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 405 final String dataSet = values.getAsString(RawContacts.DATA_SET); 406 final AccountType type = accountTypes.getAccountType(accountType, dataSet); 407 if (hasChanges(state, type)) { 408 return true; 409 } 410 } 411 return false; 412 } 413 414 /** 415 * Processing to trim any empty {@link ValuesDelta} rows from the given 416 * {@link EntityDelta}, assuming the given {@link AccountType} dictates 417 * the structure for various fields. This method ignores rows not described 418 * by the {@link AccountType}. 419 */ 420 public static void trimEmpty(EntityDelta state, AccountType accountType) { 421 boolean hasValues = false; 422 423 // Walk through entries for each well-known kind 424 for (DataKind kind : accountType.getSortedDataKinds()) { 425 final String mimeType = kind.mimeType; 426 final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType); 427 if (entries == null) continue; 428 429 for (ValuesDelta entry : entries) { 430 // Skip any values that haven't been touched 431 final boolean touched = entry.isInsert() || entry.isUpdate(); 432 if (!touched) { 433 hasValues = true; 434 continue; 435 } 436 437 // Test and remove this row if empty and it isn't a photo from google 438 final boolean isGoogleAccount = TextUtils.equals(GoogleAccountType.ACCOUNT_TYPE, 439 state.getValues().getAsString(RawContacts.ACCOUNT_TYPE)); 440 final boolean isPhoto = TextUtils.equals(Photo.CONTENT_ITEM_TYPE, kind.mimeType); 441 final boolean isGooglePhoto = isPhoto && isGoogleAccount; 442 443 if (EntityModifier.isEmpty(entry, kind) && !isGooglePhoto) { 444 if (DEBUG) { 445 Log.v(TAG, "Trimming: " + entry.toString()); 446 } 447 entry.markDeleted(); 448 } else if (!entry.isFromTemplate()) { 449 hasValues = true; 450 } 451 } 452 } 453 if (!hasValues) { 454 // Trim overall entity if no children exist 455 state.markDeleted(); 456 } 457 } 458 459 private static boolean hasChanges(EntityDelta state, AccountType accountType) { 460 for (DataKind kind : accountType.getSortedDataKinds()) { 461 final String mimeType = kind.mimeType; 462 final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType); 463 if (entries == null) continue; 464 465 for (ValuesDelta entry : entries) { 466 // An empty Insert must be ignored, because it won't save anything (an example 467 // is an empty name that stays empty) 468 final boolean isRealInsert = entry.isInsert() && !isEmpty(entry, kind); 469 if (isRealInsert || entry.isUpdate() || entry.isDelete()) { 470 return true; 471 } 472 } 473 } 474 return false; 475 } 476 477 /** 478 * Test if the given {@link ValuesDelta} would be considered "empty" in 479 * terms of {@link DataKind#fieldList}. 480 */ 481 public static boolean isEmpty(ValuesDelta values, DataKind kind) { 482 if (Photo.CONTENT_ITEM_TYPE.equals(kind.mimeType)) { 483 return values.isInsert() && values.getAsByteArray(Photo.PHOTO) == null; 484 } 485 486 // No defined fields mean this row is always empty 487 if (kind.fieldList == null) return true; 488 489 for (EditField field : kind.fieldList) { 490 // If any field has values, we're not empty 491 final String value = values.getAsString(field.column); 492 if (ContactsUtils.isGraphic(value)) { 493 return false; 494 } 495 } 496 497 return true; 498 } 499 500 /** 501 * Compares corresponding fields in values1 and values2. Only the fields 502 * declared by the DataKind are taken into consideration. 503 */ 504 protected static boolean areEqual(ValuesDelta values1, ContentValues values2, DataKind kind) { 505 if (kind.fieldList == null) return false; 506 507 for (EditField field : kind.fieldList) { 508 final String value1 = values1.getAsString(field.column); 509 final String value2 = values2.getAsString(field.column); 510 if (!TextUtils.equals(value1, value2)) { 511 return false; 512 } 513 } 514 515 return true; 516 } 517 518 /** 519 * Parse the given {@link Bundle} into the given {@link EntityDelta} state, 520 * assuming the extras defined through {@link Intents}. 521 */ 522 public static void parseExtras(Context context, AccountType accountType, EntityDelta state, 523 Bundle extras) { 524 if (extras == null || extras.size() == 0) { 525 // Bail early if no useful data 526 return; 527 } 528 529 parseStructuredNameExtra(context, accountType, state, extras); 530 parseStructuredPostalExtra(accountType, state, extras); 531 532 { 533 // Phone 534 final DataKind kind = accountType.getKindForMimetype(Phone.CONTENT_ITEM_TYPE); 535 parseExtras(state, kind, extras, Insert.PHONE_TYPE, Insert.PHONE, Phone.NUMBER); 536 parseExtras(state, kind, extras, Insert.SECONDARY_PHONE_TYPE, Insert.SECONDARY_PHONE, 537 Phone.NUMBER); 538 parseExtras(state, kind, extras, Insert.TERTIARY_PHONE_TYPE, Insert.TERTIARY_PHONE, 539 Phone.NUMBER); 540 } 541 542 { 543 // Email 544 final DataKind kind = accountType.getKindForMimetype(Email.CONTENT_ITEM_TYPE); 545 parseExtras(state, kind, extras, Insert.EMAIL_TYPE, Insert.EMAIL, Email.DATA); 546 parseExtras(state, kind, extras, Insert.SECONDARY_EMAIL_TYPE, Insert.SECONDARY_EMAIL, 547 Email.DATA); 548 parseExtras(state, kind, extras, Insert.TERTIARY_EMAIL_TYPE, Insert.TERTIARY_EMAIL, 549 Email.DATA); 550 } 551 552 { 553 // Im 554 final DataKind kind = accountType.getKindForMimetype(Im.CONTENT_ITEM_TYPE); 555 fixupLegacyImType(extras); 556 parseExtras(state, kind, extras, Insert.IM_PROTOCOL, Insert.IM_HANDLE, Im.DATA); 557 } 558 559 // Organization 560 final boolean hasOrg = extras.containsKey(Insert.COMPANY) 561 || extras.containsKey(Insert.JOB_TITLE); 562 final DataKind kindOrg = accountType.getKindForMimetype(Organization.CONTENT_ITEM_TYPE); 563 if (hasOrg && EntityModifier.canInsert(state, kindOrg)) { 564 final ValuesDelta child = EntityModifier.insertChild(state, kindOrg); 565 566 final String company = extras.getString(Insert.COMPANY); 567 if (ContactsUtils.isGraphic(company)) { 568 child.put(Organization.COMPANY, company); 569 } 570 571 final String title = extras.getString(Insert.JOB_TITLE); 572 if (ContactsUtils.isGraphic(title)) { 573 child.put(Organization.TITLE, title); 574 } 575 } 576 577 // Notes 578 final boolean hasNotes = extras.containsKey(Insert.NOTES); 579 final DataKind kindNotes = accountType.getKindForMimetype(Note.CONTENT_ITEM_TYPE); 580 if (hasNotes && EntityModifier.canInsert(state, kindNotes)) { 581 final ValuesDelta child = EntityModifier.insertChild(state, kindNotes); 582 583 final String notes = extras.getString(Insert.NOTES); 584 if (ContactsUtils.isGraphic(notes)) { 585 child.put(Note.NOTE, notes); 586 } 587 } 588 589 // Arbitrary additional data 590 ArrayList<ContentValues> values = extras.getParcelableArrayList(Insert.DATA); 591 if (values != null) { 592 parseValues(state, accountType, values); 593 } 594 } 595 596 private static void parseStructuredNameExtra( 597 Context context, AccountType accountType, EntityDelta state, Bundle extras) { 598 // StructuredName 599 EntityModifier.ensureKindExists(state, accountType, StructuredName.CONTENT_ITEM_TYPE); 600 final ValuesDelta child = state.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); 601 602 final String name = extras.getString(Insert.NAME); 603 if (ContactsUtils.isGraphic(name)) { 604 final DataKind kind = accountType.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE); 605 boolean supportsDisplayName = false; 606 if (kind.fieldList != null) { 607 for (EditField field : kind.fieldList) { 608 if (StructuredName.DISPLAY_NAME.equals(field.column)) { 609 supportsDisplayName = true; 610 break; 611 } 612 } 613 } 614 615 if (supportsDisplayName) { 616 child.put(StructuredName.DISPLAY_NAME, name); 617 } else { 618 Uri uri = ContactsContract.AUTHORITY_URI.buildUpon() 619 .appendPath("complete_name") 620 .appendQueryParameter(StructuredName.DISPLAY_NAME, name) 621 .build(); 622 Cursor cursor = context.getContentResolver().query(uri, 623 new String[]{ 624 StructuredName.PREFIX, 625 StructuredName.GIVEN_NAME, 626 StructuredName.MIDDLE_NAME, 627 StructuredName.FAMILY_NAME, 628 StructuredName.SUFFIX, 629 }, null, null, null); 630 631 try { 632 if (cursor.moveToFirst()) { 633 child.put(StructuredName.PREFIX, cursor.getString(0)); 634 child.put(StructuredName.GIVEN_NAME, cursor.getString(1)); 635 child.put(StructuredName.MIDDLE_NAME, cursor.getString(2)); 636 child.put(StructuredName.FAMILY_NAME, cursor.getString(3)); 637 child.put(StructuredName.SUFFIX, cursor.getString(4)); 638 } 639 } finally { 640 cursor.close(); 641 } 642 } 643 } 644 645 final String phoneticName = extras.getString(Insert.PHONETIC_NAME); 646 if (ContactsUtils.isGraphic(phoneticName)) { 647 child.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticName); 648 } 649 } 650 651 private static void parseStructuredPostalExtra( 652 AccountType accountType, EntityDelta state, Bundle extras) { 653 // StructuredPostal 654 final DataKind kind = accountType.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE); 655 final ValuesDelta child = parseExtras(state, kind, extras, Insert.POSTAL_TYPE, 656 Insert.POSTAL, StructuredPostal.FORMATTED_ADDRESS); 657 String address = child == null ? null 658 : child.getAsString(StructuredPostal.FORMATTED_ADDRESS); 659 if (!TextUtils.isEmpty(address)) { 660 boolean supportsFormatted = false; 661 if (kind.fieldList != null) { 662 for (EditField field : kind.fieldList) { 663 if (StructuredPostal.FORMATTED_ADDRESS.equals(field.column)) { 664 supportsFormatted = true; 665 break; 666 } 667 } 668 } 669 670 if (!supportsFormatted) { 671 child.put(StructuredPostal.STREET, address); 672 child.putNull(StructuredPostal.FORMATTED_ADDRESS); 673 } 674 } 675 } 676 677 private static void parseValues( 678 EntityDelta state, AccountType accountType, ArrayList<ContentValues> dataValueList) { 679 for (ContentValues values : dataValueList) { 680 String mimeType = values.getAsString(Data.MIMETYPE); 681 if (TextUtils.isEmpty(mimeType)) { 682 Log.e(TAG, "Mimetype is required. Ignoring: " + values); 683 continue; 684 } 685 686 // Won't override the contact name 687 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 688 continue; 689 } 690 691 DataKind kind = accountType.getKindForMimetype(mimeType); 692 if (kind == null) { 693 Log.e(TAG, "Mimetype not supported for account type " 694 + accountType.getAccountTypeAndDataSet() + ". Ignoring: " + values); 695 continue; 696 } 697 698 ValuesDelta entry = ValuesDelta.fromAfter(values); 699 if (isEmpty(entry, kind)) { 700 continue; 701 } 702 703 ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType); 704 705 if ((kind.typeOverallMax != 1) || GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) { 706 // Check for duplicates 707 boolean addEntry = true; 708 int count = 0; 709 if (entries != null && entries.size() > 0) { 710 for (ValuesDelta delta : entries) { 711 if (!delta.isDelete()) { 712 if (areEqual(delta, values, kind)) { 713 addEntry = false; 714 break; 715 } 716 count++; 717 } 718 } 719 } 720 721 if (kind.typeOverallMax != -1 && count >= kind.typeOverallMax) { 722 Log.e(TAG, "Mimetype allows at most " + kind.typeOverallMax 723 + " entries. Ignoring: " + values); 724 addEntry = false; 725 } 726 727 if (addEntry) { 728 addEntry = adjustType(entry, entries, kind); 729 } 730 731 if (addEntry) { 732 state.addEntry(entry); 733 } 734 } else { 735 // Non-list entries should not be overridden 736 boolean addEntry = true; 737 if (entries != null && entries.size() > 0) { 738 for (ValuesDelta delta : entries) { 739 if (!delta.isDelete() && !isEmpty(delta, kind)) { 740 addEntry = false; 741 break; 742 } 743 } 744 if (addEntry) { 745 for (ValuesDelta delta : entries) { 746 delta.markDeleted(); 747 } 748 } 749 } 750 751 if (addEntry) { 752 addEntry = adjustType(entry, entries, kind); 753 } 754 755 if (addEntry) { 756 state.addEntry(entry); 757 } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)){ 758 // Note is most likely to contain large amounts of text 759 // that we don't want to drop on the ground. 760 for (ValuesDelta delta : entries) { 761 if (!isEmpty(delta, kind)) { 762 delta.put(Note.NOTE, delta.getAsString(Note.NOTE) + "\n" 763 + values.getAsString(Note.NOTE)); 764 break; 765 } 766 } 767 } else { 768 Log.e(TAG, "Will not override mimetype " + mimeType + ". Ignoring: " 769 + values); 770 } 771 } 772 } 773 } 774 775 /** 776 * Checks if the data kind allows addition of another entry (e.g. Exchange only 777 * supports two "work" phone numbers). If not, tries to switch to one of the 778 * unused types. If successful, returns true. 779 */ 780 private static boolean adjustType( 781 ValuesDelta entry, ArrayList<ValuesDelta> entries, DataKind kind) { 782 if (kind.typeColumn == null || kind.typeList == null || kind.typeList.size() == 0) { 783 return true; 784 } 785 786 Integer typeInteger = entry.getAsInteger(kind.typeColumn); 787 int type = typeInteger != null ? typeInteger : kind.typeList.get(0).rawValue; 788 789 if (isTypeAllowed(type, entries, kind)) { 790 entry.put(kind.typeColumn, type); 791 return true; 792 } 793 794 // Specified type is not allowed - choose the first available type that is allowed 795 int size = kind.typeList.size(); 796 for (int i = 0; i < size; i++) { 797 EditType editType = kind.typeList.get(i); 798 if (isTypeAllowed(editType.rawValue, entries, kind)) { 799 entry.put(kind.typeColumn, editType.rawValue); 800 return true; 801 } 802 } 803 804 return false; 805 } 806 807 /** 808 * Checks if a new entry of the specified type can be added to the raw 809 * contact. For example, Exchange only supports two "work" phone numbers, so 810 * addition of a third would not be allowed. 811 */ 812 private static boolean isTypeAllowed(int type, ArrayList<ValuesDelta> entries, DataKind kind) { 813 int max = 0; 814 int size = kind.typeList.size(); 815 for (int i = 0; i < size; i++) { 816 EditType editType = kind.typeList.get(i); 817 if (editType.rawValue == type) { 818 max = editType.specificMax; 819 break; 820 } 821 } 822 823 if (max == 0) { 824 // This type is not allowed at all 825 return false; 826 } 827 828 if (max == -1) { 829 // Unlimited instances of this type are allowed 830 return true; 831 } 832 833 return getEntryCountByType(entries, kind.typeColumn, type) < max; 834 } 835 836 /** 837 * Counts occurrences of the specified type in the supplied entry list. 838 */ 839 private static int getEntryCountByType( 840 ArrayList<ValuesDelta> entries, String typeColumn, int type) { 841 int count = 0; 842 int size = entries.size(); 843 for (int i = 0; i < size; i++) { 844 Integer typeInteger = entries.get(i).getAsInteger(typeColumn); 845 if (typeInteger != null && typeInteger == type) { 846 count++; 847 } 848 } 849 return count; 850 } 851 852 /** 853 * Attempt to parse legacy {@link Insert#IM_PROTOCOL} values, replacing them 854 * with updated values. 855 */ 856 @SuppressWarnings("deprecation") 857 private static void fixupLegacyImType(Bundle bundle) { 858 final String encodedString = bundle.getString(Insert.IM_PROTOCOL); 859 if (encodedString == null) return; 860 861 try { 862 final Object protocol = android.provider.Contacts.ContactMethods 863 .decodeImProtocol(encodedString); 864 if (protocol instanceof Integer) { 865 bundle.putInt(Insert.IM_PROTOCOL, (Integer)protocol); 866 } else { 867 bundle.putString(Insert.IM_PROTOCOL, (String)protocol); 868 } 869 } catch (IllegalArgumentException e) { 870 // Ignore exception when legacy parser fails 871 } 872 } 873 874 /** 875 * Parse a specific entry from the given {@link Bundle} and insert into the 876 * given {@link EntityDelta}. Silently skips the insert when missing value 877 * or no valid {@link EditType} found. 878 * 879 * @param typeExtra {@link Bundle} key that holds the incoming 880 * {@link EditType#rawValue} value. 881 * @param valueExtra {@link Bundle} key that holds the incoming value. 882 * @param valueColumn Column to write value into {@link ValuesDelta}. 883 */ 884 public static ValuesDelta parseExtras(EntityDelta state, DataKind kind, Bundle extras, 885 String typeExtra, String valueExtra, String valueColumn) { 886 final CharSequence value = extras.getCharSequence(valueExtra); 887 888 // Bail early if account type doesn't handle this MIME type 889 if (kind == null) return null; 890 891 // Bail when can't insert type, or value missing 892 final boolean canInsert = EntityModifier.canInsert(state, kind); 893 final boolean validValue = (value != null && TextUtils.isGraphic(value)); 894 if (!validValue || !canInsert) return null; 895 896 // Find exact type when requested, otherwise best available type 897 final boolean hasType = extras.containsKey(typeExtra); 898 final int typeValue = extras.getInt(typeExtra, hasType ? BaseTypes.TYPE_CUSTOM 899 : Integer.MIN_VALUE); 900 final EditType editType = EntityModifier.getBestValidType(state, kind, true, typeValue); 901 902 // Create data row and fill with value 903 final ValuesDelta child = EntityModifier.insertChild(state, kind, editType); 904 child.put(valueColumn, value.toString()); 905 906 if (editType != null && editType.customColumn != null) { 907 // Write down label when custom type picked 908 final String customType = extras.getString(typeExtra); 909 child.put(editType.customColumn, customType); 910 } 911 912 return child; 913 } 914 915 /** 916 * Generic mime types with type support (e.g. TYPE_HOME). 917 * Here, "type support" means if the data kind has CommonColumns#TYPE or not. Data kinds which 918 * have their own migrate methods aren't listed here. 919 */ 920 private static final Set<String> sGenericMimeTypesWithTypeSupport = new HashSet<String>( 921 Arrays.asList(Phone.CONTENT_ITEM_TYPE, 922 Email.CONTENT_ITEM_TYPE, 923 Im.CONTENT_ITEM_TYPE, 924 Nickname.CONTENT_ITEM_TYPE, 925 Website.CONTENT_ITEM_TYPE, 926 Relation.CONTENT_ITEM_TYPE, 927 SipAddress.CONTENT_ITEM_TYPE)); 928 private static final Set<String> sGenericMimeTypesWithoutTypeSupport = new HashSet<String>( 929 Arrays.asList(Organization.CONTENT_ITEM_TYPE, 930 Note.CONTENT_ITEM_TYPE, 931 Photo.CONTENT_ITEM_TYPE, 932 GroupMembership.CONTENT_ITEM_TYPE)); 933 // CommonColumns.TYPE cannot be accessed as it is protected interface, so use 934 // Phone.TYPE instead. 935 private static final String COLUMN_FOR_TYPE = Phone.TYPE; 936 private static final String COLUMN_FOR_LABEL = Phone.LABEL; 937 private static final int TYPE_CUSTOM = Phone.TYPE_CUSTOM; 938 939 /** 940 * Migrates old EntityDelta to newly created one with a new restriction supplied from 941 * newAccountType. 942 * 943 * This is only for account switch during account creation (which must be insert operation). 944 */ 945 public static void migrateStateForNewContact(Context context, 946 EntityDelta oldState, EntityDelta newState, 947 AccountType oldAccountType, AccountType newAccountType) { 948 if (newAccountType == oldAccountType) { 949 // Just copying all data in oldState isn't enough, but we can still rely on a lot of 950 // shortcuts. 951 for (DataKind kind : newAccountType.getSortedDataKinds()) { 952 final String mimeType = kind.mimeType; 953 // The fields with short/long form capability must be treated properly. 954 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 955 migrateStructuredName(context, oldState, newState, kind); 956 } else { 957 List<ValuesDelta> entryList = oldState.getMimeEntries(mimeType); 958 if (entryList != null && !entryList.isEmpty()) { 959 for (ValuesDelta entry : entryList) { 960 ContentValues values = entry.getAfter(); 961 if (values != null) { 962 newState.addEntry(ValuesDelta.fromAfter(values)); 963 } 964 } 965 } 966 } 967 } 968 } else { 969 // Migrate data supported by the new account type. 970 // All the other data inside oldState are silently dropped. 971 for (DataKind kind : newAccountType.getSortedDataKinds()) { 972 if (!kind.editable) continue; 973 final String mimeType = kind.mimeType; 974 if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType) 975 || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType)) { 976 // Ignore pseudo data. 977 continue; 978 } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 979 migrateStructuredName(context, oldState, newState, kind); 980 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) { 981 migratePostal(oldState, newState, kind); 982 } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) { 983 migrateEvent(oldState, newState, kind, null /* default Year */); 984 } else if (sGenericMimeTypesWithoutTypeSupport.contains(mimeType)) { 985 migrateGenericWithoutTypeColumn(oldState, newState, kind); 986 } else if (sGenericMimeTypesWithTypeSupport.contains(mimeType)) { 987 migrateGenericWithTypeColumn(oldState, newState, kind); 988 } else { 989 throw new IllegalStateException("Unexpected editable mime-type: " + mimeType); 990 } 991 } 992 } 993 } 994 995 /** 996 * Checks {@link DataKind#isList} and {@link DataKind#typeOverallMax}, and restricts 997 * the number of entries (ValuesDelta) inside newState. 998 */ 999 private static ArrayList<ValuesDelta> ensureEntryMaxSize(EntityDelta newState, DataKind kind, 1000 ArrayList<ValuesDelta> mimeEntries) { 1001 if (mimeEntries == null) { 1002 return null; 1003 } 1004 1005 final int typeOverallMax = kind.typeOverallMax; 1006 if (typeOverallMax >= 0 && (mimeEntries.size() > typeOverallMax)) { 1007 ArrayList<ValuesDelta> newMimeEntries = new ArrayList<ValuesDelta>(typeOverallMax); 1008 for (int i = 0; i < typeOverallMax; i++) { 1009 newMimeEntries.add(mimeEntries.get(i)); 1010 } 1011 mimeEntries = newMimeEntries; 1012 } 1013 return mimeEntries; 1014 } 1015 1016 /** @hide Public only for testing. */ 1017 public static void migrateStructuredName( 1018 Context context, EntityDelta oldState, EntityDelta newState, DataKind newDataKind) { 1019 final ContentValues values = 1020 oldState.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE).getAfter(); 1021 if (values == null) { 1022 return; 1023 } 1024 1025 boolean supportDisplayName = false; 1026 boolean supportPhoneticFullName = false; 1027 boolean supportPhoneticFamilyName = false; 1028 boolean supportPhoneticMiddleName = false; 1029 boolean supportPhoneticGivenName = false; 1030 for (EditField editField : newDataKind.fieldList) { 1031 if (StructuredName.DISPLAY_NAME.equals(editField.column)) { 1032 supportDisplayName = true; 1033 } 1034 if (DataKind.PSEUDO_COLUMN_PHONETIC_NAME.equals(editField.column)) { 1035 supportPhoneticFullName = true; 1036 } 1037 if (StructuredName.PHONETIC_FAMILY_NAME.equals(editField.column)) { 1038 supportPhoneticFamilyName = true; 1039 } 1040 if (StructuredName.PHONETIC_MIDDLE_NAME.equals(editField.column)) { 1041 supportPhoneticMiddleName = true; 1042 } 1043 if (StructuredName.PHONETIC_GIVEN_NAME.equals(editField.column)) { 1044 supportPhoneticGivenName = true; 1045 } 1046 } 1047 1048 // DISPLAY_NAME <-> PREFIX, GIVEN_NAME, MIDDLE_NAME, FAMILY_NAME, SUFFIX 1049 final String displayName = values.getAsString(StructuredName.DISPLAY_NAME); 1050 if (!TextUtils.isEmpty(displayName)) { 1051 if (!supportDisplayName) { 1052 // Old data has a display name, while the new account doesn't allow it. 1053 NameConverter.displayNameToStructuredName(context, displayName, values); 1054 1055 // We don't want to migrate unseen data which may confuse users after the creation. 1056 values.remove(StructuredName.DISPLAY_NAME); 1057 } 1058 } else { 1059 if (supportDisplayName) { 1060 // Old data does not have display name, while the new account requires it. 1061 values.put(StructuredName.DISPLAY_NAME, 1062 NameConverter.structuredNameToDisplayName(context, values)); 1063 for (String field : NameConverter.STRUCTURED_NAME_FIELDS) { 1064 values.remove(field); 1065 } 1066 } 1067 } 1068 1069 // Phonetic (full) name <-> PHONETIC_FAMILY_NAME, PHONETIC_MIDDLE_NAME, PHONETIC_GIVEN_NAME 1070 final String phoneticFullName = values.getAsString(DataKind.PSEUDO_COLUMN_PHONETIC_NAME); 1071 if (!TextUtils.isEmpty(phoneticFullName)) { 1072 if (!supportPhoneticFullName) { 1073 // Old data has a phonetic (full) name, while the new account doesn't allow it. 1074 final ContentValues tmpValues = 1075 PhoneticNameEditorView.parsePhoneticName(phoneticFullName, null); 1076 values.remove(DataKind.PSEUDO_COLUMN_PHONETIC_NAME); 1077 if (supportPhoneticFamilyName) { 1078 values.put(StructuredName.PHONETIC_FAMILY_NAME, 1079 tmpValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME)); 1080 } else { 1081 values.remove(StructuredName.PHONETIC_FAMILY_NAME); 1082 } 1083 if (supportPhoneticMiddleName) { 1084 values.put(StructuredName.PHONETIC_MIDDLE_NAME, 1085 tmpValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME)); 1086 } else { 1087 values.remove(StructuredName.PHONETIC_MIDDLE_NAME); 1088 } 1089 if (supportPhoneticGivenName) { 1090 values.put(StructuredName.PHONETIC_GIVEN_NAME, 1091 tmpValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME)); 1092 } else { 1093 values.remove(StructuredName.PHONETIC_GIVEN_NAME); 1094 } 1095 } 1096 } else { 1097 if (supportPhoneticFullName) { 1098 // Old data does not have a phonetic (full) name, while the new account requires it. 1099 values.put(DataKind.PSEUDO_COLUMN_PHONETIC_NAME, 1100 PhoneticNameEditorView.buildPhoneticName( 1101 values.getAsString(StructuredName.PHONETIC_FAMILY_NAME), 1102 values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME), 1103 values.getAsString(StructuredName.PHONETIC_GIVEN_NAME))); 1104 } 1105 if (!supportPhoneticFamilyName) { 1106 values.remove(StructuredName.PHONETIC_FAMILY_NAME); 1107 } 1108 if (!supportPhoneticMiddleName) { 1109 values.remove(StructuredName.PHONETIC_MIDDLE_NAME); 1110 } 1111 if (!supportPhoneticGivenName) { 1112 values.remove(StructuredName.PHONETIC_GIVEN_NAME); 1113 } 1114 } 1115 1116 newState.addEntry(ValuesDelta.fromAfter(values)); 1117 } 1118 1119 /** @hide Public only for testing. */ 1120 public static void migratePostal(EntityDelta oldState, EntityDelta newState, 1121 DataKind newDataKind) { 1122 final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind, 1123 oldState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE)); 1124 if (mimeEntries == null || mimeEntries.isEmpty()) { 1125 return; 1126 } 1127 1128 boolean supportFormattedAddress = false; 1129 boolean supportStreet = false; 1130 final String firstColumn = newDataKind.fieldList.get(0).column; 1131 for (EditField editField : newDataKind.fieldList) { 1132 if (StructuredPostal.FORMATTED_ADDRESS.equals(editField.column)) { 1133 supportFormattedAddress = true; 1134 } 1135 if (StructuredPostal.STREET.equals(editField.column)) { 1136 supportStreet = true; 1137 } 1138 } 1139 1140 final Set<Integer> supportedTypes = new HashSet<Integer>(); 1141 if (newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) { 1142 for (EditType editType : newDataKind.typeList) { 1143 supportedTypes.add(editType.rawValue); 1144 } 1145 } 1146 1147 for (ValuesDelta entry : mimeEntries) { 1148 final ContentValues values = entry.getAfter(); 1149 if (values == null) { 1150 continue; 1151 } 1152 final Integer oldType = values.getAsInteger(StructuredPostal.TYPE); 1153 if (!supportedTypes.contains(oldType)) { 1154 int defaultType; 1155 if (newDataKind.defaultValues != null) { 1156 defaultType = newDataKind.defaultValues.getAsInteger(StructuredPostal.TYPE); 1157 } else { 1158 defaultType = newDataKind.typeList.get(0).rawValue; 1159 } 1160 values.put(StructuredPostal.TYPE, defaultType); 1161 if (oldType != null && oldType == StructuredPostal.TYPE_CUSTOM) { 1162 values.remove(StructuredPostal.LABEL); 1163 } 1164 } 1165 1166 final String formattedAddress = values.getAsString(StructuredPostal.FORMATTED_ADDRESS); 1167 if (!TextUtils.isEmpty(formattedAddress)) { 1168 if (!supportFormattedAddress) { 1169 // Old data has a formatted address, while the new account doesn't allow it. 1170 values.remove(StructuredPostal.FORMATTED_ADDRESS); 1171 1172 // Unlike StructuredName we don't have logic to split it, so first 1173 // try to use street field and. If the new account doesn't have one, 1174 // then select first one anyway. 1175 if (supportStreet) { 1176 values.put(StructuredPostal.STREET, formattedAddress); 1177 } else { 1178 values.put(firstColumn, formattedAddress); 1179 } 1180 } 1181 } else { 1182 if (supportFormattedAddress) { 1183 // Old data does not have formatted address, while the new account requires it. 1184 // Unlike StructuredName we don't have logic to join multiple address values. 1185 // Use poor join heuristics for now. 1186 String[] structuredData; 1187 final boolean useJapaneseOrder = 1188 Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage()); 1189 if (useJapaneseOrder) { 1190 structuredData = new String[] { 1191 values.getAsString(StructuredPostal.COUNTRY), 1192 values.getAsString(StructuredPostal.POSTCODE), 1193 values.getAsString(StructuredPostal.REGION), 1194 values.getAsString(StructuredPostal.CITY), 1195 values.getAsString(StructuredPostal.NEIGHBORHOOD), 1196 values.getAsString(StructuredPostal.STREET), 1197 values.getAsString(StructuredPostal.POBOX) }; 1198 } else { 1199 structuredData = new String[] { 1200 values.getAsString(StructuredPostal.POBOX), 1201 values.getAsString(StructuredPostal.STREET), 1202 values.getAsString(StructuredPostal.NEIGHBORHOOD), 1203 values.getAsString(StructuredPostal.CITY), 1204 values.getAsString(StructuredPostal.REGION), 1205 values.getAsString(StructuredPostal.POSTCODE), 1206 values.getAsString(StructuredPostal.COUNTRY) }; 1207 } 1208 final StringBuilder builder = new StringBuilder(); 1209 for (String elem : structuredData) { 1210 if (!TextUtils.isEmpty(elem)) { 1211 builder.append(elem + "\n"); 1212 } 1213 } 1214 values.put(StructuredPostal.FORMATTED_ADDRESS, builder.toString()); 1215 1216 values.remove(StructuredPostal.POBOX); 1217 values.remove(StructuredPostal.STREET); 1218 values.remove(StructuredPostal.NEIGHBORHOOD); 1219 values.remove(StructuredPostal.CITY); 1220 values.remove(StructuredPostal.REGION); 1221 values.remove(StructuredPostal.POSTCODE); 1222 values.remove(StructuredPostal.COUNTRY); 1223 } 1224 } 1225 1226 newState.addEntry(ValuesDelta.fromAfter(values)); 1227 } 1228 } 1229 1230 /** @hide Public only for testing. */ 1231 public static void migrateEvent(EntityDelta oldState, EntityDelta newState, 1232 DataKind newDataKind, Integer defaultYear) { 1233 final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind, 1234 oldState.getMimeEntries(Event.CONTENT_ITEM_TYPE)); 1235 if (mimeEntries == null || mimeEntries.isEmpty()) { 1236 return; 1237 } 1238 1239 final SparseArray<EventEditType> allowedTypes = new SparseArray<EventEditType>(); 1240 for (EditType editType : newDataKind.typeList) { 1241 allowedTypes.put(editType.rawValue, (EventEditType) editType); 1242 } 1243 for (ValuesDelta entry : mimeEntries) { 1244 final ContentValues values = entry.getAfter(); 1245 if (values == null) { 1246 continue; 1247 } 1248 final String dateString = values.getAsString(Event.START_DATE); 1249 final Integer type = values.getAsInteger(Event.TYPE); 1250 if (type != null && (allowedTypes.indexOfKey(type) >= 0) 1251 && !TextUtils.isEmpty(dateString)) { 1252 EventEditType suitableType = allowedTypes.get(type); 1253 1254 final ParsePosition position = new ParsePosition(0); 1255 boolean yearOptional = false; 1256 Date date = DateUtils.DATE_AND_TIME_FORMAT.parse(dateString, position); 1257 if (date == null) { 1258 yearOptional = true; 1259 date = DateUtils.NO_YEAR_DATE_FORMAT.parse(dateString, position); 1260 } 1261 if (date != null) { 1262 if (yearOptional && !suitableType.isYearOptional()) { 1263 // The new EditType doesn't allow optional year. Supply default. 1264 final Calendar calendar = Calendar.getInstance(DateUtils.UTC_TIMEZONE, 1265 Locale.US); 1266 if (defaultYear == null) { 1267 defaultYear = calendar.get(Calendar.YEAR); 1268 } 1269 calendar.setTime(date); 1270 final int month = calendar.get(Calendar.MONTH); 1271 final int day = calendar.get(Calendar.DAY_OF_MONTH); 1272 // Exchange requires 8:00 for birthdays 1273 calendar.set(defaultYear, month, day, 1274 EventFieldEditorView.getDefaultHourForBirthday(), 0, 0); 1275 values.put(Event.START_DATE, 1276 DateUtils.FULL_DATE_FORMAT.format(calendar.getTime())); 1277 } 1278 } 1279 newState.addEntry(ValuesDelta.fromAfter(values)); 1280 } else { 1281 // Just drop it. 1282 } 1283 } 1284 } 1285 1286 /** @hide Public only for testing. */ 1287 public static void migrateGenericWithoutTypeColumn( 1288 EntityDelta oldState, EntityDelta newState, DataKind newDataKind) { 1289 final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind, 1290 oldState.getMimeEntries(newDataKind.mimeType)); 1291 if (mimeEntries == null || mimeEntries.isEmpty()) { 1292 return; 1293 } 1294 1295 for (ValuesDelta entry : mimeEntries) { 1296 ContentValues values = entry.getAfter(); 1297 if (values != null) { 1298 newState.addEntry(ValuesDelta.fromAfter(values)); 1299 } 1300 } 1301 } 1302 1303 /** @hide Public only for testing. */ 1304 public static void migrateGenericWithTypeColumn( 1305 EntityDelta oldState, EntityDelta newState, DataKind newDataKind) { 1306 final ArrayList<ValuesDelta> mimeEntries = oldState.getMimeEntries(newDataKind.mimeType); 1307 if (mimeEntries == null || mimeEntries.isEmpty()) { 1308 return; 1309 } 1310 1311 // Note that type specified with the old account may be invalid with the new account, while 1312 // we want to preserve its data as much as possible. e.g. if a user typed a phone number 1313 // with a type which is valid with an old account but not with a new account, the user 1314 // probably wants to have the number with default type, rather than seeing complete data 1315 // loss. 1316 // 1317 // Specifically, this method works as follows: 1318 // 1. detect defaultType 1319 // 2. prepare constants & variables for iteration 1320 // 3. iterate over mimeEntries: 1321 // 3.1 stop iteration if total number of mimeEntries reached typeOverallMax specified in 1322 // DataKind 1323 // 3.2 replace unallowed types with defaultType 1324 // 3.3 check if the number of entries is below specificMax specified in AccountType 1325 1326 // Here, defaultType can be supplied in two ways 1327 // - via kind.defaultValues 1328 // - via kind.typeList.get(0).rawValue 1329 Integer defaultType = null; 1330 if (newDataKind.defaultValues != null) { 1331 defaultType = newDataKind.defaultValues.getAsInteger(COLUMN_FOR_TYPE); 1332 } 1333 final Set<Integer> allowedTypes = new HashSet<Integer>(); 1334 // key: type, value: the number of entries allowed for the type (specificMax) 1335 final SparseIntArray typeSpecificMaxMap = new SparseIntArray(); 1336 if (defaultType != null) { 1337 allowedTypes.add(defaultType); 1338 typeSpecificMaxMap.put(defaultType, -1); 1339 } 1340 // Note: typeList may be used in different purposes when defaultValues are specified. 1341 // Especially in IM, typeList contains available protocols (e.g. PROTOCOL_GOOGLE_TALK) 1342 // instead of "types" which we want to treate here (e.g. TYPE_HOME). So we don't add 1343 // anything other than defaultType into allowedTypes and typeSpecificMapMax. 1344 if (!Im.CONTENT_ITEM_TYPE.equals(newDataKind.mimeType) && 1345 newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) { 1346 for (EditType editType : newDataKind.typeList) { 1347 allowedTypes.add(editType.rawValue); 1348 typeSpecificMaxMap.put(editType.rawValue, editType.specificMax); 1349 } 1350 if (defaultType == null) { 1351 defaultType = newDataKind.typeList.get(0).rawValue; 1352 } 1353 } 1354 1355 if (defaultType == null) { 1356 Log.w(TAG, "Default type isn't available for mimetype " + newDataKind.mimeType); 1357 } 1358 1359 final int typeOverallMax = newDataKind.typeOverallMax; 1360 1361 // key: type, value: the number of current entries. 1362 final SparseIntArray currentEntryCount = new SparseIntArray(); 1363 int totalCount = 0; 1364 1365 for (ValuesDelta entry : mimeEntries) { 1366 if (typeOverallMax != -1 && totalCount >= typeOverallMax) { 1367 break; 1368 } 1369 1370 final ContentValues values = entry.getAfter(); 1371 if (values == null) { 1372 continue; 1373 } 1374 1375 final Integer oldType = entry.getAsInteger(COLUMN_FOR_TYPE); 1376 final Integer typeForNewAccount; 1377 if (!allowedTypes.contains(oldType)) { 1378 // The new account doesn't support the type. 1379 if (defaultType != null) { 1380 typeForNewAccount = defaultType.intValue(); 1381 values.put(COLUMN_FOR_TYPE, defaultType.intValue()); 1382 if (oldType != null && oldType == TYPE_CUSTOM) { 1383 values.remove(COLUMN_FOR_LABEL); 1384 } 1385 } else { 1386 typeForNewAccount = null; 1387 values.remove(COLUMN_FOR_TYPE); 1388 } 1389 } else { 1390 typeForNewAccount = oldType; 1391 } 1392 if (typeForNewAccount != null) { 1393 final int specificMax = typeSpecificMaxMap.get(typeForNewAccount, 0); 1394 if (specificMax >= 0) { 1395 final int currentCount = currentEntryCount.get(typeForNewAccount, 0); 1396 if (currentCount >= specificMax) { 1397 continue; 1398 } 1399 currentEntryCount.put(typeForNewAccount, currentCount + 1); 1400 } 1401 } 1402 newState.addEntry(ValuesDelta.fromAfter(values)); 1403 totalCount++; 1404 } 1405 } 1406} 1407