Home | History | Annotate | Download | only in model
      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      */
     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 
     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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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