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")
DataRowHandler(Context context, ContactsDatabaseHelper dbHelper, ContactAggregator aggregator, String mimetype)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 
getMimeTypeId()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      */
insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId, ContentValues values)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      */
update(SQLiteDatabase db, TransactionContext txContext, ContentValues values, Cursor c, boolean callerIsSyncAdapter)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 
hasSearchableData()170     public boolean hasSearchableData() {
171         return false;
172     }
173 
containsSearchableColumns(ContentValues values)174     public boolean containsSearchableColumns(ContentValues values) {
175         return false;
176     }
177 
appendSearchableData(SearchIndexManager.IndexBuilder builder)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      */
handlePrimaryAndSuperPrimary(ContentValues values, long dataId, long rawContactId)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 
delete(SQLiteDatabase db, TransactionContext txContext, Cursor c)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 
fixPrimary(SQLiteDatabase db, long rawContactId)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      */
getTypeRank(int type)298     protected int getTypeRank(int type) {
299         return 0;
300     }
301 
fixRawContactDisplayName(SQLiteDatabase db, TransactionContext txContext, long rawContactId)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 
isNewRawContact(TransactionContext txContext, long rawContactId)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      */
getAugmentedValues(SQLiteDatabase db, long dataId, ContentValues update)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 
triggerAggregation(TransactionContext txContext, long rawContactId)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      */
areAllEmpty(ContentValues values, String[] keys)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      */
areAnySpecified(ContentValues values, String[] keys)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