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