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         if (!callerIsSyncAdapter) {
    166             txContext.markRawContactDirty(rawContactId);
    167         }
    168 
    169         return true;
    170     }
    171 
    172     public boolean hasSearchableData() {
    173         return false;
    174     }
    175 
    176     public boolean containsSearchableColumns(ContentValues values) {
    177         return false;
    178     }
    179 
    180     public void appendSearchableData(SearchIndexManager.IndexBuilder builder) {
    181     }
    182 
    183     /**
    184      * Ensures that all super-primary and primary flags of this raw_contact are
    185      * configured correctly
    186      */
    187     private void handlePrimaryAndSuperPrimary(ContentValues values, long dataId,
    188             long rawContactId) {
    189         final boolean hasPrimary = values.containsKey(Data.IS_PRIMARY);
    190         final boolean hasSuperPrimary = values.containsKey(Data.IS_SUPER_PRIMARY);
    191 
    192         // Nothing to do? Bail out early
    193         if (!hasPrimary && !hasSuperPrimary) return;
    194 
    195         final long mimeTypeId = getMimeTypeId();
    196 
    197         // Check if we want to clear values
    198         final boolean clearPrimary = hasPrimary &&
    199                 values.getAsInteger(Data.IS_PRIMARY) == 0;
    200         final boolean clearSuperPrimary = hasSuperPrimary &&
    201                 values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0;
    202 
    203         if (clearPrimary || clearSuperPrimary) {
    204             // Test whether these values are currently set
    205             mSelectionArgs1[0] = String.valueOf(dataId);
    206             final String[] cols = new String[] { Data.IS_PRIMARY, Data.IS_SUPER_PRIMARY };
    207             final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA,
    208                     cols, Data._ID + "=?", mSelectionArgs1, null, null, null);
    209             try {
    210                 if (c.moveToFirst()) {
    211                     final boolean isPrimary = c.getInt(0) != 0;
    212                     final boolean isSuperPrimary = c.getInt(1) != 0;
    213                     // Clear values if they are currently set
    214                     if (isSuperPrimary) {
    215                         mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId);
    216                     }
    217                     if (clearPrimary && isPrimary) {
    218                         mDbHelper.setIsPrimary(rawContactId, -1, mimeTypeId);
    219                     }
    220                 }
    221             } finally {
    222                 c.close();
    223             }
    224         } else {
    225             // Check if we want to set values
    226             final boolean setPrimary = hasPrimary &&
    227                     values.getAsInteger(Data.IS_PRIMARY) != 0;
    228             final boolean setSuperPrimary = hasSuperPrimary &&
    229                     values.getAsInteger(Data.IS_SUPER_PRIMARY) != 0;
    230             if (setSuperPrimary) {
    231                 // Set both super primary and primary
    232                 mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    233                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
    234             } else if (setPrimary) {
    235                 // Primary was explicitly set, but super-primary was not.
    236                 // In this case we set super-primary on this data item, if
    237                 // any data item of the same raw-contact already is super-primary
    238                 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) {
    239                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    240                 }
    241                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
    242             }
    243         }
    244 
    245         // Now that we've taken care of clearing this, remove it from "values".
    246         values.remove(Data.IS_SUPER_PRIMARY);
    247         values.remove(Data.IS_PRIMARY);
    248     }
    249 
    250     public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) {
    251         long dataId = c.getLong(DataDeleteQuery._ID);
    252         long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
    253         boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
    254         mSelectionArgs1[0] = String.valueOf(dataId);
    255         int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
    256         mSelectionArgs1[0] = String.valueOf(rawContactId);
    257         db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
    258         if (count != 0 && primary) {
    259             fixPrimary(db, rawContactId);
    260         }
    261 
    262         if (hasSearchableData()) {
    263             txContext.invalidateSearchIndexForRawContact(rawContactId);
    264         }
    265 
    266         return count;
    267     }
    268 
    269     private void fixPrimary(SQLiteDatabase db, long rawContactId) {
    270         long mimeTypeId = getMimeTypeId();
    271         long primaryId = -1;
    272         int primaryType = -1;
    273         mSelectionArgs1[0] = String.valueOf(rawContactId);
    274         Cursor c = db.query(DataDeleteQuery.TABLE,
    275                 DataDeleteQuery.CONCRETE_COLUMNS,
    276                 Data.RAW_CONTACT_ID + "=?" +
    277                     " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
    278                 mSelectionArgs1, null, null, null);
    279         try {
    280             while (c.moveToNext()) {
    281                 long dataId = c.getLong(DataDeleteQuery._ID);
    282                 int type = c.getInt(DataDeleteQuery.DATA1);
    283                 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
    284                     primaryId = dataId;
    285                     primaryType = type;
    286                 }
    287             }
    288         } finally {
    289             c.close();
    290         }
    291         if (primaryId != -1) {
    292             mDbHelper.setIsPrimary(rawContactId, primaryId, mimeTypeId);
    293         }
    294     }
    295 
    296     /**
    297      * Returns the rank of a specific record type to be used in determining the primary
    298      * row. Lower number represents higher priority.
    299      */
    300     protected int getTypeRank(int type) {
    301         return 0;
    302     }
    303 
    304     protected void fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext,
    305             long rawContactId) {
    306         if (!isNewRawContact(txContext, rawContactId)) {
    307             mDbHelper.updateRawContactDisplayName(db, rawContactId);
    308             mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
    309         }
    310     }
    311 
    312     private boolean isNewRawContact(TransactionContext txContext, long rawContactId) {
    313         return txContext.isNewRawContact(rawContactId);
    314     }
    315 
    316     /**
    317      * Return set of values, using current values at given {@link Data#_ID}
    318      * as baseline, but augmented with any updates.  Returns null if there is
    319      * no change.
    320      */
    321     public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
    322             ContentValues update) {
    323         boolean changing = false;
    324         final ContentValues values = new ContentValues();
    325         mSelectionArgs1[0] = String.valueOf(dataId);
    326         final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
    327                 mSelectionArgs1, null, null, null);
    328         try {
    329             if (cursor.moveToFirst()) {
    330                 for (int i = 0; i < cursor.getColumnCount(); i++) {
    331                     final String key = cursor.getColumnName(i);
    332                     final String value = cursor.getString(i);
    333                     if (!changing && update.containsKey(key)) {
    334                         Object newValue = update.get(key);
    335                         String newString = newValue == null ? null : newValue.toString();
    336                         changing |= !TextUtils.equals(newString, value);
    337                     }
    338                     values.put(key, value);
    339                 }
    340             }
    341         } finally {
    342             cursor.close();
    343         }
    344         if (!changing) {
    345             return null;
    346         }
    347 
    348         values.putAll(update);
    349         return values;
    350     }
    351 
    352     public void triggerAggregation(TransactionContext txContext, long rawContactId) {
    353         mContactAggregator.triggerAggregation(txContext, rawContactId);
    354     }
    355 
    356     /**
    357      * Test all against {@link TextUtils#isEmpty(CharSequence)}.
    358      */
    359     public boolean areAllEmpty(ContentValues values, String[] keys) {
    360         for (String key : keys) {
    361             if (!TextUtils.isEmpty(values.getAsString(key))) {
    362                 return false;
    363             }
    364         }
    365         return true;
    366     }
    367 
    368     /**
    369      * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
    370      */
    371     public boolean areAnySpecified(ContentValues values, String[] keys) {
    372         for (String key : keys) {
    373             if (values.containsKey(key)) {
    374                 return true;
    375             }
    376         }
    377         return false;
    378     }
    379 }
    380