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