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 
125             // We also have to make sure that no other data item on this raw_contact is
126             // configured super primary
127             if (superPrimary != null) {
128                 if (superPrimary != 0) {
129                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
130                 } else {
131                     mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId);
132                 }
133             } else {
134                 // if there is already another data item configured as super-primary,
135                 // take over the flag (which will automatically remove it from the other item)
136                 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) {
137                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
138                 }
139             }
140         }
141 
142         if (containsSearchableColumns(values)) {
143             txContext.invalidateSearchIndexForRawContact(rawContactId);
144         }
145 
146         return dataId;
147     }
148 
149     /**
150      * Validates data and updates a {@link Data} row using the cursor, which contains
151      * the current data.
152      *
153      * @return true if update changed something
154      */
update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter)155     public boolean update(SQLiteDatabase db, TransactionContext txContext,
156             ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
157         long dataId = c.getLong(DataUpdateQuery._ID);
158         long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
159 
160         handlePrimaryAndSuperPrimary(txContext, values, dataId, rawContactId);
161         handleHashIdForUpdate(values, dataId);
162 
163         if (values.size() > 0) {
164             mSelectionArgs1[0] = String.valueOf(dataId);
165             db.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1);
166         }
167 
168         if (containsSearchableColumns(values)) {
169             txContext.invalidateSearchIndexForRawContact(rawContactId);
170         }
171 
172         txContext.markRawContactDirtyAndChanged(rawContactId, callerIsSyncAdapter);
173 
174         return true;
175     }
176 
hasSearchableData()177     public boolean hasSearchableData() {
178         return false;
179     }
180 
containsSearchableColumns(ContentValues values)181     public boolean containsSearchableColumns(ContentValues values) {
182         return false;
183     }
184 
appendSearchableData(SearchIndexManager.IndexBuilder builder)185     public void appendSearchableData(SearchIndexManager.IndexBuilder builder) {
186     }
187 
188     /**
189      * Fetch data1, data2, and data15 from values if they exist, and generate hash_id
190      * if one of data1 and data2 columns is set, otherwise using data15 instead.
191      * hash_id is null if all of these three field is null.
192      * Add hash_id key to values.
193      */
handleHashIdForInsert(ContentValues values)194     public void handleHashIdForInsert(ContentValues values) {
195         final String data1 = values.getAsString(Data.DATA1);
196         final String data2 = values.getAsString(Data.DATA2);
197         final String photoHashId= mDbHelper.getPhotoHashId();
198 
199         String hashId;
200         if (ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE.equals(mMimetype)) {
201             hashId = photoHashId;
202         } else if (!TextUtils.isEmpty(data1) || !TextUtils.isEmpty(data2)) {
203             hashId = mDbHelper.generateHashId(data1, data2);
204         } else {
205             hashId = null;
206         }
207         if (TextUtils.isEmpty(hashId)) {
208             values.putNull(Data.HASH_ID);
209         } else {
210             values.put(Data.HASH_ID, hashId);
211         }
212     }
213 
214     /**
215      * Compute hash_id column and add it to values.
216      * If this is not a photo field, and one of data1 and data2 changed, re-compute hash_id with new
217      * data1 and data2.
218      * If this is a photo field, no need to change hash_id.
219      */
handleHashIdForUpdate(ContentValues values, long dataId)220     private void handleHashIdForUpdate(ContentValues values, long dataId) {
221         if (!ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE.equals(mMimetype)
222                 && (values.containsKey(Data.DATA1) || values.containsKey(Data.DATA2))) {
223             String data1 = values.getAsString(Data.DATA1);
224             String data2 = values.getAsString(Data.DATA2);
225             mSelectionArgs1[0] = String.valueOf(dataId);
226             final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA,
227                     HASH_INPUT_COLUMNS, Data._ID + "=?", mSelectionArgs1, null, null, null);
228             try {
229                 if (c.moveToFirst()) {
230                     data1 = values.containsKey(Data.DATA1) ? data1 : c.getString(0);
231                     data2 = values.containsKey(Data.DATA2) ? data2 : c.getString(1);
232                 }
233             } finally {
234                 c.close();
235             }
236 
237             String hashId = mDbHelper.generateHashId(data1, data2);
238             if (TextUtils.isEmpty(hashId)) {
239                 values.putNull(Data.HASH_ID);
240             } else {
241                 values.put(Data.HASH_ID, hashId);
242             }
243         }
244     }
245 
246     /**
247      * Ensures that all super-primary and primary flags of this raw_contact are
248      * configured correctly
249      */
handlePrimaryAndSuperPrimary(TransactionContext txContext, ContentValues values, long dataId, long rawContactId)250     private void handlePrimaryAndSuperPrimary(TransactionContext txContext, ContentValues values,
251             long dataId, long rawContactId) {
252         final boolean hasPrimary = values.getAsInteger(Data.IS_PRIMARY) != null;
253         final boolean hasSuperPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY) != null;
254 
255         // Nothing to do? Bail out early
256         if (!hasPrimary && !hasSuperPrimary) return;
257 
258         final long mimeTypeId = getMimeTypeId();
259 
260         // Check if we want to clear values
261         final boolean clearPrimary = hasPrimary &&
262                 values.getAsInteger(Data.IS_PRIMARY) == 0;
263         final boolean clearSuperPrimary = hasSuperPrimary &&
264                 values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0;
265 
266         if (clearPrimary || clearSuperPrimary) {
267             // Test whether these values are currently set
268             mSelectionArgs1[0] = String.valueOf(dataId);
269             final String[] cols = new String[] { Data.IS_PRIMARY, Data.IS_SUPER_PRIMARY };
270             final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA,
271                     cols, Data._ID + "=?", mSelectionArgs1, null, null, null);
272             try {
273                 if (c.moveToFirst()) {
274                     final boolean isPrimary = c.getInt(0) != 0;
275                     final boolean isSuperPrimary = c.getInt(1) != 0;
276                     // Clear values if they are currently set
277                     if (isSuperPrimary) {
278                         mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId);
279                     }
280                     if (clearPrimary && isPrimary) {
281                         mDbHelper.setIsPrimary(rawContactId, -1, mimeTypeId);
282                     }
283                 }
284             } finally {
285                 c.close();
286             }
287         } else {
288             // Check if we want to set values
289             final boolean setPrimary = hasPrimary &&
290                     values.getAsInteger(Data.IS_PRIMARY) != 0;
291             final boolean setSuperPrimary = hasSuperPrimary &&
292                     values.getAsInteger(Data.IS_SUPER_PRIMARY) != 0;
293             if (setSuperPrimary) {
294                 // Set both super primary and primary
295                 mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
296                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
297             } else if (setPrimary) {
298                 // Primary was explicitly set, but super-primary was not.
299                 // In this case we set super-primary on this data item, if
300                 // any data item of the same raw-contact already is super-primary
301                 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) {
302                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
303                 }
304                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
305             }
306         }
307 
308         // Now that we've taken care of clearing this, remove it from "values".
309         values.remove(Data.IS_SUPER_PRIMARY);
310         values.remove(Data.IS_PRIMARY);
311     }
312 
delete(SQLiteDatabase db, TransactionContext txContext, Cursor c)313     public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) {
314         long dataId = c.getLong(DataDeleteQuery._ID);
315         long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
316         boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
317         mSelectionArgs1[0] = String.valueOf(dataId);
318         int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
319         mSelectionArgs1[0] = String.valueOf(rawContactId);
320         db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
321         if (count != 0 && primary) {
322             fixPrimary(db, rawContactId);
323         }
324 
325         if (hasSearchableData()) {
326             txContext.invalidateSearchIndexForRawContact(rawContactId);
327         }
328 
329         return count;
330     }
331 
fixPrimary(SQLiteDatabase db, long rawContactId)332     private void fixPrimary(SQLiteDatabase db, long rawContactId) {
333         long mimeTypeId = getMimeTypeId();
334         long primaryId = -1;
335         int primaryType = -1;
336         mSelectionArgs1[0] = String.valueOf(rawContactId);
337         Cursor c = db.query(DataDeleteQuery.TABLE,
338                 DataDeleteQuery.CONCRETE_COLUMNS,
339                 Data.RAW_CONTACT_ID + "=?" +
340                     " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
341                 mSelectionArgs1, null, null, null);
342         try {
343             while (c.moveToNext()) {
344                 long dataId = c.getLong(DataDeleteQuery._ID);
345                 int type = c.getInt(DataDeleteQuery.DATA1);
346                 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
347                     primaryId = dataId;
348                     primaryType = type;
349                 }
350             }
351         } finally {
352             c.close();
353         }
354         if (primaryId != -1) {
355             mDbHelper.setIsPrimary(rawContactId, primaryId, mimeTypeId);
356         }
357     }
358 
359     /**
360      * Returns the rank of a specific record type to be used in determining the primary
361      * row. Lower number represents higher priority.
362      */
getTypeRank(int type)363     protected int getTypeRank(int type) {
364         return 0;
365     }
366 
fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext, long rawContactId)367     protected void fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext,
368             long rawContactId) {
369         if (!isNewRawContact(txContext, rawContactId)) {
370             mDbHelper.updateRawContactDisplayName(db, rawContactId);
371             mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
372         }
373     }
374 
isNewRawContact(TransactionContext txContext, long rawContactId)375     private boolean isNewRawContact(TransactionContext txContext, long rawContactId) {
376         return txContext.isNewRawContact(rawContactId);
377     }
378 
379     /**
380      * Return set of values, using current values at given {@link Data#_ID}
381      * as baseline, but augmented with any updates.  Returns null if there is
382      * no change.
383      */
getAugmentedValues(SQLiteDatabase db, long dataId, ContentValues update)384     public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
385             ContentValues update) {
386         boolean changing = false;
387         final ContentValues values = new ContentValues();
388         mSelectionArgs1[0] = String.valueOf(dataId);
389         final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
390                 mSelectionArgs1, null, null, null);
391         try {
392             if (cursor.moveToFirst()) {
393                 for (int i = 0; i < cursor.getColumnCount(); i++) {
394                     final String key = cursor.getColumnName(i);
395                     final String value = cursor.getString(i);
396                     if (!changing && update.containsKey(key)) {
397                         Object newValue = update.get(key);
398                         String newString = newValue == null ? null : newValue.toString();
399                         changing |= !TextUtils.equals(newString, value);
400                     }
401                     values.put(key, value);
402                 }
403             }
404         } finally {
405             cursor.close();
406         }
407         if (!changing) {
408             return null;
409         }
410 
411         values.putAll(update);
412         return values;
413     }
414 
triggerAggregation(TransactionContext txContext, long rawContactId)415     public void triggerAggregation(TransactionContext txContext, long rawContactId) {
416         mContactAggregator.triggerAggregation(txContext, rawContactId);
417     }
418 
419     /**
420      * Test all against {@link TextUtils#isEmpty(CharSequence)}.
421      */
areAllEmpty(ContentValues values, String[] keys)422     public boolean areAllEmpty(ContentValues values, String[] keys) {
423         for (String key : keys) {
424             if (!TextUtils.isEmpty(values.getAsString(key))) {
425                 return false;
426             }
427         }
428         return true;
429     }
430 
431     /**
432      * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
433      */
areAnySpecified(ContentValues values, String[] keys)434     public boolean areAnySpecified(ContentValues values, String[] keys) {
435         for (String key : keys) {
436             if (values.containsKey(key)) {
437                 return true;
438             }
439         }
440         return false;
441     }
442 }
443