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