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