| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License |
| */ |
| package com.android.providers.contacts; |
| |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.provider.ContactsContract; |
| import android.provider.ContactsContract.CommonDataKinds.Email; |
| import android.provider.ContactsContract.CommonDataKinds.Nickname; |
| import android.provider.ContactsContract.CommonDataKinds.Organization; |
| import android.provider.ContactsContract.CommonDataKinds.Phone; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredName; |
| import android.provider.ContactsContract.Data; |
| import android.text.TextUtils; |
| import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns; |
| import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns; |
| import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns; |
| import com.android.providers.contacts.ContactsDatabaseHelper.Tables; |
| import com.android.providers.contacts.aggregation.AbstractContactAggregator; |
| |
| /** |
| * Handles inserts and update for a specific Data type. |
| */ |
| public abstract class DataRowHandler { |
| |
| private static final String[] HASH_INPUT_COLUMNS = new String[] { |
| Data.DATA1, Data.DATA2}; |
| |
| public interface DataDeleteQuery { |
| public static final String TABLE = Tables.DATA_JOIN_MIMETYPES; |
| |
| public static final String[] CONCRETE_COLUMNS = new String[] { |
| DataColumns.CONCRETE_ID, |
| MimetypesColumns.MIMETYPE, |
| Data.RAW_CONTACT_ID, |
| Data.IS_PRIMARY, |
| Data.DATA1, |
| }; |
| |
| public static final String[] COLUMNS = new String[] { |
| Data._ID, |
| MimetypesColumns.MIMETYPE, |
| Data.RAW_CONTACT_ID, |
| Data.IS_PRIMARY, |
| Data.DATA1, |
| }; |
| |
| public static final int _ID = 0; |
| public static final int MIMETYPE = 1; |
| public static final int RAW_CONTACT_ID = 2; |
| public static final int IS_PRIMARY = 3; |
| public static final int DATA1 = 4; |
| } |
| |
| public interface DataUpdateQuery { |
| String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE }; |
| |
| int _ID = 0; |
| int RAW_CONTACT_ID = 1; |
| int MIMETYPE = 2; |
| } |
| |
| protected final Context mContext; |
| protected final ContactsDatabaseHelper mDbHelper; |
| protected final AbstractContactAggregator mContactAggregator; |
| protected String[] mSelectionArgs1 = new String[1]; |
| protected final String mMimetype; |
| protected long mMimetypeId; |
| |
| @SuppressWarnings("all") |
| public DataRowHandler(Context context, ContactsDatabaseHelper dbHelper, |
| AbstractContactAggregator aggregator, String mimetype) { |
| mContext = context; |
| mDbHelper = dbHelper; |
| mContactAggregator = aggregator; |
| mMimetype = mimetype; |
| |
| // To ensure the data column position. This is dead code if properly configured. |
| if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1 |
| || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1 |
| || Email.DATA != Data.DATA1) { |
| throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary" |
| + " data is not in DATA1 column"); |
| } |
| } |
| |
| protected long getMimeTypeId() { |
| if (mMimetypeId == 0) { |
| mMimetypeId = mDbHelper.getMimeTypeId(mMimetype); |
| } |
| return mMimetypeId; |
| } |
| |
| /** |
| * Inserts a row into the {@link Data} table. |
| */ |
| public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, |
| ContentValues values) { |
| // Generate hash_id from data1 and data2 columns. |
| // For photo, use data15 column instead of data1 and data2 to generate hash_id. |
| handleHashIdForInsert(values); |
| final long dataId = db.insert(Tables.DATA, null, values); |
| |
| final Integer primary = values.getAsInteger(Data.IS_PRIMARY); |
| final Integer superPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY); |
| if ((primary != null && primary != 0) || (superPrimary != null && superPrimary != 0)) { |
| final long mimeTypeId = getMimeTypeId(); |
| mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId); |
| txContext.markRawContactMetadataDirty(rawContactId, /* isMetadataSyncAdapter =*/false); |
| |
| // We also have to make sure that no other data item on this raw_contact is |
| // configured super primary |
| if (superPrimary != null) { |
| if (superPrimary != 0) { |
| mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId); |
| } else { |
| mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId); |
| } |
| } else { |
| // if there is already another data item configured as super-primary, |
| // take over the flag (which will automatically remove it from the other item) |
| if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) { |
| mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId); |
| } |
| } |
| } |
| |
| if (containsSearchableColumns(values)) { |
| txContext.invalidateSearchIndexForRawContact(rawContactId); |
| } |
| |
| return dataId; |
| } |
| |
| /** |
| * Validates data and updates a {@link Data} row using the cursor, which contains |
| * the current data. |
| * |
| * @return true if update changed something |
| */ |
| public boolean update(SQLiteDatabase db, TransactionContext txContext, |
| ContentValues values, Cursor c, boolean callerIsSyncAdapter, |
| boolean callerIsMetadataSyncAdapter) { |
| long dataId = c.getLong(DataUpdateQuery._ID); |
| long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID); |
| |
| handlePrimaryAndSuperPrimary(txContext, values, dataId, rawContactId, |
| callerIsMetadataSyncAdapter); |
| handleHashIdForUpdate(values, dataId); |
| |
| if (values.size() > 0) { |
| mSelectionArgs1[0] = String.valueOf(dataId); |
| db.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1); |
| } |
| |
| if (containsSearchableColumns(values)) { |
| txContext.invalidateSearchIndexForRawContact(rawContactId); |
| } |
| |
| txContext.markRawContactDirtyAndChanged(rawContactId, callerIsSyncAdapter); |
| |
| return true; |
| } |
| |
| public boolean hasSearchableData() { |
| return false; |
| } |
| |
| public boolean containsSearchableColumns(ContentValues values) { |
| return false; |
| } |
| |
| public void appendSearchableData(SearchIndexManager.IndexBuilder builder) { |
| } |
| |
| /** |
| * Fetch data1, data2, and data15 from values if they exist, and generate hash_id |
| * if one of data1 and data2 columns is set, otherwise using data15 instead. |
| * hash_id is null if all of these three field is null. |
| * Add hash_id key to values. |
| */ |
| public void handleHashIdForInsert(ContentValues values) { |
| final String data1 = values.getAsString(Data.DATA1); |
| final String data2 = values.getAsString(Data.DATA2); |
| final String photoHashId= mDbHelper.getPhotoHashId(); |
| |
| String hashId; |
| if (ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE.equals(mMimetype)) { |
| hashId = photoHashId; |
| } else if (!TextUtils.isEmpty(data1) || !TextUtils.isEmpty(data2)) { |
| hashId = mDbHelper.generateHashId(data1, data2); |
| } else { |
| hashId = null; |
| } |
| if (TextUtils.isEmpty(hashId)) { |
| values.putNull(Data.HASH_ID); |
| } else { |
| values.put(Data.HASH_ID, hashId); |
| } |
| } |
| |
| /** |
| * Compute hash_id column and add it to values. |
| * If this is not a photo field, and one of data1 and data2 changed, re-compute hash_id with new |
| * data1 and data2. |
| * If this is a photo field, no need to change hash_id. |
| */ |
| private void handleHashIdForUpdate(ContentValues values, long dataId) { |
| if (!ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE.equals(mMimetype) |
| && (values.containsKey(Data.DATA1) || values.containsKey(Data.DATA2))) { |
| String data1 = values.getAsString(Data.DATA1); |
| String data2 = values.getAsString(Data.DATA2); |
| mSelectionArgs1[0] = String.valueOf(dataId); |
| final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA, |
| HASH_INPUT_COLUMNS, Data._ID + "=?", mSelectionArgs1, null, null, null); |
| try { |
| if (c.moveToFirst()) { |
| data1 = values.containsKey(Data.DATA1) ? data1 : c.getString(0); |
| data2 = values.containsKey(Data.DATA2) ? data2 : c.getString(1); |
| } |
| } finally { |
| c.close(); |
| } |
| |
| String hashId = mDbHelper.generateHashId(data1, data2); |
| if (TextUtils.isEmpty(hashId)) { |
| values.putNull(Data.HASH_ID); |
| } else { |
| values.put(Data.HASH_ID, hashId); |
| } |
| } |
| } |
| |
| /** |
| * Ensures that all super-primary and primary flags of this raw_contact are |
| * configured correctly |
| */ |
| private void handlePrimaryAndSuperPrimary(TransactionContext txContext, ContentValues values, |
| long dataId, long rawContactId, boolean callerIsMetadataSyncAdapter) { |
| final boolean hasPrimary = values.getAsInteger(Data.IS_PRIMARY) != null; |
| final boolean hasSuperPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY) != null; |
| |
| // Nothing to do? Bail out early |
| if (!hasPrimary && !hasSuperPrimary) return; |
| |
| txContext.markRawContactMetadataDirty(rawContactId, callerIsMetadataSyncAdapter); |
| |
| final long mimeTypeId = getMimeTypeId(); |
| |
| // Check if we want to clear values |
| final boolean clearPrimary = hasPrimary && |
| values.getAsInteger(Data.IS_PRIMARY) == 0; |
| final boolean clearSuperPrimary = hasSuperPrimary && |
| values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0; |
| |
| if (clearPrimary || clearSuperPrimary) { |
| // Test whether these values are currently set |
| mSelectionArgs1[0] = String.valueOf(dataId); |
| final String[] cols = new String[] { Data.IS_PRIMARY, Data.IS_SUPER_PRIMARY }; |
| final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA, |
| cols, Data._ID + "=?", mSelectionArgs1, null, null, null); |
| try { |
| if (c.moveToFirst()) { |
| final boolean isPrimary = c.getInt(0) != 0; |
| final boolean isSuperPrimary = c.getInt(1) != 0; |
| // Clear values if they are currently set |
| if (isSuperPrimary) { |
| mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId); |
| } |
| if (clearPrimary && isPrimary) { |
| mDbHelper.setIsPrimary(rawContactId, -1, mimeTypeId); |
| } |
| } |
| } finally { |
| c.close(); |
| } |
| } else { |
| // Check if we want to set values |
| final boolean setPrimary = hasPrimary && |
| values.getAsInteger(Data.IS_PRIMARY) != 0; |
| final boolean setSuperPrimary = hasSuperPrimary && |
| values.getAsInteger(Data.IS_SUPER_PRIMARY) != 0; |
| if (setSuperPrimary) { |
| // Set both super primary and primary |
| mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId); |
| mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId); |
| } else if (setPrimary) { |
| // Primary was explicitly set, but super-primary was not. |
| // In this case we set super-primary on this data item, if |
| // any data item of the same raw-contact already is super-primary |
| if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) { |
| mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId); |
| } |
| mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId); |
| } |
| } |
| |
| // Now that we've taken care of clearing this, remove it from "values". |
| values.remove(Data.IS_SUPER_PRIMARY); |
| values.remove(Data.IS_PRIMARY); |
| } |
| |
| public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) { |
| long dataId = c.getLong(DataDeleteQuery._ID); |
| long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); |
| boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0; |
| mSelectionArgs1[0] = String.valueOf(dataId); |
| int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1); |
| mSelectionArgs1[0] = String.valueOf(rawContactId); |
| db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1); |
| if (count != 0 && primary) { |
| fixPrimary(db, rawContactId); |
| txContext.markRawContactMetadataDirty(rawContactId, /* isMetadataSyncAdapter =*/false); |
| } |
| |
| if (hasSearchableData()) { |
| txContext.invalidateSearchIndexForRawContact(rawContactId); |
| } |
| |
| return count; |
| } |
| |
| private void fixPrimary(SQLiteDatabase db, long rawContactId) { |
| long mimeTypeId = getMimeTypeId(); |
| long primaryId = -1; |
| int primaryType = -1; |
| mSelectionArgs1[0] = String.valueOf(rawContactId); |
| Cursor c = db.query(DataDeleteQuery.TABLE, |
| DataDeleteQuery.CONCRETE_COLUMNS, |
| Data.RAW_CONTACT_ID + "=?" + |
| " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId, |
| mSelectionArgs1, null, null, null); |
| try { |
| while (c.moveToNext()) { |
| long dataId = c.getLong(DataDeleteQuery._ID); |
| int type = c.getInt(DataDeleteQuery.DATA1); |
| if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) { |
| primaryId = dataId; |
| primaryType = type; |
| } |
| } |
| } finally { |
| c.close(); |
| } |
| if (primaryId != -1) { |
| mDbHelper.setIsPrimary(rawContactId, primaryId, mimeTypeId); |
| } |
| } |
| |
| /** |
| * Returns the rank of a specific record type to be used in determining the primary |
| * row. Lower number represents higher priority. |
| */ |
| protected int getTypeRank(int type) { |
| return 0; |
| } |
| |
| protected void fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext, |
| long rawContactId) { |
| if (!isNewRawContact(txContext, rawContactId)) { |
| mDbHelper.updateRawContactDisplayName(db, rawContactId); |
| mContactAggregator.updateDisplayNameForRawContact(db, rawContactId); |
| } |
| } |
| |
| private boolean isNewRawContact(TransactionContext txContext, long rawContactId) { |
| return txContext.isNewRawContact(rawContactId); |
| } |
| |
| /** |
| * Return set of values, using current values at given {@link Data#_ID} |
| * as baseline, but augmented with any updates. Returns null if there is |
| * no change. |
| */ |
| public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId, |
| ContentValues update) { |
| boolean changing = false; |
| final ContentValues values = new ContentValues(); |
| mSelectionArgs1[0] = String.valueOf(dataId); |
| final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?", |
| mSelectionArgs1, null, null, null); |
| try { |
| if (cursor.moveToFirst()) { |
| for (int i = 0; i < cursor.getColumnCount(); i++) { |
| final String key = cursor.getColumnName(i); |
| final String value = cursor.getString(i); |
| if (!changing && update.containsKey(key)) { |
| Object newValue = update.get(key); |
| String newString = newValue == null ? null : newValue.toString(); |
| changing |= !TextUtils.equals(newString, value); |
| } |
| values.put(key, value); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| if (!changing) { |
| return null; |
| } |
| |
| values.putAll(update); |
| return values; |
| } |
| |
| public void triggerAggregation(TransactionContext txContext, long rawContactId) { |
| mContactAggregator.triggerAggregation(txContext, rawContactId); |
| } |
| |
| /** |
| * Test all against {@link TextUtils#isEmpty(CharSequence)}. |
| */ |
| public boolean areAllEmpty(ContentValues values, String[] keys) { |
| for (String key : keys) { |
| if (!TextUtils.isEmpty(values.getAsString(key))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Returns true if a value (possibly null) is specified for at least one of the supplied keys. |
| */ |
| public boolean areAnySpecified(ContentValues values, String[] keys) { |
| for (String key : keys) { |
| if (values.containsKey(key)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |