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