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