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.DatabaseUtils;
22import android.database.sqlite.SQLiteDatabase;
23import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
24import android.provider.ContactsContract.Groups;
25import android.provider.ContactsContract.RawContacts;
26
27import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
28import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
29import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
30import com.android.providers.contacts.ContactsDatabaseHelper.Projections;
31import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
32import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
33import com.android.providers.contacts.ContactsProvider2.GroupIdCacheEntry;
34import com.android.providers.contacts.aggregation.ContactAggregator;
35
36import java.util.ArrayList;
37import java.util.HashMap;
38
39/**
40 * Handler for group membership data rows.
41 */
42public class DataRowHandlerForGroupMembership extends DataRowHandler {
43
44    interface RawContactsQuery {
45        String TABLE = Tables.RAW_CONTACTS;
46
47        String[] COLUMNS = new String[] {
48                RawContacts.DELETED,
49                RawContactsColumns.ACCOUNT_ID,
50        };
51
52        int DELETED = 0;
53        int ACCOUNT_ID = 1;
54    }
55
56    private static final String SELECTION_RAW_CONTACT_ID = RawContacts._ID + "=?";
57
58    private static final String QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID =
59            "SELECT COUNT(*) FROM " + Tables.DATA + " LEFT OUTER JOIN " + Tables .GROUPS
60                    + " ON " + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID
61                    + "=" + GroupsColumns.CONCRETE_ID
62                    + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
63                    + " AND " + Tables.DATA + "." + GroupMembership.RAW_CONTACT_ID + "=?"
64                    + " AND " + Groups.FAVORITES + "!=0";
65
66    private final HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache;
67
68    public DataRowHandlerForGroupMembership(Context context, ContactsDatabaseHelper dbHelper,
69            ContactAggregator aggregator,
70            HashMap<String, ArrayList<GroupIdCacheEntry>> groupIdCache) {
71        super(context, dbHelper, aggregator, GroupMembership.CONTENT_ITEM_TYPE);
72        mGroupIdCache = groupIdCache;
73    }
74
75    @Override
76    public long insert(SQLiteDatabase db, TransactionContext txContext, long rawContactId,
77            ContentValues values) {
78        resolveGroupSourceIdInValues(txContext, rawContactId, db, values, true);
79        long dataId = super.insert(db, txContext, rawContactId, values);
80        if (hasFavoritesGroupMembership(db, rawContactId)) {
81            updateRawContactsStar(db, rawContactId, true /* starred */);
82        }
83        updateVisibility(txContext, rawContactId);
84        return dataId;
85    }
86
87    @Override
88    public boolean update(SQLiteDatabase db, TransactionContext txContext, ContentValues values,
89            Cursor c, boolean callerIsSyncAdapter) {
90        long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
91        boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId);
92        resolveGroupSourceIdInValues(txContext, rawContactId, db, values, false);
93        if (!super.update(db, txContext, values, c, callerIsSyncAdapter)) {
94            return false;
95        }
96        boolean isStarred = hasFavoritesGroupMembership(db, rawContactId);
97        if (wasStarred != isStarred) {
98            updateRawContactsStar(db, rawContactId, isStarred);
99        }
100        updateVisibility(txContext, rawContactId);
101        return true;
102    }
103
104    private void updateRawContactsStar(SQLiteDatabase db, long rawContactId, boolean starred) {
105        ContentValues rawContactValues = new ContentValues();
106        rawContactValues.put(RawContacts.STARRED, starred ? 1 : 0);
107        if (db.update(Tables.RAW_CONTACTS, rawContactValues, SELECTION_RAW_CONTACT_ID,
108                new String[]{Long.toString(rawContactId)}) > 0) {
109            mContactAggregator.updateStarred(rawContactId);
110        }
111    }
112
113    private boolean hasFavoritesGroupMembership(SQLiteDatabase db, long rawContactId) {
114        // TODO compiled SQL statement
115        final long groupMembershipMimetypeId = mDbHelper
116                .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
117        boolean isStarred = 0 < DatabaseUtils
118                .longForQuery(db, QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID,
119                new String[]{Long.toString(groupMembershipMimetypeId), Long.toString(rawContactId)});
120        return isStarred;
121    }
122
123    @Override
124    public int delete(SQLiteDatabase db, TransactionContext txContext, Cursor c) {
125        long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
126        boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId);
127        int count = super.delete(db, txContext, c);
128        boolean isStarred = hasFavoritesGroupMembership(db, rawContactId);
129        if (wasStarred && !isStarred) {
130            updateRawContactsStar(db, rawContactId, false /* starred */);
131        }
132        updateVisibility(txContext, rawContactId);
133        return count;
134    }
135
136    private void updateVisibility(TransactionContext txContext, long rawContactId) {
137        long contactId = mDbHelper.getContactId(rawContactId);
138        if (contactId == 0) {
139            return;
140        }
141
142        if (mDbHelper.updateContactVisibleOnlyIfChanged(txContext, contactId)) {
143            mContactAggregator.updateAggregationAfterVisibilityChange(contactId);
144        }
145    }
146
147    private void resolveGroupSourceIdInValues(TransactionContext txContext,
148            long rawContactId, SQLiteDatabase db, ContentValues values, boolean isInsert) {
149        boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
150        boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
151        if (containsGroupSourceId && containsGroupId) {
152            throw new IllegalArgumentException(
153                    "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
154                            + "and GroupMembership.GROUP_ROW_ID");
155        }
156
157        if (!containsGroupSourceId && !containsGroupId) {
158            if (isInsert) {
159                throw new IllegalArgumentException(
160                        "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
161                                + "and GroupMembership.GROUP_ROW_ID");
162            } else {
163                return;
164            }
165        }
166
167        if (containsGroupSourceId) {
168            final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
169            final long groupId = getOrMakeGroup(db, rawContactId, sourceId,
170                    txContext.getAccountIdOrNullForRawContact(rawContactId));
171            values.remove(GroupMembership.GROUP_SOURCE_ID);
172            values.put(GroupMembership.GROUP_ROW_ID, groupId);
173        }
174    }
175
176    /**
177     * Returns the group id of the group with sourceId and the same account as rawContactId.
178     * If the group doesn't already exist then it is first created.
179     *
180     * @param db SQLiteDatabase to use for this operation
181     * @param rawContactId the raw contact this group is associated with
182     * @param sourceId the source ID of the group to query or create
183     * @param accountIdOrNull the account ID for the raw contact.  If null it'll be queried from
184     *    the raw_contacts table.
185     * @return the group id of the existing or created group
186     * @throws IllegalArgumentException if the contact is not associated with an account
187     * @throws IllegalStateException if a group needs to be created but the creation failed
188     */
189    private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId,
190            Long accountIdOrNull) {
191
192        if (accountIdOrNull == null) {
193            mSelectionArgs1[0] = String.valueOf(rawContactId);
194            Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
195                    RawContactsColumns.CONCRETE_ID + "=?", mSelectionArgs1, null, null, null);
196            try {
197                if (c.moveToFirst()) {
198                    accountIdOrNull = c.getLong(RawContactsQuery.ACCOUNT_ID);
199                }
200            } finally {
201                c.close();
202            }
203        }
204
205        if (accountIdOrNull == null) {
206            throw new IllegalArgumentException("Raw contact not found for _ID=" + rawContactId);
207        }
208        final long accountId = accountIdOrNull;
209
210        ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId);
211        if (entries == null) {
212            entries = new ArrayList<GroupIdCacheEntry>(1);
213            mGroupIdCache.put(sourceId, entries);
214        }
215
216        int count = entries.size();
217        for (int i = 0; i < count; i++) {
218            GroupIdCacheEntry entry = entries.get(i);
219            if (entry.accountId == accountId) {
220                return entry.groupId;
221            }
222        }
223
224        GroupIdCacheEntry entry = new GroupIdCacheEntry();
225        entry.accountId = accountId;
226        entry.sourceId = sourceId;
227        entries.add(0, entry);
228
229        // look up the group that contains this sourceId and has the same account as the contact
230        // referred to by rawContactId
231        Cursor c = db.query(Tables.GROUPS, Projections.ID,
232                Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
233                new String[]{sourceId, Long.toString(accountId)}, null, null, null);
234
235        try {
236            if (c.moveToFirst()) {
237                entry.groupId = c.getLong(0);
238            } else {
239                ContentValues groupValues = new ContentValues();
240                groupValues.put(GroupsColumns.ACCOUNT_ID, accountId);
241                groupValues.put(Groups.SOURCE_ID, sourceId);
242                long groupId = db.insert(Tables.GROUPS, null, groupValues);
243                if (groupId < 0) {
244                    throw new IllegalStateException("unable to create a new group with "
245                            + "this sourceid: " + groupValues);
246                }
247                entry.groupId = groupId;
248            }
249        } finally {
250            c.close();
251        }
252
253        return entry.groupId;
254    }
255}
256