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