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