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 */
16package com.android.providers.contacts;
17
18import android.content.ContentValues;
19import android.content.Context;
20import android.database.Cursor;
21import android.database.sqlite.SQLiteDatabase;
22import android.provider.ContactsContract.CommonDataKinds.Email;
23import android.provider.ContactsContract.CommonDataKinds.Nickname;
24import android.provider.ContactsContract.CommonDataKinds.Organization;
25import android.provider.ContactsContract.CommonDataKinds.Phone;
26import android.provider.ContactsContract.CommonDataKinds.StructuredName;
27import android.provider.ContactsContract.Data;
28import android.text.TextUtils;
29
30import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
31import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
32import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
33import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
34import com.android.providers.contacts.aggregation.ContactAggregator;
35
36/**
37 * Handles inserts and update for a specific Data type.
38 */
39public 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")
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
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     */
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     */
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
170    public boolean hasSearchableData() {
171        return false;
172    }
173
174    public boolean containsSearchableColumns(ContentValues values) {
175        return false;
176    }
177
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     */
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
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
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     */
298    protected int getTypeRank(int type) {
299        return 0;
300    }
301
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
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     */
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
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     */
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     */
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