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;
     23 import android.provider.ContactsContract.CommonDataKinds.Email;
     24 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     25 import android.provider.ContactsContract.CommonDataKinds.Organization;
     26 import android.provider.ContactsContract.CommonDataKinds.Phone;
     27 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     28 import android.provider.ContactsContract.Data;
     29 import android.text.TextUtils;
     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.AbstractContactAggregator;
     35 
     36 /**
     37  * Handles inserts and update for a specific Data type.
     38  */
     39 public abstract class DataRowHandler {
     40 
     41     private static final String[] HASH_INPUT_COLUMNS = new String[] {
     42             Data.DATA1, Data.DATA2};
     43 
     44     public interface DataDeleteQuery {
     45         public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
     46 
     47         public static final String[] CONCRETE_COLUMNS = new String[] {
     48             DataColumns.CONCRETE_ID,
     49             MimetypesColumns.MIMETYPE,
     50             Data.RAW_CONTACT_ID,
     51             Data.IS_PRIMARY,
     52             Data.DATA1,
     53         };
     54 
     55         public static final String[] COLUMNS = new String[] {
     56             Data._ID,
     57             MimetypesColumns.MIMETYPE,
     58             Data.RAW_CONTACT_ID,
     59             Data.IS_PRIMARY,
     60             Data.DATA1,
     61         };
     62 
     63         public static final int _ID = 0;
     64         public static final int MIMETYPE = 1;
     65         public static final int RAW_CONTACT_ID = 2;
     66         public static final int IS_PRIMARY = 3;
     67         public static final int DATA1 = 4;
     68     }
     69 
     70     public interface DataUpdateQuery {
     71         String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };
     72 
     73         int _ID = 0;
     74         int RAW_CONTACT_ID = 1;
     75         int MIMETYPE = 2;
     76     }
     77 
     78     protected final Context mContext;
     79     protected final ContactsDatabaseHelper mDbHelper;
     80     protected final AbstractContactAggregator mContactAggregator;
     81     protected String[] mSelectionArgs1 = new String[1];
     82     protected final String mMimetype;
     83     protected long mMimetypeId;
     84 
     85     @SuppressWarnings("all")
     86     public DataRowHandler(Context context, ContactsDatabaseHelper dbHelper,
     87             AbstractContactAggregator aggregator, String mimetype) {
     88         mContext = context;
     89         mDbHelper = dbHelper;
     90         mContactAggregator = aggregator;
     91         mMimetype = mimetype;
     92 
     93         // To ensure the data column position. This is dead code if properly configured.
     94         if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1
     95                 || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
     96                 || Email.DATA != Data.DATA1) {
     97             throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
     98                     + " data is not in DATA1 column");
     99         }
    100     }
    101 
    102     protected long getMimeTypeId() {
    103         if (mMimetypeId == 0) {
    104             mMimetypeId = mDbHelper.getMimeTypeId(mMimetype);
    105         }
    106         return mMimetypeId;
    107     }
    108 
    109     /**
    110      * Inserts a row into the {@link Data} table.
    111      */
    112     public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId,
    113             ContentValues values) {
    114         // Generate hash_id from data1 and data2 columns.
    115         // For photo, use data15 column instead of data1 and data2 to generate hash_id.
    116         handleHashIdForInsert(values);
    117         final long dataId = db.insert(Tables.DATA, null, values);
    118 
    119         final Integer primary = values.getAsInteger(Data.IS_PRIMARY);
    120         final Integer superPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY);
    121         if ((primary != null && primary != 0) || (superPrimary != null && superPrimary != 0)) {
    122             final long mimeTypeId = getMimeTypeId();
    123             mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
    124             txContext.markRawContactMetadataDirty(rawContactId, /* isMetadataSyncAdapter =*/false);
    125 
    126             // We also have to make sure that no other data item on this raw_contact is
    127             // configured super primary
    128             if (superPrimary != null) {
    129                 if (superPrimary != 0) {
    130                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    131                 } else {
    132                     mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId);
    133                 }
    134             } else {
    135                 // if there is already another data item configured as super-primary,
    136                 // take over the flag (which will automatically remove it from the other item)
    137                 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) {
    138                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    139                 }
    140             }
    141         }
    142 
    143         if (containsSearchableColumns(values)) {
    144             txContext.invalidateSearchIndexForRawContact(rawContactId);
    145         }
    146 
    147         return dataId;
    148     }
    149 
    150     /**
    151      * Validates data and updates a {@link Data} row using the cursor, which contains
    152      * the current data.
    153      *
    154      * @return true if update changed something
    155      */
    156     public boolean update(SQLiteDatabase db, TransactionContext txContext,
    157             ContentValues values, Cursor c, boolean callerIsSyncAdapter,
    158             boolean callerIsMetadataSyncAdapter) {
    159         long dataId = c.getLong(DataUpdateQuery._ID);
    160         long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
    161 
    162         handlePrimaryAndSuperPrimary(txContext, values, dataId, rawContactId,
    163                 callerIsMetadataSyncAdapter);
    164         handleHashIdForUpdate(values, dataId);
    165 
    166         if (values.size() > 0) {
    167             mSelectionArgs1[0] = String.valueOf(dataId);
    168             db.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1);
    169         }
    170 
    171         if (containsSearchableColumns(values)) {
    172             txContext.invalidateSearchIndexForRawContact(rawContactId);
    173         }
    174 
    175         txContext.markRawContactDirtyAndChanged(rawContactId, callerIsSyncAdapter);
    176 
    177         return true;
    178     }
    179 
    180     public boolean hasSearchableData() {
    181         return false;
    182     }
    183 
    184     public boolean containsSearchableColumns(ContentValues values) {
    185         return false;
    186     }
    187 
    188     public void appendSearchableData(SearchIndexManager.IndexBuilder builder) {
    189     }
    190 
    191     /**
    192      * Fetch data1, data2, and data15 from values if they exist, and generate hash_id
    193      * if one of data1 and data2 columns is set, otherwise using data15 instead.
    194      * hash_id is null if all of these three field is null.
    195      * Add hash_id key to values.
    196      */
    197     public void handleHashIdForInsert(ContentValues values) {
    198         final String data1 = values.getAsString(Data.DATA1);
    199         final String data2 = values.getAsString(Data.DATA2);
    200         final String photoHashId= mDbHelper.getPhotoHashId();
    201 
    202         String hashId;
    203         if (ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE.equals(mMimetype)) {
    204             hashId = photoHashId;
    205         } else if (!TextUtils.isEmpty(data1) || !TextUtils.isEmpty(data2)) {
    206             hashId = mDbHelper.generateHashId(data1, data2);
    207         } else {
    208             hashId = null;
    209         }
    210         if (TextUtils.isEmpty(hashId)) {
    211             values.putNull(Data.HASH_ID);
    212         } else {
    213             values.put(Data.HASH_ID, hashId);
    214         }
    215     }
    216 
    217     /**
    218      * Compute hash_id column and add it to values.
    219      * If this is not a photo field, and one of data1 and data2 changed, re-compute hash_id with new
    220      * data1 and data2.
    221      * If this is a photo field, no need to change hash_id.
    222      */
    223     private void handleHashIdForUpdate(ContentValues values, long dataId) {
    224         if (!ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE.equals(mMimetype)
    225                 && (values.containsKey(Data.DATA1) || values.containsKey(Data.DATA2))) {
    226             String data1 = values.getAsString(Data.DATA1);
    227             String data2 = values.getAsString(Data.DATA2);
    228             mSelectionArgs1[0] = String.valueOf(dataId);
    229             final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA,
    230                     HASH_INPUT_COLUMNS, Data._ID + "=?", mSelectionArgs1, null, null, null);
    231             try {
    232                 if (c.moveToFirst()) {
    233                     data1 = values.containsKey(Data.DATA1) ? data1 : c.getString(0);
    234                     data2 = values.containsKey(Data.DATA2) ? data2 : c.getString(1);
    235                 }
    236             } finally {
    237                 c.close();
    238             }
    239 
    240             String hashId = mDbHelper.generateHashId(data1, data2);
    241             if (TextUtils.isEmpty(hashId)) {
    242                 values.putNull(Data.HASH_ID);
    243             } else {
    244                 values.put(Data.HASH_ID, hashId);
    245             }
    246         }
    247     }
    248 
    249     /**
    250      * Ensures that all super-primary and primary flags of this raw_contact are
    251      * configured correctly
    252      */
    253     private void handlePrimaryAndSuperPrimary(TransactionContext txContext, ContentValues values,
    254             long dataId, long rawContactId, boolean callerIsMetadataSyncAdapter) {
    255         final boolean hasPrimary = values.getAsInteger(Data.IS_PRIMARY) != null;
    256         final boolean hasSuperPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY) != null;
    257 
    258         // Nothing to do? Bail out early
    259         if (!hasPrimary && !hasSuperPrimary) return;
    260 
    261         txContext.markRawContactMetadataDirty(rawContactId, callerIsMetadataSyncAdapter);
    262 
    263         final long mimeTypeId = getMimeTypeId();
    264 
    265         // Check if we want to clear values
    266         final boolean clearPrimary = hasPrimary &&
    267                 values.getAsInteger(Data.IS_PRIMARY) == 0;
    268         final boolean clearSuperPrimary = hasSuperPrimary &&
    269                 values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0;
    270 
    271         if (clearPrimary || clearSuperPrimary) {
    272             // Test whether these values are currently set
    273             mSelectionArgs1[0] = String.valueOf(dataId);
    274             final String[] cols = new String[] { Data.IS_PRIMARY, Data.IS_SUPER_PRIMARY };
    275             final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA,
    276                     cols, Data._ID + "=?", mSelectionArgs1, null, null, null);
    277             try {
    278                 if (c.moveToFirst()) {
    279                     final boolean isPrimary = c.getInt(0) != 0;
    280                     final boolean isSuperPrimary = c.getInt(1) != 0;
    281                     // Clear values if they are currently set
    282                     if (isSuperPrimary) {
    283                         mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId);
    284                     }
    285                     if (clearPrimary && isPrimary) {
    286                         mDbHelper.setIsPrimary(rawContactId, -1, mimeTypeId);
    287                     }
    288                 }
    289             } finally {
    290                 c.close();
    291             }
    292         } else {
    293             // Check if we want to set values
    294             final boolean setPrimary = hasPrimary &&
    295                     values.getAsInteger(Data.IS_PRIMARY) != 0;
    296             final boolean setSuperPrimary = hasSuperPrimary &&
    297                     values.getAsInteger(Data.IS_SUPER_PRIMARY) != 0;
    298             if (setSuperPrimary) {
    299                 // Set both super primary and primary
    300                 mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    301                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
    302             } else if (setPrimary) {
    303                 // Primary was explicitly set, but super-primary was not.
    304                 // In this case we set super-primary on this data item, if
    305                 // any data item of the same raw-contact already is super-primary
    306                 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) {
    307                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
    308                 }
    309                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
    310             }
    311         }
    312 
    313         // Now that we've taken care of clearing this, remove it from "values".
    314         values.remove(Data.IS_SUPER_PRIMARY);
    315         values.remove(Data.IS_PRIMARY);
    316     }
    317 
    318     public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) {
    319         long dataId = c.getLong(DataDeleteQuery._ID);
    320         long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
    321         boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
    322         mSelectionArgs1[0] = String.valueOf(dataId);
    323         int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
    324         mSelectionArgs1[0] = String.valueOf(rawContactId);
    325         db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
    326         if (count != 0 && primary) {
    327             fixPrimary(db, rawContactId);
    328             txContext.markRawContactMetadataDirty(rawContactId, /* isMetadataSyncAdapter =*/false);
    329         }
    330 
    331         if (hasSearchableData()) {
    332             txContext.invalidateSearchIndexForRawContact(rawContactId);
    333         }
    334 
    335         return count;
    336     }
    337 
    338     private void fixPrimary(SQLiteDatabase db, long rawContactId) {
    339         long mimeTypeId = getMimeTypeId();
    340         long primaryId = -1;
    341         int primaryType = -1;
    342         mSelectionArgs1[0] = String.valueOf(rawContactId);
    343         Cursor c = db.query(DataDeleteQuery.TABLE,
    344                 DataDeleteQuery.CONCRETE_COLUMNS,
    345                 Data.RAW_CONTACT_ID + "=?" +
    346                     " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
    347                 mSelectionArgs1, null, null, null);
    348         try {
    349             while (c.moveToNext()) {
    350                 long dataId = c.getLong(DataDeleteQuery._ID);
    351                 int type = c.getInt(DataDeleteQuery.DATA1);
    352                 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
    353                     primaryId = dataId;
    354                     primaryType = type;
    355                 }
    356             }
    357         } finally {
    358             c.close();
    359         }
    360         if (primaryId != -1) {
    361             mDbHelper.setIsPrimary(rawContactId, primaryId, mimeTypeId);
    362         }
    363     }
    364 
    365     /**
    366      * Returns the rank of a specific record type to be used in determining the primary
    367      * row. Lower number represents higher priority.
    368      */
    369     protected int getTypeRank(int type) {
    370         return 0;
    371     }
    372 
    373     protected void fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext,
    374             long rawContactId) {
    375         if (!isNewRawContact(txContext, rawContactId)) {
    376             mDbHelper.updateRawContactDisplayName(db, rawContactId);
    377             mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
    378         }
    379     }
    380 
    381     private boolean isNewRawContact(TransactionContext txContext, long rawContactId) {
    382         return txContext.isNewRawContact(rawContactId);
    383     }
    384 
    385     /**
    386      * Return set of values, using current values at given {@link Data#_ID}
    387      * as baseline, but augmented with any updates.  Returns null if there is
    388      * no change.
    389      */
    390     public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
    391             ContentValues update) {
    392         boolean changing = false;
    393         final ContentValues values = new ContentValues();
    394         mSelectionArgs1[0] = String.valueOf(dataId);
    395         final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
    396                 mSelectionArgs1, null, null, null);
    397         try {
    398             if (cursor.moveToFirst()) {
    399                 for (int i = 0; i < cursor.getColumnCount(); i++) {
    400                     final String key = cursor.getColumnName(i);
    401                     final String value = cursor.getString(i);
    402                     if (!changing && update.containsKey(key)) {
    403                         Object newValue = update.get(key);
    404                         String newString = newValue == null ? null : newValue.toString();
    405                         changing |= !TextUtils.equals(newString, value);
    406                     }
    407                     values.put(key, value);
    408                 }
    409             }
    410         } finally {
    411             cursor.close();
    412         }
    413         if (!changing) {
    414             return null;
    415         }
    416 
    417         values.putAll(update);
    418         return values;
    419     }
    420 
    421     public void triggerAggregation(TransactionContext txContext, long rawContactId) {
    422         mContactAggregator.triggerAggregation(txContext, rawContactId);
    423     }
    424 
    425     /**
    426      * Test all against {@link TextUtils#isEmpty(CharSequence)}.
    427      */
    428     public boolean areAllEmpty(ContentValues values, String[] keys) {
    429         for (String key : keys) {
    430             if (!TextUtils.isEmpty(values.getAsString(key))) {
    431                 return false;
    432             }
    433         }
    434         return true;
    435     }
    436 
    437     /**
    438      * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
    439      */
    440     public boolean areAnySpecified(ContentValues values, String[] keys) {
    441         for (String key : keys) {
    442             if (values.containsKey(key)) {
    443                 return true;
    444             }
    445         }
    446         return false;
    447     }
    448 }
    449