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