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