blob: b24f87701aa2ec639a843cc1e7ff6929376a4edd [file] [log] [blame]
Yorke Lee2644d942013-10-28 11:05:43 -07001/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.contacts.common.model;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.database.Cursor;
22import android.net.Uri;
23import android.os.Bundle;
24import android.provider.ContactsContract;
25import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
26import android.provider.ContactsContract.CommonDataKinds.Email;
27import android.provider.ContactsContract.CommonDataKinds.Event;
28import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
29import android.provider.ContactsContract.CommonDataKinds.Im;
30import android.provider.ContactsContract.CommonDataKinds.Nickname;
31import android.provider.ContactsContract.CommonDataKinds.Note;
32import android.provider.ContactsContract.CommonDataKinds.Organization;
33import android.provider.ContactsContract.CommonDataKinds.Phone;
34import android.provider.ContactsContract.CommonDataKinds.Photo;
35import android.provider.ContactsContract.CommonDataKinds.Relation;
36import android.provider.ContactsContract.CommonDataKinds.SipAddress;
37import android.provider.ContactsContract.CommonDataKinds.StructuredName;
38import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
39import android.provider.ContactsContract.CommonDataKinds.Website;
40import android.provider.ContactsContract.Data;
41import android.provider.ContactsContract.Intents;
42import android.provider.ContactsContract.Intents.Insert;
43import android.provider.ContactsContract.RawContacts;
44import android.text.TextUtils;
45import android.util.Log;
46import android.util.SparseArray;
47import android.util.SparseIntArray;
48
49import com.android.contacts.common.ContactsUtils;
Yorke Lee2644d942013-10-28 11:05:43 -070050import com.android.contacts.common.model.account.AccountType;
51import com.android.contacts.common.model.account.AccountType.EditField;
52import com.android.contacts.common.model.account.AccountType.EditType;
53import com.android.contacts.common.model.account.AccountType.EventEditType;
54import com.android.contacts.common.model.account.GoogleAccountType;
55import com.android.contacts.common.model.dataitem.DataKind;
56import com.android.contacts.common.model.dataitem.PhoneDataItem;
57import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
Gary Mai7a6daea2016-10-10 15:41:48 -070058import com.android.contacts.common.util.CommonDateUtils;
59import com.android.contacts.common.util.DateUtils;
60import com.android.contacts.common.util.NameConverter;
Yorke Lee2644d942013-10-28 11:05:43 -070061
62import java.text.ParsePosition;
63import java.util.ArrayList;
64import java.util.Arrays;
65import java.util.Calendar;
66import java.util.Date;
67import java.util.HashSet;
68import java.util.Iterator;
69import java.util.List;
70import java.util.Locale;
71import java.util.Set;
72
73/**
74 * Helper methods for modifying an {@link RawContactDelta}, such as inserting
75 * new rows, or enforcing {@link AccountType}.
76 */
77public 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 */
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
97 public static boolean hasValidTypes(RawContactDelta state, DataKind kind) {
98 if (RawContactModifier.hasEditTypes(kind)) {
Tingting Wangd7e0d692015-12-10 14:46:51 -080099 return (getValidTypes(state, kind, null, true, null, true).size() > 0);
Yorke Lee2644d942013-10-28 11:05:43 -0700100 } 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 */
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}.
Yorke Lee2644d942013-10-28 11:05:43 -0700136 *
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)}.
Tingting Wangd7e0d692015-12-10 14:46:51 -0800145 * @param checkOverall If true, check if the overall number of types is under limit.
Yorke Lee2644d942013-10-28 11:05:43 -0700146 */
Tingting Wangd7e0d692015-12-10 14:46:51 -0800147 public static ArrayList<EditType> getValidTypes(RawContactDelta state, DataKind kind,
148 EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount,
149 boolean checkOverall) {
Yorke Lee2644d942013-10-28 11:05:43 -0700150 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
Tingting Wangd7e0d692015-12-10 14:46:51 -0800161 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
Yorke Lee2644d942013-10-28 11:05:43 -0700168 for (EditType type : kind.typeList) {
Yorke Lee2644d942013-10-28 11:05:43 -0700169 final boolean validSpecific = (type.specificMax == -1 ? true : typeCount
Tingting Wangd7e0d692015-12-10 14:46:51 -0800170 .get(type.rawValue) < type.specificMax);
Yorke Lee2644d942013-10-28 11:05:43 -0700171 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 */
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 */
217 public static boolean hasEditTypes(DataKind kind) {
Marcus Hagerottcf7f2952016-09-06 13:49:17 -0700218 return kind != null && kind.typeList != null && kind.typeList.size() > 0;
Yorke Lee2644d942013-10-28 11:05:43 -0700219 }
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 */
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 */
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 */
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 */
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 */
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 */
286 public static EditType getBestValidType(RawContactDelta state, DataKind kind,
287 boolean includeSecondary, int exactValue) {
288 // Shortcut when no types
Jay Shrauner2ebf20e2014-01-10 11:51:45 -0800289 if (kind == null || kind.typeColumn == null) return null;
Yorke Lee2644d942013-10-28 11:05:43 -0700290
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,
Tingting Wangd7e0d692015-12-10 14:46:51 -0800294 typeCount, /*checkOverall=*/ true);
Yorke Lee2644d942013-10-28 11:05:43 -0700295 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 */
330 public static ValuesDelta insertChild(RawContactDelta state, DataKind kind) {
Jay Shrauner2ebf20e2014-01-10 11:51:45 -0800331 // Bail early if invalid kind
332 if (kind == null) return null;
Yorke Lee2644d942013-10-28 11:05:43 -0700333 // 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 */
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 */
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
385 public static boolean hasChanges(RawContactDeltaList set, AccountTypeManager accountTypes) {
Walter Jang9c375532015-10-27 15:19:21 -0700386 return hasChanges(set, accountTypes, /* excludedMimeTypes =*/ null);
387 }
388
389 public static boolean hasChanges(RawContactDeltaList set, AccountTypeManager accountTypes,
390 Set<String> excludedMimeTypes) {
Yorke Lee2644d942013-10-28 11:05:43 -0700391 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);
Walter Jang9c375532015-10-27 15:19:21 -0700400 if (hasChanges(state, type, excludedMimeTypes)) {
Yorke Lee2644d942013-10-28 11:05:43 -0700401 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 */
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
Walter Jang9c375532015-10-27 15:19:21 -0700452 private static boolean hasChanges(RawContactDelta state, AccountType accountType,
453 Set<String> excludedMimeTypes) {
Yorke Lee2644d942013-10-28 11:05:43 -0700454 for (DataKind kind : accountType.getSortedDataKinds()) {
455 final String mimeType = kind.mimeType;
Walter Jang9c375532015-10-27 15:19:21 -0700456 if (excludedMimeTypes != null && excludedMimeTypes.contains(mimeType)) continue;
Yorke Lee2644d942013-10-28 11:05:43 -0700457 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 */
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 */
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 */
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
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
Jay Shrauner007d5302014-01-27 17:08:47 -0800626 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();
Yorke Lee2644d942013-10-28 11:05:43 -0700637 }
Yorke Lee2644d942013-10-28 11:05:43 -0700638 }
639 }
640 }
641
642 final String phoneticName = extras.getString(Insert.PHONETIC_NAME);
643 if (ContactsUtils.isGraphic(phoneticName)) {
Walter Jang773eff62015-05-14 10:50:45 -0700644 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());
Yorke Lee2644d942013-10-28 11:05:43 -0700648 }
649 }
650
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
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
Gary Mai363af602016-09-28 10:01:23 -0700695 // popping up immediately upon entering the ContactEditorFragment
Yorke Lee2644d942013-10-28 11:05:43 -0700696 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 */
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 */
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 */
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")
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 */
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 */
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;
Gary Mai7a6daea2016-10-10 15:41:48 -0700989 if (DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType) ||
990 DataKind.PSEUDO_MIME_TYPE_NAME.equals(mimeType)) {
Yorke Lee2644d942013-10-28 11:05:43 -0700991 // 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 */
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. */
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
Yorke Lee2644d942013-10-28 11:05:43 -07001041 boolean supportPhoneticFullName = false;
1042 boolean supportPhoneticFamilyName = false;
1043 boolean supportPhoneticMiddleName = false;
1044 boolean supportPhoneticGivenName = false;
1045 for (EditField editField : newDataKind.fieldList) {
Yorke Lee2644d942013-10-28 11:05:43 -07001046 if (DataKind.PSEUDO_COLUMN_PHONETIC_NAME.equals(editField.column)) {
1047 supportPhoneticFullName = true;
1048 }
1049 if (StructuredName.PHONETIC_FAMILY_NAME.equals(editField.column)) {
1050 supportPhoneticFamilyName = true;
1051 }
1052 if (StructuredName.PHONETIC_MIDDLE_NAME.equals(editField.column)) {
1053 supportPhoneticMiddleName = true;
1054 }
1055 if (StructuredName.PHONETIC_GIVEN_NAME.equals(editField.column)) {
1056 supportPhoneticGivenName = true;
1057 }
1058 }
1059
Yorke Lee2644d942013-10-28 11:05:43 -07001060
1061 // Phonetic (full) name <-> PHONETIC_FAMILY_NAME, PHONETIC_MIDDLE_NAME, PHONETIC_GIVEN_NAME
1062 final String phoneticFullName = values.getAsString(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
1063 if (!TextUtils.isEmpty(phoneticFullName)) {
1064 if (!supportPhoneticFullName) {
1065 // Old data has a phonetic (full) name, while the new account doesn't allow it.
1066 final StructuredNameDataItem tmpItem =
1067 NameConverter.parsePhoneticName(phoneticFullName, null);
1068 values.remove(DataKind.PSEUDO_COLUMN_PHONETIC_NAME);
1069 if (supportPhoneticFamilyName) {
1070 values.put(StructuredName.PHONETIC_FAMILY_NAME,
1071 tmpItem.getPhoneticFamilyName());
1072 } else {
1073 values.remove(StructuredName.PHONETIC_FAMILY_NAME);
1074 }
1075 if (supportPhoneticMiddleName) {
1076 values.put(StructuredName.PHONETIC_MIDDLE_NAME,
1077 tmpItem.getPhoneticMiddleName());
1078 } else {
1079 values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
1080 }
1081 if (supportPhoneticGivenName) {
1082 values.put(StructuredName.PHONETIC_GIVEN_NAME,
1083 tmpItem.getPhoneticGivenName());
1084 } else {
1085 values.remove(StructuredName.PHONETIC_GIVEN_NAME);
1086 }
1087 }
1088 } else {
1089 if (supportPhoneticFullName) {
1090 // Old data does not have a phonetic (full) name, while the new account requires it.
1091 values.put(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
1092 NameConverter.buildPhoneticName(
1093 values.getAsString(StructuredName.PHONETIC_FAMILY_NAME),
1094 values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME),
1095 values.getAsString(StructuredName.PHONETIC_GIVEN_NAME)));
1096 }
1097 if (!supportPhoneticFamilyName) {
1098 values.remove(StructuredName.PHONETIC_FAMILY_NAME);
1099 }
1100 if (!supportPhoneticMiddleName) {
1101 values.remove(StructuredName.PHONETIC_MIDDLE_NAME);
1102 }
1103 if (!supportPhoneticGivenName) {
1104 values.remove(StructuredName.PHONETIC_GIVEN_NAME);
1105 }
1106 }
1107
1108 newState.addEntry(ValuesDelta.fromAfter(values));
1109 }
1110
1111 /** @hide Public only for testing. */
1112 public static void migratePostal(RawContactDelta oldState, RawContactDelta newState,
1113 DataKind newDataKind) {
1114 final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
1115 oldState.getMimeEntries(StructuredPostal.CONTENT_ITEM_TYPE));
1116 if (mimeEntries == null || mimeEntries.isEmpty()) {
1117 return;
1118 }
1119
1120 boolean supportFormattedAddress = false;
1121 boolean supportStreet = false;
1122 final String firstColumn = newDataKind.fieldList.get(0).column;
1123 for (EditField editField : newDataKind.fieldList) {
1124 if (StructuredPostal.FORMATTED_ADDRESS.equals(editField.column)) {
1125 supportFormattedAddress = true;
1126 }
1127 if (StructuredPostal.STREET.equals(editField.column)) {
1128 supportStreet = true;
1129 }
1130 }
1131
1132 final Set<Integer> supportedTypes = new HashSet<Integer>();
1133 if (newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
1134 for (EditType editType : newDataKind.typeList) {
1135 supportedTypes.add(editType.rawValue);
1136 }
1137 }
1138
1139 for (ValuesDelta entry : mimeEntries) {
1140 final ContentValues values = entry.getAfter();
1141 if (values == null) {
1142 continue;
1143 }
1144 final Integer oldType = values.getAsInteger(StructuredPostal.TYPE);
1145 if (!supportedTypes.contains(oldType)) {
1146 int defaultType;
1147 if (newDataKind.defaultValues != null) {
1148 defaultType = newDataKind.defaultValues.getAsInteger(StructuredPostal.TYPE);
1149 } else {
1150 defaultType = newDataKind.typeList.get(0).rawValue;
1151 }
1152 values.put(StructuredPostal.TYPE, defaultType);
1153 if (oldType != null && oldType == StructuredPostal.TYPE_CUSTOM) {
1154 values.remove(StructuredPostal.LABEL);
1155 }
1156 }
1157
1158 final String formattedAddress = values.getAsString(StructuredPostal.FORMATTED_ADDRESS);
1159 if (!TextUtils.isEmpty(formattedAddress)) {
1160 if (!supportFormattedAddress) {
1161 // Old data has a formatted address, while the new account doesn't allow it.
1162 values.remove(StructuredPostal.FORMATTED_ADDRESS);
1163
1164 // Unlike StructuredName we don't have logic to split it, so first
1165 // try to use street field and. If the new account doesn't have one,
1166 // then select first one anyway.
1167 if (supportStreet) {
1168 values.put(StructuredPostal.STREET, formattedAddress);
1169 } else {
1170 values.put(firstColumn, formattedAddress);
1171 }
1172 }
1173 } else {
1174 if (supportFormattedAddress) {
1175 // Old data does not have formatted address, while the new account requires it.
1176 // Unlike StructuredName we don't have logic to join multiple address values.
1177 // Use poor join heuristics for now.
1178 String[] structuredData;
1179 final boolean useJapaneseOrder =
1180 Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
1181 if (useJapaneseOrder) {
1182 structuredData = new String[] {
1183 values.getAsString(StructuredPostal.COUNTRY),
1184 values.getAsString(StructuredPostal.POSTCODE),
1185 values.getAsString(StructuredPostal.REGION),
1186 values.getAsString(StructuredPostal.CITY),
1187 values.getAsString(StructuredPostal.NEIGHBORHOOD),
1188 values.getAsString(StructuredPostal.STREET),
1189 values.getAsString(StructuredPostal.POBOX) };
1190 } else {
1191 structuredData = new String[] {
1192 values.getAsString(StructuredPostal.POBOX),
1193 values.getAsString(StructuredPostal.STREET),
1194 values.getAsString(StructuredPostal.NEIGHBORHOOD),
1195 values.getAsString(StructuredPostal.CITY),
1196 values.getAsString(StructuredPostal.REGION),
1197 values.getAsString(StructuredPostal.POSTCODE),
1198 values.getAsString(StructuredPostal.COUNTRY) };
1199 }
1200 final StringBuilder builder = new StringBuilder();
1201 for (String elem : structuredData) {
1202 if (!TextUtils.isEmpty(elem)) {
1203 builder.append(elem + "\n");
1204 }
1205 }
1206 values.put(StructuredPostal.FORMATTED_ADDRESS, builder.toString());
1207
1208 values.remove(StructuredPostal.POBOX);
1209 values.remove(StructuredPostal.STREET);
1210 values.remove(StructuredPostal.NEIGHBORHOOD);
1211 values.remove(StructuredPostal.CITY);
1212 values.remove(StructuredPostal.REGION);
1213 values.remove(StructuredPostal.POSTCODE);
1214 values.remove(StructuredPostal.COUNTRY);
1215 }
1216 }
1217
1218 newState.addEntry(ValuesDelta.fromAfter(values));
1219 }
1220 }
1221
1222 /** @hide Public only for testing. */
1223 public static void migrateEvent(RawContactDelta oldState, RawContactDelta newState,
1224 DataKind newDataKind, Integer defaultYear) {
1225 final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
1226 oldState.getMimeEntries(Event.CONTENT_ITEM_TYPE));
1227 if (mimeEntries == null || mimeEntries.isEmpty()) {
1228 return;
1229 }
1230
1231 final SparseArray<EventEditType> allowedTypes = new SparseArray<EventEditType>();
1232 for (EditType editType : newDataKind.typeList) {
1233 allowedTypes.put(editType.rawValue, (EventEditType) editType);
1234 }
1235 for (ValuesDelta entry : mimeEntries) {
1236 final ContentValues values = entry.getAfter();
1237 if (values == null) {
1238 continue;
1239 }
1240 final String dateString = values.getAsString(Event.START_DATE);
1241 final Integer type = values.getAsInteger(Event.TYPE);
1242 if (type != null && (allowedTypes.indexOfKey(type) >= 0)
1243 && !TextUtils.isEmpty(dateString)) {
1244 EventEditType suitableType = allowedTypes.get(type);
1245
1246 final ParsePosition position = new ParsePosition(0);
1247 boolean yearOptional = false;
1248 Date date = CommonDateUtils.DATE_AND_TIME_FORMAT.parse(dateString, position);
1249 if (date == null) {
1250 yearOptional = true;
1251 date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(dateString, position);
1252 }
1253 if (date != null) {
1254 if (yearOptional && !suitableType.isYearOptional()) {
1255 // The new EditType doesn't allow optional year. Supply default.
1256 final Calendar calendar = Calendar.getInstance(DateUtils.UTC_TIMEZONE,
1257 Locale.US);
1258 if (defaultYear == null) {
1259 defaultYear = calendar.get(Calendar.YEAR);
1260 }
1261 calendar.setTime(date);
1262 final int month = calendar.get(Calendar.MONTH);
1263 final int day = calendar.get(Calendar.DAY_OF_MONTH);
1264 // Exchange requires 8:00 for birthdays
1265 calendar.set(defaultYear, month, day,
1266 CommonDateUtils.DEFAULT_HOUR, 0, 0);
1267 values.put(Event.START_DATE,
1268 CommonDateUtils.FULL_DATE_FORMAT.format(calendar.getTime()));
1269 }
1270 }
1271 newState.addEntry(ValuesDelta.fromAfter(values));
1272 } else {
1273 // Just drop it.
1274 }
1275 }
1276 }
1277
1278 /** @hide Public only for testing. */
1279 public static void migrateGenericWithoutTypeColumn(
1280 RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
1281 final ArrayList<ValuesDelta> mimeEntries = ensureEntryMaxSize(newState, newDataKind,
1282 oldState.getMimeEntries(newDataKind.mimeType));
1283 if (mimeEntries == null || mimeEntries.isEmpty()) {
1284 return;
1285 }
1286
1287 for (ValuesDelta entry : mimeEntries) {
1288 ContentValues values = entry.getAfter();
1289 if (values != null) {
1290 newState.addEntry(ValuesDelta.fromAfter(values));
1291 }
1292 }
1293 }
1294
1295 /** @hide Public only for testing. */
1296 public static void migrateGenericWithTypeColumn(
1297 RawContactDelta oldState, RawContactDelta newState, DataKind newDataKind) {
1298 final ArrayList<ValuesDelta> mimeEntries = oldState.getMimeEntries(newDataKind.mimeType);
1299 if (mimeEntries == null || mimeEntries.isEmpty()) {
1300 return;
1301 }
1302
1303 // Note that type specified with the old account may be invalid with the new account, while
1304 // we want to preserve its data as much as possible. e.g. if a user typed a phone number
1305 // with a type which is valid with an old account but not with a new account, the user
1306 // probably wants to have the number with default type, rather than seeing complete data
1307 // loss.
1308 //
1309 // Specifically, this method works as follows:
1310 // 1. detect defaultType
1311 // 2. prepare constants & variables for iteration
1312 // 3. iterate over mimeEntries:
1313 // 3.1 stop iteration if total number of mimeEntries reached typeOverallMax specified in
1314 // DataKind
1315 // 3.2 replace unallowed types with defaultType
1316 // 3.3 check if the number of entries is below specificMax specified in AccountType
1317
1318 // Here, defaultType can be supplied in two ways
1319 // - via kind.defaultValues
1320 // - via kind.typeList.get(0).rawValue
1321 Integer defaultType = null;
1322 if (newDataKind.defaultValues != null) {
1323 defaultType = newDataKind.defaultValues.getAsInteger(COLUMN_FOR_TYPE);
1324 }
1325 final Set<Integer> allowedTypes = new HashSet<Integer>();
1326 // key: type, value: the number of entries allowed for the type (specificMax)
1327 final SparseIntArray typeSpecificMaxMap = new SparseIntArray();
1328 if (defaultType != null) {
1329 allowedTypes.add(defaultType);
1330 typeSpecificMaxMap.put(defaultType, -1);
1331 }
1332 // Note: typeList may be used in different purposes when defaultValues are specified.
1333 // Especially in IM, typeList contains available protocols (e.g. PROTOCOL_GOOGLE_TALK)
1334 // instead of "types" which we want to treate here (e.g. TYPE_HOME). So we don't add
1335 // anything other than defaultType into allowedTypes and typeSpecificMapMax.
1336 if (!Im.CONTENT_ITEM_TYPE.equals(newDataKind.mimeType) &&
1337 newDataKind.typeList != null && !newDataKind.typeList.isEmpty()) {
1338 for (EditType editType : newDataKind.typeList) {
1339 allowedTypes.add(editType.rawValue);
1340 typeSpecificMaxMap.put(editType.rawValue, editType.specificMax);
1341 }
1342 if (defaultType == null) {
1343 defaultType = newDataKind.typeList.get(0).rawValue;
1344 }
1345 }
1346
1347 if (defaultType == null) {
1348 Log.w(TAG, "Default type isn't available for mimetype " + newDataKind.mimeType);
1349 }
1350
1351 final int typeOverallMax = newDataKind.typeOverallMax;
1352
1353 // key: type, value: the number of current entries.
1354 final SparseIntArray currentEntryCount = new SparseIntArray();
1355 int totalCount = 0;
1356
1357 for (ValuesDelta entry : mimeEntries) {
1358 if (typeOverallMax != -1 && totalCount >= typeOverallMax) {
1359 break;
1360 }
1361
1362 final ContentValues values = entry.getAfter();
1363 if (values == null) {
1364 continue;
1365 }
1366
1367 final Integer oldType = entry.getAsInteger(COLUMN_FOR_TYPE);
1368 final Integer typeForNewAccount;
1369 if (!allowedTypes.contains(oldType)) {
1370 // The new account doesn't support the type.
1371 if (defaultType != null) {
1372 typeForNewAccount = defaultType.intValue();
1373 values.put(COLUMN_FOR_TYPE, defaultType.intValue());
1374 if (oldType != null && oldType == TYPE_CUSTOM) {
1375 values.remove(COLUMN_FOR_LABEL);
1376 }
1377 } else {
1378 typeForNewAccount = null;
1379 values.remove(COLUMN_FOR_TYPE);
1380 }
1381 } else {
1382 typeForNewAccount = oldType;
1383 }
1384 if (typeForNewAccount != null) {
1385 final int specificMax = typeSpecificMaxMap.get(typeForNewAccount, 0);
1386 if (specificMax >= 0) {
1387 final int currentCount = currentEntryCount.get(typeForNewAccount, 0);
1388 if (currentCount >= specificMax) {
1389 continue;
1390 }
1391 currentEntryCount.put(typeForNewAccount, currentCount + 1);
1392 }
1393 }
1394 newState.addEntry(ValuesDelta.fromAfter(values));
1395 totalCount++;
1396 }
1397 }
1398}