• 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.model;
18 
19 import com.android.contacts.ContactsUtils;
20 import com.android.contacts.model.ContactsSource.DataKind;
21 import com.android.contacts.model.ContactsSource.EditField;
22 import com.android.contacts.model.ContactsSource.EditType;
23 import com.android.contacts.model.EntityDelta.ValuesDelta;
24 import com.google.android.collect.Lists;
25 
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.database.Cursor;
29 import android.os.Bundle;
30 import android.provider.ContactsContract.Data;
31 import android.provider.ContactsContract.Intents;
32 import android.provider.ContactsContract.RawContacts;
33 import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
34 import android.provider.ContactsContract.CommonDataKinds.Email;
35 import android.provider.ContactsContract.CommonDataKinds.Im;
36 import android.provider.ContactsContract.CommonDataKinds.Phone;
37 import android.provider.ContactsContract.CommonDataKinds.Photo;
38 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
39 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
40 import android.provider.ContactsContract.Intents.Insert;
41 import android.provider.ContactsContract;
42 import android.text.TextUtils;
43 import android.util.Log;
44 import android.util.SparseIntArray;
45 
46 import java.util.ArrayList;
47 import java.util.Iterator;
48 import java.util.List;
49 
50 /**
51  * Helper methods for modifying an {@link EntityDelta}, such as inserting
52  * new rows, or enforcing {@link ContactsSource}.
53  */
54 public class EntityModifier {
55     private static final String TAG = "EntityModifier";
56 
57     /**
58      * For the given {@link EntityDelta}, determine if the given
59      * {@link DataKind} could be inserted under specific
60      * {@link ContactsSource}.
61      */
canInsert(EntityDelta state, DataKind kind)62     public static boolean canInsert(EntityDelta state, DataKind kind) {
63         // Insert possible when have valid types and under overall maximum
64         final int visibleCount = state.getMimeEntriesCount(kind.mimeType, true);
65         final boolean validTypes = hasValidTypes(state, kind);
66         final boolean validOverall = (kind.typeOverallMax == -1)
67                 || (visibleCount < kind.typeOverallMax);
68         return (validTypes && validOverall);
69     }
70 
hasValidTypes(EntityDelta state, DataKind kind)71     public static boolean hasValidTypes(EntityDelta state, DataKind kind) {
72         if (EntityModifier.hasEditTypes(kind)) {
73             return (getValidTypes(state, kind).size() > 0);
74         } else {
75             return true;
76         }
77     }
78 
79     /**
80      * Ensure that at least one of the given {@link DataKind} exists in the
81      * given {@link EntityDelta} state, and try creating one if none exist.
82      */
ensureKindExists(EntityDelta state, ContactsSource source, String mimeType)83     public static void ensureKindExists(EntityDelta state, ContactsSource source, String mimeType) {
84         final DataKind kind = source.getKindForMimetype(mimeType);
85         final boolean hasChild = state.getMimeEntriesCount(mimeType, true) > 0;
86 
87         if (!hasChild && kind != null) {
88             // Create child when none exists and valid kind
89             final ValuesDelta child = insertChild(state, kind);
90             if (kind.mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
91                 child.setFromTemplate(true);
92             }
93         }
94     }
95 
96     /**
97      * For the given {@link EntityDelta} and {@link DataKind}, return the
98      * list possible {@link EditType} options available based on
99      * {@link ContactsSource}.
100      */
getValidTypes(EntityDelta state, DataKind kind)101     public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind) {
102         return getValidTypes(state, kind, null, true, null);
103     }
104 
105     /**
106      * For the given {@link EntityDelta} and {@link DataKind}, return the
107      * list possible {@link EditType} options available based on
108      * {@link ContactsSource}.
109      *
110      * @param forceInclude Always include this {@link EditType} in the returned
111      *            list, even when an otherwise-invalid choice. This is useful
112      *            when showing a dialog that includes the current type.
113      */
getValidTypes(EntityDelta state, DataKind kind, EditType forceInclude)114     public static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind,
115             EditType forceInclude) {
116         return getValidTypes(state, kind, forceInclude, true, null);
117     }
118 
119     /**
120      * For the given {@link EntityDelta} and {@link DataKind}, return the
121      * list possible {@link EditType} options available based on
122      * {@link ContactsSource}.
123      *
124      * @param forceInclude Always include this {@link EditType} in the returned
125      *            list, even when an otherwise-invalid choice. This is useful
126      *            when showing a dialog that includes the current type.
127      * @param includeSecondary If true, include any valid types marked as
128      *            {@link EditType#secondary}.
129      * @param typeCount When provided, will be used for the frequency count of
130      *            each {@link EditType}, otherwise built using
131      *            {@link #getTypeFrequencies(EntityDelta, DataKind)}.
132      */
getValidTypes(EntityDelta state, DataKind kind, EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount)133     private static ArrayList<EditType> getValidTypes(EntityDelta state, DataKind kind,
134             EditType forceInclude, boolean includeSecondary, SparseIntArray typeCount) {
135         final ArrayList<EditType> validTypes = Lists.newArrayList();
136 
137         // Bail early if no types provided
138         if (!hasEditTypes(kind)) return validTypes;
139 
140         if (typeCount == null) {
141             // Build frequency counts if not provided
142             typeCount = getTypeFrequencies(state, kind);
143         }
144 
145         // Build list of valid types
146         final int overallCount = typeCount.get(FREQUENCY_TOTAL);
147         for (EditType type : kind.typeList) {
148             final boolean validOverall = (kind.typeOverallMax == -1 ? true
149                     : overallCount < kind.typeOverallMax);
150             final boolean validSpecific = (type.specificMax == -1 ? true : typeCount
151                     .get(type.rawValue) < type.specificMax);
152             final boolean validSecondary = (includeSecondary ? true : !type.secondary);
153             final boolean forcedInclude = type.equals(forceInclude);
154             if (forcedInclude || (validOverall && validSpecific && validSecondary)) {
155                 // Type is valid when no limit, under limit, or forced include
156                 validTypes.add(type);
157             }
158         }
159 
160         return validTypes;
161     }
162 
163     private static final int FREQUENCY_TOTAL = Integer.MIN_VALUE;
164 
165     /**
166      * Count up the frequency that each {@link EditType} appears in the given
167      * {@link EntityDelta}. The returned {@link SparseIntArray} maps from
168      * {@link EditType#rawValue} to counts, with the total overall count stored
169      * as {@link #FREQUENCY_TOTAL}.
170      */
getTypeFrequencies(EntityDelta state, DataKind kind)171     private static SparseIntArray getTypeFrequencies(EntityDelta state, DataKind kind) {
172         final SparseIntArray typeCount = new SparseIntArray();
173 
174         // Find all entries for this kind, bailing early if none found
175         final List<ValuesDelta> mimeEntries = state.getMimeEntries(kind.mimeType);
176         if (mimeEntries == null) return typeCount;
177 
178         int totalCount = 0;
179         for (ValuesDelta entry : mimeEntries) {
180             // Only count visible entries
181             if (!entry.isVisible()) continue;
182             totalCount++;
183 
184             final EditType type = getCurrentType(entry, kind);
185             if (type != null) {
186                 final int count = typeCount.get(type.rawValue);
187                 typeCount.put(type.rawValue, count + 1);
188             }
189         }
190         typeCount.put(FREQUENCY_TOTAL, totalCount);
191         return typeCount;
192     }
193 
194     /**
195      * Check if the given {@link DataKind} has multiple types that should be
196      * displayed for users to pick.
197      */
hasEditTypes(DataKind kind)198     public static boolean hasEditTypes(DataKind kind) {
199         return kind.typeList != null && kind.typeList.size() > 0;
200     }
201 
202     /**
203      * Find the {@link EditType} that describes the given
204      * {@link ValuesDelta} row, assuming the given {@link DataKind} dictates
205      * the possible types.
206      */
getCurrentType(ValuesDelta entry, DataKind kind)207     public static EditType getCurrentType(ValuesDelta entry, DataKind kind) {
208         final Long rawValue = entry.getAsLong(kind.typeColumn);
209         if (rawValue == null) return null;
210         return getType(kind, rawValue.intValue());
211     }
212 
213     /**
214      * Find the {@link EditType} that describes the given {@link ContentValues} row,
215      * assuming the given {@link DataKind} dictates the possible types.
216      */
getCurrentType(ContentValues entry, DataKind kind)217     public static EditType getCurrentType(ContentValues entry, DataKind kind) {
218         if (kind.typeColumn == null) return null;
219         final Integer rawValue = entry.getAsInteger(kind.typeColumn);
220         if (rawValue == null) return null;
221         return getType(kind, rawValue);
222     }
223 
224     /**
225      * Find the {@link EditType} that describes the given {@link Cursor} row,
226      * assuming the given {@link DataKind} dictates the possible types.
227      */
getCurrentType(Cursor cursor, DataKind kind)228     public static EditType getCurrentType(Cursor cursor, DataKind kind) {
229         if (kind.typeColumn == null) return null;
230         final int index = cursor.getColumnIndex(kind.typeColumn);
231         if (index == -1) return null;
232         final int rawValue = cursor.getInt(index);
233         return getType(kind, rawValue);
234     }
235 
236     /**
237      * Find the {@link EditType} with the given {@link EditType#rawValue}.
238      */
getType(DataKind kind, int rawValue)239     public static EditType getType(DataKind kind, int rawValue) {
240         for (EditType type : kind.typeList) {
241             if (type.rawValue == rawValue) {
242                 return type;
243             }
244         }
245         return null;
246     }
247 
248     /**
249      * Return the precedence for the the given {@link EditType#rawValue}, where
250      * lower numbers are higher precedence.
251      */
getTypePrecedence(DataKind kind, int rawValue)252     public static int getTypePrecedence(DataKind kind, int rawValue) {
253         for (int i = 0; i < kind.typeList.size(); i++) {
254             final EditType type = kind.typeList.get(i);
255             if (type.rawValue == rawValue) {
256                 return i;
257             }
258         }
259         return Integer.MAX_VALUE;
260     }
261 
262     /**
263      * Find the best {@link EditType} for a potential insert. The "best" is the
264      * first primary type that doesn't already exist. When all valid types
265      * exist, we pick the last valid option.
266      */
getBestValidType(EntityDelta state, DataKind kind, boolean includeSecondary, int exactValue)267     public static EditType getBestValidType(EntityDelta state, DataKind kind,
268             boolean includeSecondary, int exactValue) {
269         // Shortcut when no types
270         if (kind.typeColumn == null) return null;
271 
272         // Find type counts and valid primary types, bail if none
273         final SparseIntArray typeCount = getTypeFrequencies(state, kind);
274         final ArrayList<EditType> validTypes = getValidTypes(state, kind, null, includeSecondary,
275                 typeCount);
276         if (validTypes.size() == 0) return null;
277 
278         // Keep track of the last valid type
279         final EditType lastType = validTypes.get(validTypes.size() - 1);
280 
281         // Remove any types that already exist
282         Iterator<EditType> iterator = validTypes.iterator();
283         while (iterator.hasNext()) {
284             final EditType type = iterator.next();
285             final int count = typeCount.get(type.rawValue);
286 
287             if (exactValue == type.rawValue) {
288                 // Found exact value match
289                 return type;
290             }
291 
292             if (count > 0) {
293                 // Type already appears, so don't consider
294                 iterator.remove();
295             }
296         }
297 
298         // Use the best remaining, otherwise the last valid
299         if (validTypes.size() > 0) {
300             return validTypes.get(0);
301         } else {
302             return lastType;
303         }
304     }
305 
306     /**
307      * Insert a new child of kind {@link DataKind} into the given
308      * {@link EntityDelta}. Tries using the best {@link EditType} found using
309      * {@link #getBestValidType(EntityDelta, DataKind, boolean, int)}.
310      */
insertChild(EntityDelta state, DataKind kind)311     public static ValuesDelta insertChild(EntityDelta state, DataKind kind) {
312         // First try finding a valid primary
313         EditType bestType = getBestValidType(state, kind, false, Integer.MIN_VALUE);
314         if (bestType == null) {
315             // No valid primary found, so expand search to secondary
316             bestType = getBestValidType(state, kind, true, Integer.MIN_VALUE);
317         }
318         return insertChild(state, kind, bestType);
319     }
320 
321     /**
322      * Insert a new child of kind {@link DataKind} into the given
323      * {@link EntityDelta}, marked with the given {@link EditType}.
324      */
insertChild(EntityDelta state, DataKind kind, EditType type)325     public static ValuesDelta insertChild(EntityDelta state, DataKind kind, EditType type) {
326         // Bail early if invalid kind
327         if (kind == null) return null;
328         final ContentValues after = new ContentValues();
329 
330         // Our parent CONTACT_ID is provided later
331         after.put(Data.MIMETYPE, kind.mimeType);
332 
333         // Fill-in with any requested default values
334         if (kind.defaultValues != null) {
335             after.putAll(kind.defaultValues);
336         }
337 
338         if (kind.typeColumn != null && type != null) {
339             // Set type, if provided
340             after.put(kind.typeColumn, type.rawValue);
341         }
342 
343         final ValuesDelta child = ValuesDelta.fromAfter(after);
344 	state.addEntry(child);
345         return child;
346     }
347 
348     /**
349      * Processing to trim any empty {@link ValuesDelta} and {@link EntityDelta}
350      * from the given {@link EntitySet}, assuming the given {@link Sources}
351      * dictates the structure for various fields. This method ignores rows not
352      * described by the {@link ContactsSource}.
353      */
trimEmpty(EntitySet set, Sources sources)354     public static void trimEmpty(EntitySet set, Sources sources) {
355         for (EntityDelta state : set) {
356             final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
357             final ContactsSource source = sources.getInflatedSource(accountType,
358                     ContactsSource.LEVEL_MIMETYPES);
359             trimEmpty(state, source);
360         }
361     }
362 
363     /**
364      * Processing to trim any empty {@link ValuesDelta} rows from the given
365      * {@link EntityDelta}, assuming the given {@link ContactsSource} dictates
366      * the structure for various fields. This method ignores rows not described
367      * by the {@link ContactsSource}.
368      */
trimEmpty(EntityDelta state, ContactsSource source)369     public static void trimEmpty(EntityDelta state, ContactsSource source) {
370         boolean hasValues = false;
371 
372         // Walk through entries for each well-known kind
373         for (DataKind kind : source.getSortedDataKinds()) {
374             final String mimeType = kind.mimeType;
375             final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
376             if (entries == null) continue;
377 
378             for (ValuesDelta entry : entries) {
379                 // Skip any values that haven't been touched
380                 final boolean touched = entry.isInsert() || entry.isUpdate();
381                 if (!touched) {
382                     hasValues = true;
383                     continue;
384                 }
385 
386                 // Test and remove this row if empty and it isn't a photo from google
387                 final boolean isGoogleSource = TextUtils.equals(GoogleSource.ACCOUNT_TYPE,
388                         state.getValues().getAsString(RawContacts.ACCOUNT_TYPE));
389                 final boolean isPhoto = TextUtils.equals(Photo.CONTENT_ITEM_TYPE, kind.mimeType);
390                 final boolean isGooglePhoto = isPhoto && isGoogleSource;
391 
392                 if (EntityModifier.isEmpty(entry, kind) && !isGooglePhoto) {
393                     // TODO: remove this verbose logging
394                     Log.w(TAG, "Trimming: " + entry.toString());
395                     entry.markDeleted();
396                 } else if (!entry.isFromTemplate()) {
397                     hasValues = true;
398                 }
399             }
400         }
401         if (!hasValues) {
402             // Trim overall entity if no children exist
403             state.markDeleted();
404         }
405     }
406 
407     /**
408      * Test if the given {@link ValuesDelta} would be considered "empty" in
409      * terms of {@link DataKind#fieldList}.
410      */
isEmpty(ValuesDelta values, DataKind kind)411     public static boolean isEmpty(ValuesDelta values, DataKind kind) {
412         // No defined fields mean this row is always empty
413         if (kind.fieldList == null) return true;
414 
415         boolean hasValues = false;
416         for (EditField field : kind.fieldList) {
417             // If any field has values, we're not empty
418             final String value = values.getAsString(field.column);
419             if (ContactsUtils.isGraphic(value)) {
420                 hasValues = true;
421             }
422         }
423 
424         return !hasValues;
425     }
426 
427     /**
428      * Parse the given {@link Bundle} into the given {@link EntityDelta} state,
429      * assuming the extras defined through {@link Intents}.
430      */
parseExtras(Context context, ContactsSource source, EntityDelta state, Bundle extras)431     public static void parseExtras(Context context, ContactsSource source, EntityDelta state,
432             Bundle extras) {
433         if (extras == null || extras.size() == 0) {
434             // Bail early if no useful data
435             return;
436         }
437 
438         {
439             // StructuredName
440             EntityModifier.ensureKindExists(state, source, StructuredName.CONTENT_ITEM_TYPE);
441             final ValuesDelta child = state.getPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
442 
443             final String name = extras.getString(Insert.NAME);
444             if (ContactsUtils.isGraphic(name)) {
445                 child.put(StructuredName.GIVEN_NAME, name);
446             }
447 
448             final String phoneticName = extras.getString(Insert.PHONETIC_NAME);
449             if (ContactsUtils.isGraphic(phoneticName)) {
450                 child.put(StructuredName.PHONETIC_GIVEN_NAME, phoneticName);
451             }
452         }
453 
454         {
455             // StructuredPostal
456             final DataKind kind = source.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
457             parseExtras(state, kind, extras, Insert.POSTAL_TYPE, Insert.POSTAL,
458                     StructuredPostal.STREET);
459         }
460 
461         {
462             // Phone
463             final DataKind kind = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
464             parseExtras(state, kind, extras, Insert.PHONE_TYPE, Insert.PHONE, Phone.NUMBER);
465             parseExtras(state, kind, extras, Insert.SECONDARY_PHONE_TYPE, Insert.SECONDARY_PHONE,
466                     Phone.NUMBER);
467             parseExtras(state, kind, extras, Insert.TERTIARY_PHONE_TYPE, Insert.TERTIARY_PHONE,
468                     Phone.NUMBER);
469         }
470 
471         {
472             // Email
473             final DataKind kind = source.getKindForMimetype(Email.CONTENT_ITEM_TYPE);
474             parseExtras(state, kind, extras, Insert.EMAIL_TYPE, Insert.EMAIL, Email.DATA);
475             parseExtras(state, kind, extras, Insert.SECONDARY_EMAIL_TYPE, Insert.SECONDARY_EMAIL,
476                     Email.DATA);
477             parseExtras(state, kind, extras, Insert.TERTIARY_EMAIL_TYPE, Insert.TERTIARY_EMAIL,
478                     Email.DATA);
479         }
480 
481         {
482             // Im
483             final DataKind kind = source.getKindForMimetype(Im.CONTENT_ITEM_TYPE);
484             fixupLegacyImType(extras);
485             parseExtras(state, kind, extras, Insert.IM_PROTOCOL, Insert.IM_HANDLE, Im.DATA);
486         }
487     }
488 
489     /**
490      * Attempt to parse legacy {@link Insert#IM_PROTOCOL} values, replacing them
491      * with updated values.
492      */
fixupLegacyImType(Bundle bundle)493     private static void fixupLegacyImType(Bundle bundle) {
494         final String encodedString = bundle.getString(Insert.IM_PROTOCOL);
495         if (encodedString == null) return;
496 
497         try {
498             final Object protocol = android.provider.Contacts.ContactMethods
499                     .decodeImProtocol(encodedString);
500             if (protocol instanceof Integer) {
501                 bundle.putInt(Insert.IM_PROTOCOL, (Integer)protocol);
502             } else {
503                 bundle.putString(Insert.IM_PROTOCOL, (String)protocol);
504             }
505         } catch (IllegalArgumentException e) {
506             // Ignore exception when legacy parser fails
507         }
508     }
509 
510     /**
511      * Parse a specific entry from the given {@link Bundle} and insert into the
512      * given {@link EntityDelta}. Silently skips the insert when missing value
513      * or no valid {@link EditType} found.
514      *
515      * @param typeExtra {@link Bundle} key that holds the incoming
516      *            {@link EditType#rawValue} value.
517      * @param valueExtra {@link Bundle} key that holds the incoming value.
518      * @param valueColumn Column to write value into {@link ValuesDelta}.
519      */
parseExtras(EntityDelta state, DataKind kind, Bundle extras, String typeExtra, String valueExtra, String valueColumn)520     public static void parseExtras(EntityDelta state, DataKind kind, Bundle extras,
521             String typeExtra, String valueExtra, String valueColumn) {
522         final CharSequence value = extras.getCharSequence(valueExtra);
523 
524         // Bail early if source doesn't handle this type
525         if (kind == null) return;
526 
527         // Bail when can't insert type, or value missing
528         final boolean canInsert = EntityModifier.canInsert(state, kind);
529         final boolean validValue = (value != null && TextUtils.isGraphic(value));
530         if (!validValue || !canInsert) return;
531 
532         // Find exact type when requested, otherwise best available type
533         final boolean hasType = extras.containsKey(typeExtra);
534         final int typeValue = extras.getInt(typeExtra, hasType ? BaseTypes.TYPE_CUSTOM
535                 : Integer.MIN_VALUE);
536         final EditType editType = EntityModifier.getBestValidType(state, kind, true, typeValue);
537 
538         // Create data row and fill with value
539         final ValuesDelta child = EntityModifier.insertChild(state, kind, editType);
540         child.put(valueColumn, value.toString());
541 
542         if (editType != null && editType.customColumn != null) {
543             // Write down label when custom type picked
544             final String customType = extras.getString(typeExtra);
545             child.put(editType.customColumn, customType);
546         }
547     }
548 }
549