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