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