Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License
     15  */
     16 package com.android.providers.contacts;
     17 
     18 import android.content.ContentValues;
     19 import android.content.Context;
     20 import android.database.Cursor;
     21 import android.database.sqlite.SQLiteDatabase;
     22 import android.provider.ContactsContract.CommonDataKinds.Email;
     23 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     24 import android.provider.ContactsContract.CommonDataKinds.Organization;
     25 import android.provider.ContactsContract.CommonDataKinds.Phone;
     26 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     27 import android.provider.ContactsContract.Data;
     28 import android.text.TextUtils;
     29 
     30 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
     31 import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
     32 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
     33 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     34 import com.android.providers.contacts.aggregation.ContactAggregator;
     35 
     36 /**
     37  * Handles inserts and update for a specific Data type.
     38  */
     39 public abstract class DataRowHandler {
     40 
     41     public interface DataDeleteQuery {
     42         public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
     43 
     44         public static final String[] CONCRETE_COLUMNS = new String[] {
     45             DataColumns.CONCRETE_ID,
     46             MimetypesColumns.MIMETYPE,
     47             Data.RAW_CONTACT_ID,
     48             Data.IS_PRIMARY,
     49             Data.DATA1,
     50         };
     51 
     52         public static final String[] COLUMNS = new String[] {
     53             Data._ID,
     54             MimetypesColumns.MIMETYPE,
     55             Data.RAW_CONTACT_ID,
     56             Data.IS_PRIMARY,
     57             Data.DATA1,
     58         };
     59 
     60         public static final int _ID = 0;
     61         public static final int MIMETYPE = 1;
     62         public static final int RAW_CONTACT_ID = 2;
     63         public static final int IS_PRIMARY = 3;
     64         public static final int DATA1 = 4;
     65     }
     66 
     67     public interface DataUpdateQuery {
     68         String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };
     69 
     70         int _ID = 0;
     71         int RAW_CONTACT_ID = 1;
     72         int MIMETYPE = 2;
     73     }
     74 
     75     protected final Context mContext;
     76     protected final ContactsDatabaseHelper mDbHelper;
     77     protected final ContactAggregator mContactAggregator;
     78     protected String[] mSelectionArgs1 = new String[1];
     79     protected final String mMimetype;
     80     protected long mMimetypeId;
     81 
     82     @SuppressWarnings("all")
     83     public DataRowHandler(Context context, ContactsDatabaseHelper dbHelper,
     84             ContactAggregator aggregator, String mimetype) {
     85         mContext = context;
     86         mDbHelper = dbHelper;
     87         mContactAggregator = aggregator;
     88         mMimetype = mimetype;
     89 
     90         // To ensure the data column position. This is dead code if properly configured.
     91         if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1
     92                 || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
     93                 || Email.DATA != Data.DATA1) {
     94             throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
     95                     + " data is not in DATA1 column");
     96         }
     97     }
     98 
     99     protected long getMimeTypeId() {
    100         if (mMimetypeId == 0) {
    101             mMimetypeId = mDbHelper.getMimeTypeId(mMimetype);
    102         }
    103         return mMimetypeId;
    104     }
    105 
    106     /**
    107      * Inserts a row into the {@link Data} table.
    108      */
    109     public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId,
    110             ContentValues values) {
    111         final long dataId = db.insert(Tables.DATA, null, values);
    112 
    113         final Integer primary = values.getAsInteger(Data.IS_PRIMARY);
    114         final Integer superPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY);
    115         if ((primary != null && primary != 0) || (superPrimary != null && superPrimary != 0)) {
    116             final long mimeTypeId = getMimeTypeId();
    117             mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
    118 
    119             // We also have to make sure that no other data item on this raw_contact is
    120             // configured super primary
    121             if (superPrimary != null) {
    122                 if (superPrimary != 0) {
    123                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    124                 } else {
    125                     mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId);
    126                 }
    127             } else {
    128                 // if there is already another data item configured as super-primary,
    129                 // take over the flag (which will automatically remove it from the other item)
    130                 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) {
    131                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    132                 }
    133             }
    134         }
    135 
    136         if (containsSearchableColumns(values)) {
    137             txContext.invalidateSearchIndexForRawContact(rawContactId);
    138         }
    139 
    140         return dataId;
    141     }
    142 
    143     /**
    144      * Validates data and updates a {@link Data} row using the cursor, which contains
    145      * the current data.
    146      *
    147      * @return true if update changed something
    148      */
    149     public boolean update(SQLiteDatabase db, TransactionContext txContext,
    150             ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
    151         long dataId = c.getLong(DataUpdateQuery._ID);
    152         long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
    153 
    154         handlePrimaryAndSuperPrimary(values, dataId, rawContactId);
    155 
    156         if (values.size() > 0) {
    157             mSelectionArgs1[0] = String.valueOf(dataId);
    158             db.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1);
    159         }
    160 
    161         if (containsSearchableColumns(values)) {
    162             txContext.invalidateSearchIndexForRawContact(rawContactId);
    163         }
    164 
    165         txContext.markRawContactDirtyAndChanged(rawContactId, callerIsSyncAdapter);
    166 
    167         return true;
    168     }
    169 
    170     public boolean hasSearchableData() {
    171         return false;
    172     }
    173 
    174     public boolean containsSearchableColumns(ContentValues values) {
    175         return false;
    176     }
    177 
    178     public void appendSearchableData(SearchIndexManager.IndexBuilder builder) {
    179     }
    180 
    181     /**
    182      * Ensures that all super-primary and primary flags of this raw_contact are
    183      * configured correctly
    184      */
    185     private void handlePrimaryAndSuperPrimary(ContentValues values, long dataId,
    186             long rawContactId) {
    187         final boolean hasPrimary = values.getAsInteger(Data.IS_PRIMARY) != null;
    188         final boolean hasSuperPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY) != null;
    189 
    190         // Nothing to do? Bail out early
    191         if (!hasPrimary && !hasSuperPrimary) return;
    192 
    193         final long mimeTypeId = getMimeTypeId();
    194 
    195         // Check if we want to clear values
    196         final boolean clearPrimary = hasPrimary &&
    197                 values.getAsInteger(Data.IS_PRIMARY) == 0;
    198         final boolean clearSuperPrimary = hasSuperPrimary &&
    199                 values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0;
    200 
    201         if (clearPrimary || clearSuperPrimary) {
    202             // Test whether these values are currently set
    203             mSelectionArgs1[0] = String.valueOf(dataId);
    204             final String[] cols = new String[] { Data.IS_PRIMARY, Data.IS_SUPER_PRIMARY };
    205             final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA,
    206                     cols, Data._ID + "=?", mSelectionArgs1, null, null, null);
    207             try {
    208                 if (c.moveToFirst()) {
    209                     final boolean isPrimary = c.getInt(0) != 0;
    210                     final boolean isSuperPrimary = c.getInt(1) != 0;
    211                     // Clear values if they are currently set
    212                     if (isSuperPrimary) {
    213                         mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId);
    214                     }
    215                     if (clearPrimary && isPrimary) {
    216                         mDbHelper.setIsPrimary(rawContactId, -1, mimeTypeId);
    217                     }
    218                 }
    219             } finally {
    220                 c.close();
    221             }
    222         } else {
    223             // Check if we want to set values
    224             final boolean setPrimary = hasPrimary &&
    225                     values.getAsInteger(Data.IS_PRIMARY) != 0;
    226             final boolean setSuperPrimary = hasSuperPrimary &&
    227                     values.getAsInteger(Data.IS_SUPER_PRIMARY) != 0;
    228             if (setSuperPrimary) {
    229                 // Set both super primary and primary
    230                 mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    231                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
    232             } else if (setPrimary) {
    233                 // Primary was explicitly set, but super-primary was not.
    234                 // In this case we set super-primary on this data item, if
    235                 // any data item of the same raw-contact already is super-primary
    236                 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) {
    237                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    238                 }
    239                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
    240             }
    241         }
    242 
    243         // Now that we've taken care of clearing this, remove it from "values".
    244         values.remove(Data.IS_SUPER_PRIMARY);
    245         values.remove(Data.IS_PRIMARY);
    246     }
    247 
    248     public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) {
    249         long dataId = c.getLong(DataDeleteQuery._ID);
    250         long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
    251         boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
    252         mSelectionArgs1[0] = String.valueOf(dataId);
    253         int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
    254         mSelectionArgs1[0] = String.valueOf(rawContactId);
    255         db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
    256         if (count != 0 && primary) {
    257             fixPrimary(db, rawContactId);
    258         }
    259 
    260         if (hasSearchableData()) {
    261             txContext.invalidateSearchIndexForRawContact(rawContactId);
    262         }
    263 
    264         return count;
    265     }
    266 
    267     private void fixPrimary(SQLiteDatabase db, long rawContactId) {
    268         long mimeTypeId = getMimeTypeId();
    269         long primaryId = -1;
    270         int primaryType = -1;
    271         mSelectionArgs1[0] = String.valueOf(rawContactId);
    272         Cursor c = db.query(DataDeleteQuery.TABLE,
    273                 DataDeleteQuery.CONCRETE_COLUMNS,
    274                 Data.RAW_CONTACT_ID + "=?" +
    275                     " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
    276                 mSelectionArgs1, null, null, null);
    277         try {
    278             while (c.moveToNext()) {
    279                 long dataId = c.getLong(DataDeleteQuery._ID);
    280                 int type = c.getInt(DataDeleteQuery.DATA1);
    281                 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
    282                     primaryId = dataId;
    283                     primaryType = type;
    284                 }
    285             }
    286         } finally {
    287             c.close();
    288         }
    289         if (primaryId != -1) {
    290             mDbHelper.setIsPrimary(rawContactId, primaryId, mimeTypeId);
    291         }
    292     }
    293 
    294     /**
    295      * Returns the rank of a specific record type to be used in determining the primary
    296      * row. Lower number represents higher priority.
    297      */
    298     protected int getTypeRank(int type) {
    299         return 0;
    300     }
    301 
    302     protected void fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext,
    303             long rawContactId) {
    304         if (!isNewRawContact(txContext, rawContactId)) {
    305             mDbHelper.updateRawContactDisplayName(db, rawContactId);
    306             mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
    307         }
    308     }
    309 
    310     private boolean isNewRawContact(TransactionContext txContext, long rawContactId) {
    311         return txContext.isNewRawContact(rawContactId);
    312     }
    313 
    314     /**
    315      * Return set of values, using current values at given {@link Data#_ID}
    316      * as baseline, but augmented with any updates.  Returns null if there is
    317      * no change.
    318      */
    319     public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
    320             ContentValues update) {
    321         boolean changing = false;
    322         final ContentValues values = new ContentValues();
    323         mSelectionArgs1[0] = String.valueOf(dataId);
    324         final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
    325                 mSelectionArgs1, null, null, null);
    326         try {
    327             if (cursor.moveToFirst()) {
    328                 for (int i = 0; i < cursor.getColumnCount(); i++) {
    329                     final String key = cursor.getColumnName(i);
    330                     final String value = cursor.getString(i);
    331                     if (!changing && update.containsKey(key)) {
    332                         Object newValue = update.get(key);
    333                         String newString = newValue == null ? null : newValue.toString();
    334                         changing |= !TextUtils.equals(newString, value);
    335                     }
    336                     values.put(key, value);
    337                 }
    338             }
    339         } finally {
    340             cursor.close();
    341         }
    342         if (!changing) {
    343             return null;
    344         }
    345 
    346         values.putAll(update);
    347         return values;
    348     }
    349 
    350     public void triggerAggregation(TransactionContext txContext, long rawContactId) {
    351         mContactAggregator.triggerAggregation(txContext, rawContactId);
    352     }
    353 
    354     /**
    355      * Test all against {@link TextUtils#isEmpty(CharSequence)}.
    356      */
    357     public boolean areAllEmpty(ContentValues values, String[] keys) {
    358         for (String key : keys) {
    359             if (!TextUtils.isEmpty(values.getAsString(key))) {
    360                 return false;
    361             }
    362         }
    363         return true;
    364     }
    365 
    366     /**
    367      * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
    368      */
    369     public boolean areAnySpecified(ContentValues values, String[] keys) {
    370         for (String key : keys) {
    371             if (values.containsKey(key)) {
    372                 return true;
    373             }
    374         }
    375         return false;
    376     }
    377 }
    378