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 txContext.markRawContactDirtyAndChanged(rawContactId, callerIsSyncAdapter); 166 167 return true; 168 } 169 170 public boolean hasSearchableData() { 171 return false; 172 } 173 174 public boolean containsSearchableColumns(ContentValues values) { 175 return false; 176 } 177 178 public void appendSearchableData(SearchIndexManager.IndexBuilder builder) { 179 } 180 181 /** 182 * Ensures that all super-primary and primary flags of this raw_contact are 183 * configured correctly 184 */ 185 private void handlePrimaryAndSuperPrimary(ContentValues values, long dataId, 186 long rawContactId) { 187 final boolean hasPrimary = values.getAsInteger(Data.IS_PRIMARY) != null; 188 final boolean hasSuperPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY) != null; 189 190 // Nothing to do? Bail out early 191 if (!hasPrimary && !hasSuperPrimary) return; 192 193 final long mimeTypeId = getMimeTypeId(); 194 195 // Check if we want to clear values 196 final boolean clearPrimary = hasPrimary && 197 values.getAsInteger(Data.IS_PRIMARY) == 0; 198 final boolean clearSuperPrimary = hasSuperPrimary && 199 values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0; 200 201 if (clearPrimary || clearSuperPrimary) { 202 // Test whether these values are currently set 203 mSelectionArgs1[0] = String.valueOf(dataId); 204 final String[] cols = new String[] { Data.IS_PRIMARY, Data.IS_SUPER_PRIMARY }; 205 final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA, 206 cols, Data._ID + "=?", mSelectionArgs1, null, null, null); 207 try { 208 if (c.moveToFirst()) { 209 final boolean isPrimary = c.getInt(0) != 0; 210 final boolean isSuperPrimary = c.getInt(1) != 0; 211 // Clear values if they are currently set 212 if (isSuperPrimary) { 213 mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId); 214 } 215 if (clearPrimary && isPrimary) { 216 mDbHelper.setIsPrimary(rawContactId, -1, mimeTypeId); 217 } 218 } 219 } finally { 220 c.close(); 221 } 222 } else { 223 // Check if we want to set values 224 final boolean setPrimary = hasPrimary && 225 values.getAsInteger(Data.IS_PRIMARY) != 0; 226 final boolean setSuperPrimary = hasSuperPrimary && 227 values.getAsInteger(Data.IS_SUPER_PRIMARY) != 0; 228 if (setSuperPrimary) { 229 // Set both super primary and primary 230 mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId); 231 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId); 232 } else if (setPrimary) { 233 // Primary was explicitly set, but super-primary was not. 234 // In this case we set super-primary on this data item, if 235 // any data item of the same raw-contact already is super-primary 236 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) { 237 mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId); 238 } 239 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId); 240 } 241 } 242 243 // Now that we've taken care of clearing this, remove it from "values". 244 values.remove(Data.IS_SUPER_PRIMARY); 245 values.remove(Data.IS_PRIMARY); 246 } 247 248 public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) { 249 long dataId = c.getLong(DataDeleteQuery._ID); 250 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID); 251 boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0; 252 mSelectionArgs1[0] = String.valueOf(dataId); 253 int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1); 254 mSelectionArgs1[0] = String.valueOf(rawContactId); 255 db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1); 256 if (count != 0 && primary) { 257 fixPrimary(db, rawContactId); 258 } 259 260 if (hasSearchableData()) { 261 txContext.invalidateSearchIndexForRawContact(rawContactId); 262 } 263 264 return count; 265 } 266 267 private void fixPrimary(SQLiteDatabase db, long rawContactId) { 268 long mimeTypeId = getMimeTypeId(); 269 long primaryId = -1; 270 int primaryType = -1; 271 mSelectionArgs1[0] = String.valueOf(rawContactId); 272 Cursor c = db.query(DataDeleteQuery.TABLE, 273 DataDeleteQuery.CONCRETE_COLUMNS, 274 Data.RAW_CONTACT_ID + "=?" + 275 " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId, 276 mSelectionArgs1, null, null, null); 277 try { 278 while (c.moveToNext()) { 279 long dataId = c.getLong(DataDeleteQuery._ID); 280 int type = c.getInt(DataDeleteQuery.DATA1); 281 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) { 282 primaryId = dataId; 283 primaryType = type; 284 } 285 } 286 } finally { 287 c.close(); 288 } 289 if (primaryId != -1) { 290 mDbHelper.setIsPrimary(rawContactId, primaryId, mimeTypeId); 291 } 292 } 293 294 /** 295 * Returns the rank of a specific record type to be used in determining the primary 296 * row. Lower number represents higher priority. 297 */ 298 protected int getTypeRank(int type) { 299 return 0; 300 } 301 302 protected void fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext, 303 long rawContactId) { 304 if (!isNewRawContact(txContext, rawContactId)) { 305 mDbHelper.updateRawContactDisplayName(db, rawContactId); 306 mContactAggregator.updateDisplayNameForRawContact(db, rawContactId); 307 } 308 } 309 310 private boolean isNewRawContact(TransactionContext txContext, long rawContactId) { 311 return txContext.isNewRawContact(rawContactId); 312 } 313 314 /** 315 * Return set of values, using current values at given {@link Data#_ID} 316 * as baseline, but augmented with any updates. Returns null if there is 317 * no change. 318 */ 319 public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId, 320 ContentValues update) { 321 boolean changing = false; 322 final ContentValues values = new ContentValues(); 323 mSelectionArgs1[0] = String.valueOf(dataId); 324 final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?", 325 mSelectionArgs1, null, null, null); 326 try { 327 if (cursor.moveToFirst()) { 328 for (int i = 0; i < cursor.getColumnCount(); i++) { 329 final String key = cursor.getColumnName(i); 330 final String value = cursor.getString(i); 331 if (!changing && update.containsKey(key)) { 332 Object newValue = update.get(key); 333 String newString = newValue == null ? null : newValue.toString(); 334 changing |= !TextUtils.equals(newString, value); 335 } 336 values.put(key, value); 337 } 338 } 339 } finally { 340 cursor.close(); 341 } 342 if (!changing) { 343 return null; 344 } 345 346 values.putAll(update); 347 return values; 348 } 349 350 public void triggerAggregation(TransactionContext txContext, long rawContactId) { 351 mContactAggregator.triggerAggregation(txContext, rawContactId); 352 } 353 354 /** 355 * Test all against {@link TextUtils#isEmpty(CharSequence)}. 356 */ 357 public boolean areAllEmpty(ContentValues values, String[] keys) { 358 for (String key : keys) { 359 if (!TextUtils.isEmpty(values.getAsString(key))) { 360 return false; 361 } 362 } 363 return true; 364 } 365 366 /** 367 * Returns true if a value (possibly null) is specified for at least one of the supplied keys. 368 */ 369 public boolean areAnySpecified(ContentValues values, String[] keys) { 370 for (String key : keys) { 371 if (values.containsKey(key)) { 372 return true; 373 } 374 } 375 return false; 376 } 377 } 378