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")
DataRowHandler(Context context, ContactsDatabaseHelper dbHelper, AbstractContactAggregator aggregator, String mimetype)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 
getMimeTypeId()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      */
insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, ContentValues values)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      */
update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter, boolean callerIsMetadataSyncAdapter)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 
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, boolean callerIsMetadataSyncAdapter)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 
delete(SQLiteDatabase db, TransactionContext txContext, Cursor c)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 
fixPrimary(SQLiteDatabase db, long rawContactId)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      */
getTypeRank(int type)369     protected int getTypeRank(int type) {
370         return 0;
371     }
372 
fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext, long rawContactId)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 
isNewRawContact(TransactionContext txContext, long rawContactId)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      */
getAugmentedValues(SQLiteDatabase db, long dataId, ContentValues update)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 
triggerAggregation(TransactionContext txContext, long rawContactId)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      */
areAllEmpty(ContentValues values, String[] keys)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      */
areAnySpecified(ContentValues values, String[] keys)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