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