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