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