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