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