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