• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
32 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
33 import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
34 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
35 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
36 import com.android.providers.contacts.aggregation.AbstractContactAggregator;
37 
38 /**
39  * Handles inserts and update for a specific Data type.
40  */
41 public abstract class DataRowHandler {
42     private static final String TAG = AbstractContactsProvider.TAG;
43 
44     private static final String[] HASH_INPUT_COLUMNS = new String[] {
45             Data.DATA1, Data.DATA2};
46 
47     public interface DataDeleteQuery {
48         public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
49 
50         public static final String[] CONCRETE_COLUMNS = new String[] {
51             DataColumns.CONCRETE_ID,
52             MimetypesColumns.MIMETYPE,
53             Data.RAW_CONTACT_ID,
54             Data.IS_PRIMARY,
55             Data.DATA1,
56         };
57 
58         public static final String[] COLUMNS = new String[] {
59             Data._ID,
60             MimetypesColumns.MIMETYPE,
61             Data.RAW_CONTACT_ID,
62             Data.IS_PRIMARY,
63             Data.DATA1,
64         };
65 
66         public static final int _ID = 0;
67         public static final int MIMETYPE = 1;
68         public static final int RAW_CONTACT_ID = 2;
69         public static final int IS_PRIMARY = 3;
70         public static final int DATA1 = 4;
71     }
72 
73     public interface DataUpdateQuery {
74         String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };
75 
76         int _ID = 0;
77         int RAW_CONTACT_ID = 1;
78         int MIMETYPE = 2;
79     }
80 
81     protected final Context mContext;
82     protected final ContactsDatabaseHelper mDbHelper;
83     protected final AbstractContactAggregator mContactAggregator;
84     protected String[] mSelectionArgs1 = new String[1];
85     protected final String mMimetype;
86     protected long mMimetypeId;
87 
88     @SuppressWarnings("all")
DataRowHandler(Context context, ContactsDatabaseHelper dbHelper, AbstractContactAggregator aggregator, String mimetype)89     public DataRowHandler(Context context, ContactsDatabaseHelper dbHelper,
90             AbstractContactAggregator aggregator, String mimetype) {
91         mContext = context;
92         mDbHelper = dbHelper;
93         mContactAggregator = aggregator;
94         mMimetype = mimetype;
95 
96         // To ensure the data column position. This is dead code if properly configured.
97         if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1
98                 || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
99                 || Email.DATA != Data.DATA1) {
100             throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
101                     + " data is not in DATA1 column");
102         }
103     }
104 
getMimeTypeId()105     protected long getMimeTypeId() {
106         if (mMimetypeId == 0) {
107             mMimetypeId = mDbHelper.getMimeTypeId(mMimetype);
108         }
109         return mMimetypeId;
110     }
111 
112     /**
113      * Inserts a row into the {@link Data} table.
114      */
insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, ContentValues values)115     public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId,
116             ContentValues values) {
117         // Generate hash_id from data1 and data2 columns.
118         // For photo, use data15 column instead of data1 and data2 to generate hash_id.
119         handleHashIdForInsert(values);
120         final long dataId = db.insert(Tables.DATA, null, values);
121 
122         final Integer primary = values.getAsInteger(Data.IS_PRIMARY);
123         final Integer superPrimary = values.getAsInteger(Data.IS_SUPER_PRIMARY);
124         if ((primary != null && primary != 0) || (superPrimary != null && superPrimary != 0)) {
125             final long mimeTypeId = getMimeTypeId();
126             mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
127 
128             // We also have to make sure that no other data item on this raw_contact is
129             // configured super primary
130             if (superPrimary != null) {
131                 if (superPrimary != 0) {
132                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
133                 } else {
134                     mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId);
135                 }
136             } else {
137                 // if there is already another data item configured as super-primary,
138                 // take over the flag (which will automatically remove it from the other item)
139                 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) {
140                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
141                 }
142             }
143         }
144 
145         if (containsSearchableColumns(values)) {
146             txContext.invalidateSearchIndexForRawContact(db, rawContactId);
147         }
148 
149         return dataId;
150     }
151 
152     /**
153      * Validates data and updates a {@link Data} row using the cursor, which contains
154      * the current data.
155      *
156      * @return true if update changed something
157      */
update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter)158     public boolean update(SQLiteDatabase db, TransactionContext txContext,
159             ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
160         long dataId = c.getLong(DataUpdateQuery._ID);
161         long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
162 
163         handlePrimaryAndSuperPrimary(txContext, values, dataId, rawContactId);
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(db, 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)253     private void handlePrimaryAndSuperPrimary(TransactionContext txContext, ContentValues values,
254             long dataId, long rawContactId) {
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         final long mimeTypeId = getMimeTypeId();
262 
263         // Check if we want to clear values
264         final boolean clearPrimary = hasPrimary &&
265                 values.getAsInteger(Data.IS_PRIMARY) == 0;
266         final boolean clearSuperPrimary = hasSuperPrimary &&
267                 values.getAsInteger(Data.IS_SUPER_PRIMARY) == 0;
268 
269         if (clearPrimary || clearSuperPrimary) {
270             // Test whether these values are currently set
271             mSelectionArgs1[0] = String.valueOf(dataId);
272             final String[] cols = new String[] { Data.IS_PRIMARY, Data.IS_SUPER_PRIMARY };
273             final Cursor c = mDbHelper.getReadableDatabase().query(Tables.DATA,
274                     cols, Data._ID + "=?", mSelectionArgs1, null, null, null);
275             try {
276                 if (c.moveToFirst()) {
277                     final boolean isPrimary = c.getInt(0) != 0;
278                     final boolean isSuperPrimary = c.getInt(1) != 0;
279                     // Clear values if they are currently set
280                     if (isSuperPrimary) {
281                         mDbHelper.clearSuperPrimary(rawContactId, mimeTypeId);
282                     }
283                     if (clearPrimary && isPrimary) {
284                         mDbHelper.setIsPrimary(rawContactId, -1, mimeTypeId);
285                     }
286                 }
287             } finally {
288                 c.close();
289             }
290         } else {
291             // Check if we want to set values
292             final boolean setPrimary = hasPrimary &&
293                     values.getAsInteger(Data.IS_PRIMARY) != 0;
294             final boolean setSuperPrimary = hasSuperPrimary &&
295                     values.getAsInteger(Data.IS_SUPER_PRIMARY) != 0;
296             if (setSuperPrimary) {
297                 // Set both super primary and primary
298                 mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
299                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
300             } else if (setPrimary) {
301                 // Primary was explicitly set, but super-primary was not.
302                 // In this case we set super-primary on this data item, if
303                 // any data item of the same raw-contact already is super-primary
304                 if (mDbHelper.rawContactHasSuperPrimary(rawContactId, mimeTypeId)) {
305                     mDbHelper.setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
306                 }
307                 mDbHelper.setIsPrimary(rawContactId, dataId, mimeTypeId);
308             }
309         }
310 
311         // Now that we've taken care of clearing this, remove it from "values".
312         values.remove(Data.IS_SUPER_PRIMARY);
313         values.remove(Data.IS_PRIMARY);
314     }
315 
delete(SQLiteDatabase db, TransactionContext txContext, Cursor c)316     public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) {
317         long dataId = c.getLong(DataDeleteQuery._ID);
318         long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
319         boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
320         mSelectionArgs1[0] = String.valueOf(dataId);
321         int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
322         mSelectionArgs1[0] = String.valueOf(rawContactId);
323         db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
324         if (count != 0 && primary) {
325             fixPrimary(db, rawContactId);
326         }
327 
328         if (hasSearchableData()) {
329             txContext.invalidateSearchIndexForRawContact(db, rawContactId);
330         }
331 
332         return count;
333     }
334 
fixPrimary(SQLiteDatabase db, long rawContactId)335     private void fixPrimary(SQLiteDatabase db, long rawContactId) {
336         long mimeTypeId = getMimeTypeId();
337         long primaryId = -1;
338         int primaryType = -1;
339         mSelectionArgs1[0] = String.valueOf(rawContactId);
340         Cursor c = db.query(DataDeleteQuery.TABLE,
341                 DataDeleteQuery.CONCRETE_COLUMNS,
342                 Data.RAW_CONTACT_ID + "=?" +
343                     " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
344                 mSelectionArgs1, null, null, null);
345         try {
346             while (c.moveToNext()) {
347                 long dataId = c.getLong(DataDeleteQuery._ID);
348                 int type = c.getInt(DataDeleteQuery.DATA1);
349                 if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
350                     primaryId = dataId;
351                     primaryType = type;
352                 }
353             }
354         } finally {
355             c.close();
356         }
357         if (primaryId != -1) {
358             mDbHelper.setIsPrimary(rawContactId, primaryId, mimeTypeId);
359         }
360     }
361 
362     /**
363      * Returns the rank of a specific record type to be used in determining the primary
364      * row. Lower number represents higher priority.
365      */
getTypeRank(int type)366     protected int getTypeRank(int type) {
367         return 0;
368     }
369 
fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext, long rawContactId)370     protected void fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext,
371             long rawContactId) {
372         if (!isNewRawContact(txContext, rawContactId)) {
373             mDbHelper.updateRawContactDisplayName(db, rawContactId);
374             mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
375         }
376     }
377 
isNewRawContact(TransactionContext txContext, long rawContactId)378     private boolean isNewRawContact(TransactionContext txContext, long rawContactId) {
379         return txContext.isNewRawContact(rawContactId);
380     }
381 
382     /**
383      * Return set of values, using current values at given {@link Data#_ID}
384      * as baseline, but augmented with any updates.  Returns null if there is
385      * no change.
386      */
getAugmentedValues(SQLiteDatabase db, long dataId, ContentValues update)387     public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
388             ContentValues update) {
389         boolean changing = false;
390         final ContentValues values = new ContentValues();
391         mSelectionArgs1[0] = String.valueOf(dataId);
392         final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
393                 mSelectionArgs1, null, null, null);
394         try {
395             if (cursor.moveToFirst()) {
396                 for (int i = 0; i < cursor.getColumnCount(); i++) {
397                     final String key = cursor.getColumnName(i);
398                     final String value = cursor.getString(i);
399                     if (!changing && update.containsKey(key)) {
400                         Object newValue = update.get(key);
401                         String newString = newValue == null ? null : newValue.toString();
402                         changing |= !TextUtils.equals(newString, value);
403                     }
404                     values.put(key, value);
405                 }
406             }
407         } finally {
408             cursor.close();
409         }
410         if (!changing) {
411             return null;
412         }
413 
414         values.putAll(update);
415         return values;
416     }
417 
triggerAggregation(TransactionContext txContext, long rawContactId)418     public void triggerAggregation(TransactionContext txContext, long rawContactId) {
419         mContactAggregator.triggerAggregation(txContext, rawContactId);
420     }
421 
422     /**
423      * Test all against {@link TextUtils#isEmpty(CharSequence)}.
424      */
areAllEmpty(ContentValues values, String[] keys)425     public boolean areAllEmpty(ContentValues values, String[] keys) {
426         for (String key : keys) {
427             if (!TextUtils.isEmpty(values.getAsString(key))) {
428                 return false;
429             }
430         }
431         return true;
432     }
433 
434     /**
435      * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
436      */
areAnySpecified(ContentValues values, String[] keys)437     public boolean areAnySpecified(ContentValues values, String[] keys) {
438         for (String key : keys) {
439             if (values.containsKey(key)) {
440                 return true;
441             }
442         }
443         return false;
444     }
445 
applySimpleFieldMaxSize(ContentValues cv, String column)446     protected static void applySimpleFieldMaxSize(ContentValues cv, String column) {
447         final int maxSize = ContactsDatabaseHelper.getSimpleFieldMaxSize();
448         String v = cv.getAsString(column);
449         if (v == null || v.length() <= maxSize) {
450             return;
451         }
452         Log.w(TAG, "Truncating field " + column + ": length=" + v.length() + " max=" + maxSize);
453         cv.put(column, v.substring(0, maxSize));
454     }
455 }
456