1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.providers.contacts;
18
19import android.accounts.Account;
20import android.content.ContentProviderOperation;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.OperationApplicationException;
24import android.database.Cursor;
25import android.net.Uri;
26import android.os.RemoteException;
27import android.provider.ContactsContract;
28import android.provider.ContactsContract.AggregationExceptions;
29import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
30import android.provider.ContactsContract.Contacts;
31import android.provider.ContactsContract.Groups;
32import android.provider.ContactsContract.Settings;
33import android.test.suitebuilder.annotation.MediumTest;
34
35import com.google.android.collect.Lists;
36
37import com.android.providers.contacts.testutil.RawContactUtil;
38
39import java.util.ArrayList;
40
41/**
42 * Unit tests for {@link Groups} and {@link GroupMembership}.
43 *
44 * Run the test like this:
45 * <code>
46 * adb shell am instrument -e class com.android.providers.contacts.GroupsTest -w \
47 *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
48 * </code>
49 */
50@MediumTest
51public class GroupsTest extends BaseContactsProvider2Test {
52
53    private static final String GROUP_GREY = "Grey";
54    private static final String GROUP_RED = "Red";
55    private static final String GROUP_GREEN = "Green";
56    private static final String GROUP_BLUE = "Blue";
57
58    private static final String PERSON_ALPHA = "Alpha";
59    private static final String PERSON_BRAVO = "Bravo";
60    private static final String PERSON_CHARLIE = "Charlie";
61    private static final String PERSON_DELTA = "Delta";
62
63    private static final String PHONE_ALPHA = "555-1111";
64    private static final String PHONE_BRAVO_1 = "555-2222";
65    private static final String PHONE_BRAVO_2 = "555-3333";
66    private static final String PHONE_CHARLIE_1 = "555-4444";
67    private static final String PHONE_CHARLIE_2 = "555-5555";
68
69    public void testGroupSummary() {
70
71        // Clear any existing data before starting
72        // TODO make the provider wipe data automatically
73        ((SynchronousContactsProvider2)mActor.provider).wipeData();
74
75        // Create a handful of groups
76        long groupGrey = mActor.createGroup(GROUP_GREY);
77        long groupRed = mActor.createGroup(GROUP_RED);
78        long groupGreen = mActor.createGroup(GROUP_GREEN);
79        long groupBlue = mActor.createGroup(GROUP_BLUE);
80
81        // Create a handful of contacts
82        long contactAlpha = mActor.createRawContact(PERSON_ALPHA);
83        long contactBravo = mActor.createRawContact(PERSON_BRAVO);
84        long contactCharlie = mActor.createRawContact(PERSON_CHARLIE);
85        long contactCharlieDupe = mActor.createRawContact(PERSON_CHARLIE);
86        setAggregationException(
87                AggregationExceptions.TYPE_KEEP_TOGETHER, contactCharlie, contactCharlieDupe);
88        long contactDelta = mActor.createRawContact(PERSON_DELTA);
89
90        assertAggregated(contactCharlie, contactCharlieDupe);
91
92        // Add phone numbers to specific contacts
93        mActor.createPhone(contactAlpha, PHONE_ALPHA);
94        mActor.createPhone(contactBravo, PHONE_BRAVO_1);
95        mActor.createPhone(contactBravo, PHONE_BRAVO_2);
96        mActor.createPhone(contactCharlie, PHONE_CHARLIE_1);
97        mActor.createPhone(contactCharlieDupe, PHONE_CHARLIE_2);
98
99        // Add contacts to various mixture of groups. Grey will have all
100        // contacts, Red only with phone numbers, Green with no phones, and Blue
101        // with no contacts at all.
102        mActor.createGroupMembership(contactAlpha, groupGrey);
103        mActor.createGroupMembership(contactBravo, groupGrey);
104        mActor.createGroupMembership(contactCharlie, groupGrey);
105        mActor.createGroupMembership(contactDelta, groupGrey);
106
107        mActor.createGroupMembership(contactAlpha, groupRed);
108        mActor.createGroupMembership(contactBravo, groupRed);
109        mActor.createGroupMembership(contactCharlie, groupRed);
110
111        mActor.createGroupMembership(contactDelta, groupGreen);
112
113        // Walk across groups summary cursor and verify returned counts.
114        final Cursor cursor = mActor.resolver.query(Groups.CONTENT_SUMMARY_URI,
115                Projections.PROJ_SUMMARY, null, null, null);
116
117        // Require that each group has a summary row
118        assertTrue("Didn't return summary for all groups", (cursor.getCount() == 4));
119
120        while (cursor.moveToNext()) {
121            final long groupId = cursor.getLong(Projections.COL_ID);
122            final int summaryCount = cursor.getInt(Projections.COL_SUMMARY_COUNT);
123            final int summaryWithPhones = cursor.getInt(Projections.COL_SUMMARY_WITH_PHONES);
124
125            if (groupId == groupGrey) {
126                // Grey should have four aggregates, three with phones.
127                assertEquals("Incorrect Grey count", 4, summaryCount);
128                assertEquals("Incorrect Grey with phones count", 3, summaryWithPhones);
129            } else if (groupId == groupRed) {
130                // Red should have 3 aggregates, all with phones.
131                assertEquals("Incorrect Red count", 3, summaryCount);
132                assertEquals("Incorrect Red with phones count", 3, summaryWithPhones);
133            } else if (groupId == groupGreen) {
134                // Green should have 1 aggregate, none with phones.
135                assertEquals("Incorrect Green count", 1, summaryCount);
136                assertEquals("Incorrect Green with phones count", 0, summaryWithPhones);
137            } else if (groupId == groupBlue) {
138                // Blue should have no contacts.
139                assertEquals("Incorrect Blue count", 0, summaryCount);
140                assertEquals("Incorrect Blue with phones count", 0, summaryWithPhones);
141            } else {
142                fail("Unrecognized group in summary cursor");
143            }
144        }
145        cursor.close();
146    }
147
148    public void testGroupDirtySetOnChange() {
149        Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
150                createGroup(mAccount, "gsid1", "title1"));
151        assertDirty(uri, true);
152        clearDirty(uri);
153        assertDirty(uri, false);
154    }
155
156    public void testMarkAsDirtyParameter() {
157        Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
158                createGroup(mAccount, "gsid1", "title1"));
159        clearDirty(uri);
160        Uri updateUri = setCallerIsSyncAdapter(uri, mAccount);
161
162        ContentValues values = new ContentValues();
163        values.put(Groups.NOTES, "New notes");
164        mResolver.update(updateUri, values, null, null);
165        assertDirty(uri, false);
166    }
167
168    public void testGroupDirtyClearedWhenSetExplicitly() {
169        Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
170                createGroup(mAccount, "gsid1", "title1"));
171        assertDirty(uri, true);
172
173        ContentValues values = new ContentValues();
174        values.put(Groups.DIRTY, 0);
175        values.put(Groups.NOTES, "other notes");
176        assertEquals(1, mResolver.update(uri, values, null, null));
177
178        assertDirty(uri, false);
179    }
180
181    public void testGroupDeletion1() {
182        long groupId = createGroup(mAccount, "g1", "gt1");
183        Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
184
185        assertEquals(1, getCount(uri, null, null));
186        mResolver.delete(uri, null, null);
187        assertEquals(1, getCount(uri, null, null));
188        assertStoredValue(uri, Groups.DELETED, "1");
189
190        Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
191        mResolver.delete(permanentDeletionUri, null, null);
192        assertEquals(0, getCount(uri, null, null));
193    }
194
195    public void testGroupDeletion2() {
196        long groupId = createGroup(mAccount, "g1", "gt1");
197        Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
198
199        assertEquals(1, getCount(uri, null, null));
200        Uri permanentDeletionUri = setCallerIsSyncAdapter(uri, mAccount);
201        mResolver.delete(permanentDeletionUri, null, null);
202        assertEquals(0, getCount(uri, null, null));
203    }
204
205    public void testGroupVersionUpdates() {
206        Uri uri = ContentUris.withAppendedId(Groups.CONTENT_URI,
207                createGroup(mAccount, "gsid1", "title1"));
208        long version = getVersion(uri);
209        ContentValues values = new ContentValues();
210        values.put(Groups.TITLE, "title2");
211        mResolver.update(uri, values, null, null);
212        assertEquals(version + 1, getVersion(uri));
213    }
214
215    private interface Projections {
216        public static final String[] PROJ_SUMMARY = new String[] {
217            Groups._ID,
218            Groups.SUMMARY_COUNT,
219            Groups.SUMMARY_WITH_PHONES,
220        };
221
222        public static final int COL_ID = 0;
223        public static final int COL_SUMMARY_COUNT = 1;
224        public static final int COL_SUMMARY_WITH_PHONES = 2;
225    }
226
227    private static final Account sTestAccount = new Account("user@example.com", "com.example");
228    private static final Account sSecondAccount = new Account("other@example.net", "net.example");
229    private static final String GROUP_ID = "testgroup";
230
231    public void assertRawContactVisible(long rawContactId, boolean expected) {
232        final long contactId = this.queryContactId(rawContactId);
233        assertContactVisible(contactId, expected);
234    }
235
236    public void assertContactVisible(long contactId, boolean expected) {
237        final Cursor cursor = mResolver.query(Contacts.CONTENT_URI, new String[] {
238            Contacts.IN_VISIBLE_GROUP
239        }, Contacts._ID + "=" + contactId, null, null);
240        assertTrue("Contact not found", cursor.moveToFirst());
241        final boolean actual = (cursor.getInt(0) != 0);
242        cursor.close();
243        assertEquals("Unexpected visibility", expected, actual);
244    }
245
246    public ContentProviderOperation buildVisibleAssert(long contactId, boolean visible) {
247        return ContentProviderOperation.newAssertQuery(Contacts.CONTENT_URI).withSelection(
248                Contacts._ID + "=" + contactId + " AND " + Contacts.IN_VISIBLE_GROUP + "="
249                        + (visible ? 1 : 0), null).withExpectedCount(1).build();
250    }
251
252    public void testDelayVisibleTransaction() throws RemoteException, OperationApplicationException {
253        final ContentValues values = new ContentValues();
254
255        final long groupId = this.createGroup(sTestAccount, GROUP_ID, GROUP_ID, 1);
256        final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
257
258        // Create contact with specific membership
259        final long rawContactId = RawContactUtil.createRawContact(this.mResolver, sTestAccount);
260        final long contactId = this.queryContactId(rawContactId);
261        final Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
262
263        this.insertGroupMembership(rawContactId, groupId);
264
265        final ArrayList<ContentProviderOperation> oper = Lists.newArrayList();
266
267        // Update visibility inside a transaction and assert that inside the
268        // transaction it hasn't been updated yet.
269        oper.add(buildVisibleAssert(contactId, true));
270        oper.add(ContentProviderOperation.newUpdate(groupUri).withValue(Groups.GROUP_VISIBLE, 0)
271                .build());
272        oper.add(buildVisibleAssert(contactId, true));
273        mResolver.applyBatch(ContactsContract.AUTHORITY, oper);
274
275        // After previous transaction finished, visibility should be updated
276        oper.clear();
277        oper.add(buildVisibleAssert(contactId, false));
278        mResolver.applyBatch(ContactsContract.AUTHORITY, oper);
279    }
280
281    public void testLocalSingleVisible() {
282        final long rawContactId = RawContactUtil.createRawContact(this.mResolver);
283
284        // Single, local contacts should always be visible
285        assertRawContactVisible(rawContactId, true);
286    }
287
288    public void testLocalMixedVisible() {
289        // Aggregate, when mixed with local, should become visible
290        final long rawContactId1 = RawContactUtil.createRawContact(this.mResolver);
291        final long rawContactId2 = RawContactUtil.createRawContact(this.mResolver, sTestAccount);
292
293        final long groupId = this.createGroup(sTestAccount, GROUP_ID, GROUP_ID, 0);
294        this.insertGroupMembership(rawContactId2, groupId);
295
296        // Make sure they are still apart
297        assertNotAggregated(rawContactId1, rawContactId2);
298        assertRawContactVisible(rawContactId1, true);
299        assertRawContactVisible(rawContactId2, false);
300
301        // Force together and see what happens
302        final ContentValues values = new ContentValues();
303        values.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
304        values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
305        values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
306        mResolver.update(AggregationExceptions.CONTENT_URI, values, null, null);
307
308        assertRawContactVisible(rawContactId1, true);
309        assertRawContactVisible(rawContactId2, true);
310    }
311
312    public void testUngroupedVisible() {
313        final long rawContactId = RawContactUtil.createRawContact(this.mResolver, sTestAccount);
314
315        final ContentValues values = new ContentValues();
316        values.put(Settings.ACCOUNT_NAME, sTestAccount.name);
317        values.put(Settings.ACCOUNT_TYPE, sTestAccount.type);
318        values.put(Settings.UNGROUPED_VISIBLE, 0);
319        mResolver.insert(Settings.CONTENT_URI, values);
320
321        assertRawContactVisible(rawContactId, false);
322
323        values.clear();
324        values.put(Settings.UNGROUPED_VISIBLE, 1);
325        mResolver.update(Settings.CONTENT_URI, values, Settings.ACCOUNT_NAME + "=? AND "
326                + Settings.ACCOUNT_TYPE + "=?", new String[] {
327                sTestAccount.name, sTestAccount.type
328        });
329
330        assertRawContactVisible(rawContactId, true);
331    }
332
333    public void testMultipleSourcesVisible() {
334        final long rawContactId1 = RawContactUtil.createRawContact(this.mResolver, sTestAccount);
335        final long rawContactId2 = RawContactUtil.createRawContact(this.mResolver, sSecondAccount);
336
337        final long groupId = this.createGroup(sTestAccount, GROUP_ID, GROUP_ID, 0);
338        this.insertGroupMembership(rawContactId1, groupId);
339
340        // Make sure still invisible
341        assertRawContactVisible(rawContactId1, false);
342        assertRawContactVisible(rawContactId2, false);
343
344        // Make group visible
345        final ContentValues values = new ContentValues();
346        values.put(Groups.GROUP_VISIBLE, 1);
347        mResolver.update(Groups.CONTENT_URI, values, Groups._ID + "=" + groupId, null);
348
349        assertRawContactVisible(rawContactId1, true);
350        assertRawContactVisible(rawContactId2, false);
351
352        // Force them together
353        values.clear();
354        values.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
355        values.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
356        values.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
357        mResolver.update(AggregationExceptions.CONTENT_URI, values, null, null);
358
359        assertRawContactVisible(rawContactId1, true);
360        assertRawContactVisible(rawContactId2, true);
361
362        // Make group invisible
363        values.clear();
364        values.put(Groups.GROUP_VISIBLE, 0);
365        mResolver.update(Groups.CONTENT_URI, values, Groups._ID + "=" + groupId, null);
366
367        assertRawContactVisible(rawContactId1, false);
368        assertRawContactVisible(rawContactId2, false);
369
370        // Turn on ungrouped for first
371        values.clear();
372        values.put(Settings.ACCOUNT_NAME, sTestAccount.name);
373        values.put(Settings.ACCOUNT_TYPE, sTestAccount.type);
374        values.put(Settings.UNGROUPED_VISIBLE, 1);
375        mResolver.insert(Settings.CONTENT_URI, values);
376
377        assertRawContactVisible(rawContactId1, false);
378        assertRawContactVisible(rawContactId2, false);
379
380        // Turn on ungrouped for second account
381        values.clear();
382        values.put(Settings.ACCOUNT_NAME, sSecondAccount.name);
383        values.put(Settings.ACCOUNT_TYPE, sSecondAccount.type);
384        values.put(Settings.UNGROUPED_VISIBLE, 1);
385        mResolver.insert(Settings.CONTENT_URI, values);
386
387        assertRawContactVisible(rawContactId1, true);
388        assertRawContactVisible(rawContactId2, true);
389    }
390}
391