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