1 /*
2  * Copyright (C) 2016 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 
17 package com.android.contacts;
18 
19 import android.content.ContentProviderOperation;
20 import android.content.ContentResolver;
21 import android.content.ContentUris;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.OperationApplicationException;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.os.RemoteException;
29 import android.provider.ContactsContract;
30 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
31 import android.provider.ContactsContract.Data;
32 import android.test.InstrumentationTestCase;
33 import android.test.suitebuilder.annotation.MediumTest;
34 
35 import com.android.contacts.model.account.AccountWithDataSet;
36 
37 import java.util.ArrayList;
38 import java.util.List;
39 
40 /**
41  * Tests of GroupsDaoImpl that perform DB operations directly against CP2
42  */
43 @MediumTest
44 public class GroupsDaoIntegrationTests extends InstrumentationTestCase {
45 
46     private ContentResolver mResolver;
47     private List<Uri> mTestRecords;
48 
49     @Override
setUp()50     protected void setUp() throws Exception {
51         super.setUp();
52 
53         mTestRecords = new ArrayList<>();
54         mResolver = getContext().getContentResolver();
55     }
56 
57     @Override
tearDown()58     protected void tearDown() throws Exception {
59         super.tearDown();
60 
61         // Cleanup anything leftover by the tests.
62         cleanupTestRecords();
63         mTestRecords.clear();
64     }
65 
test_createGroup_createsGroupWithCorrectTitle()66     public void test_createGroup_createsGroupWithCorrectTitle() throws Exception {
67         final ContactSaveService.GroupsDao sut = createDao();
68         final Uri uri = sut.create("Test Create Group", getLocalAccount());
69 
70         assertNotNull(uri);
71         assertGroupHasTitle(uri, "Test Create Group");
72     }
73 
test_deleteEmptyGroup_marksRowDeleted()74     public void test_deleteEmptyGroup_marksRowDeleted() throws Exception {
75         final ContactSaveService.GroupsDao sut = createDao();
76         final Uri uri = sut.create("Test Delete Group", getLocalAccount());
77 
78         assertEquals(1, sut.delete(uri));
79 
80         final Cursor cursor = mResolver.query(uri, null, null, null, null, null);
81         try {
82             cursor.moveToFirst();
83             assertEquals(1, cursor.getInt(cursor.getColumnIndexOrThrow(
84                     ContactsContract.Groups.DELETED)));
85         } finally {
86             cursor.close();
87         }
88     }
89 
test_undoDeleteEmptyGroup_createsGroupWithMatchingTitle()90     public void test_undoDeleteEmptyGroup_createsGroupWithMatchingTitle() throws Exception {
91         final ContactSaveService.GroupsDao sut = createDao();
92         final Uri uri = sut.create("Test Undo Delete Empty Group", getLocalAccount());
93 
94         final Bundle undoData = sut.captureDeletionUndoData(uri);
95 
96         assertEquals(1, sut.delete(uri));
97 
98         final Uri groupUri = sut.undoDeletion(undoData);
99 
100         assertGroupHasTitle(groupUri, "Test Undo Delete Empty Group");
101     }
102 
test_deleteNonEmptyGroup_removesGroupAndMembers()103     public void test_deleteNonEmptyGroup_removesGroupAndMembers() throws Exception {
104         final ContactSaveService.GroupsDao sut = createDao();
105         final Uri groupUri = sut.create("Test delete non-empty group", getLocalAccount());
106 
107         final long groupId = ContentUris.parseId(groupUri);
108         addMemberToGroup(ContentUris.parseId(createRawContact()), groupId);
109         addMemberToGroup(ContentUris.parseId(createRawContact()), groupId);
110 
111         assertEquals(1, sut.delete(groupUri));
112 
113         final Cursor cursor = mResolver.query(Data.CONTENT_URI, null,
114                 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
115                 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(groupId) },
116                 null, null);
117 
118         try {
119             cursor.moveToFirst();
120             // This is more of a characterization test since our code isn't manually deleting
121             // the membership rows just the group but this still helps document the expected
122             // behavior.
123             assertEquals(0, cursor.getCount());
124         } finally {
125             cursor.close();
126         }
127     }
128 
test_undoDeleteNonEmptyGroup_restoresGroupAndMembers()129     public void test_undoDeleteNonEmptyGroup_restoresGroupAndMembers() throws Exception {
130         final ContactSaveService.GroupsDao sut = createDao();
131         final Uri groupUri = sut.create("Test undo delete non-empty group", getLocalAccount());
132 
133         final long groupId = ContentUris.parseId(groupUri);
134         addMemberToGroup(ContentUris.parseId(createRawContact()), groupId);
135         addMemberToGroup(ContentUris.parseId(createRawContact()), groupId);
136 
137         final Bundle undoData = sut.captureDeletionUndoData(groupUri);
138 
139         sut.delete(groupUri);
140 
141         final Uri recreatedGroup = sut.undoDeletion(undoData);
142 
143         final long newGroupId = ContentUris.parseId(recreatedGroup);
144 
145         final Cursor cursor = mResolver.query(Data.CONTENT_URI, null,
146                 Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
147                 new String[] { GroupMembership.CONTENT_ITEM_TYPE, String.valueOf(newGroupId) },
148                 null, null);
149 
150         try {
151             assertEquals(2, cursor.getCount());
152         } finally {
153             cursor.close();
154         }
155     }
156 
test_captureUndoDataForDeletedGroup_returnsEmptyBundle()157     public void test_captureUndoDataForDeletedGroup_returnsEmptyBundle() {
158         final ContactSaveService.GroupsDao sut = createDao();
159 
160         final Uri uri = sut.create("a deleted group", getLocalAccount());
161         sut.delete(uri);
162 
163         final Bundle undoData = sut.captureDeletionUndoData(uri);
164 
165         assertTrue(undoData.isEmpty());
166     }
167 
test_captureUndoDataForNonExistentGroup_returnsEmptyBundle()168     public void test_captureUndoDataForNonExistentGroup_returnsEmptyBundle() {
169         final ContactSaveService.GroupsDao sut = createDao();
170 
171         // This test could potentially be flaky if this ID exists for some reason. 10 is subtracted
172         // to reduce the likelihood of this happening; some other test may use Integer.MAX_VALUE
173         // or nearby values  to cover some special case or boundary condition.
174         final long nonExistentId = Integer.MAX_VALUE - 10;
175 
176         final Bundle undoData = sut.captureDeletionUndoData(ContentUris
177                 .withAppendedId(ContactsContract.Groups.CONTENT_URI, nonExistentId));
178 
179         assertTrue(undoData.isEmpty());
180     }
181 
test_undoWithEmptyBundle_doesNothing()182     public void test_undoWithEmptyBundle_doesNothing() {
183         final ContactSaveService.GroupsDao sut = createDao();
184 
185         final Uri uri = sut.undoDeletion(new Bundle());
186 
187         assertNull(uri);
188     }
189 
test_undoDeleteEmptyGroupWithMissingMembersKey_shouldRecreateGroup()190     public void test_undoDeleteEmptyGroupWithMissingMembersKey_shouldRecreateGroup() {
191         final ContactSaveService.GroupsDao sut = createDao();
192         final Uri groupUri = sut.create("Test undo delete null memberIds", getLocalAccount());
193 
194         final Bundle undoData = sut.captureDeletionUndoData(groupUri);
195         undoData.remove(ContactSaveService.GroupsDaoImpl.KEY_GROUP_MEMBERS);
196         sut.delete(groupUri);
197 
198         sut.undoDeletion(undoData);
199 
200         assertGroupWithTitleExists("Test undo delete null memberIds");
201     }
202 
assertGroupHasTitle(Uri groupUri, String title)203     private void assertGroupHasTitle(Uri groupUri, String title) {
204         final Cursor cursor = mResolver.query(groupUri,
205                 new String[] { ContactsContract.Groups.TITLE },
206                 ContactsContract.Groups.DELETED + "=?",
207                 new String[] { "0" }, null, null);
208         try {
209             assertTrue("Group does not have title \"" + title + "\"",
210                     cursor.getCount() == 1 && cursor.moveToFirst() &&
211                             title.equals(cursor.getString(0)));
212         } finally {
213             cursor.close();
214         }
215     }
216 
assertGroupWithTitleExists(String title)217     private void assertGroupWithTitleExists(String title) {
218         final Cursor cursor = mResolver.query(ContactsContract.Groups.CONTENT_URI, null,
219                 ContactsContract.Groups.TITLE + "=? AND " +
220                         ContactsContract.Groups.DELETED + "=?",
221                 new String[] { title, "0" }, null, null);
222         try {
223             assertTrue("No group exists with title \"" + title + "\"", cursor.getCount() > 0);
224         } finally {
225             cursor.close();
226         }
227     }
228 
createDao()229     public ContactSaveService.GroupsDao createDao() {
230         return new GroupsDaoWrapper(new ContactSaveService.GroupsDaoImpl(getContext()));
231     }
232 
createRawContact()233     private Uri createRawContact() {
234         final ContentValues values = new ContentValues();
235         values.putNull(ContactsContract.RawContacts.ACCOUNT_NAME);
236         values.putNull(ContactsContract.RawContacts.ACCOUNT_TYPE);
237         final Uri result = mResolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
238         mTestRecords.add(result);
239         return result;
240     }
241 
addMemberToGroup(long rawContactId, long groupId)242     private Uri addMemberToGroup(long rawContactId, long groupId) {
243         final ContentValues values = new ContentValues();
244         values.put(Data.RAW_CONTACT_ID, rawContactId);
245         values.put(Data.MIMETYPE,
246                 GroupMembership.CONTENT_ITEM_TYPE);
247         values.put(GroupMembership.GROUP_ROW_ID, groupId);
248 
249         // Dont' need to add to testRecords because it will be cleaned up when parent raw_contact
250         // is deleted.
251         return mResolver.insert(Data.CONTENT_URI, values);
252     }
253 
getContext()254     private Context getContext() {
255         return getInstrumentation().getTargetContext();
256     }
257 
getLocalAccount()258     private AccountWithDataSet getLocalAccount() {
259         return new AccountWithDataSet(null, null, null);
260     }
261 
cleanupTestRecords()262     private void cleanupTestRecords() throws RemoteException, OperationApplicationException {
263         final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
264         for (Uri uri : mTestRecords) {
265             if (uri == null) continue;
266             ops.add(ContentProviderOperation
267                     .newDelete(uri.buildUpon()
268                             .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
269                             .build())
270                     .build());
271         }
272         mResolver.applyBatch(ContactsContract.AUTHORITY, ops);
273     }
274 
275     private class GroupsDaoWrapper implements ContactSaveService.GroupsDao {
276         private final ContactSaveService.GroupsDao mDelegate;
277 
GroupsDaoWrapper(ContactSaveService.GroupsDao delegate)278         public GroupsDaoWrapper(ContactSaveService.GroupsDao delegate) {
279             mDelegate = delegate;
280         }
281 
282         @Override
create(String title, AccountWithDataSet account)283         public Uri create(String title, AccountWithDataSet account) {
284             final Uri result = mDelegate.create(title, account);
285             mTestRecords.add(result);
286             return result;
287         }
288 
289         @Override
delete(Uri groupUri)290         public int delete(Uri groupUri) {
291             return mDelegate.delete(groupUri);
292         }
293 
294         @Override
captureDeletionUndoData(Uri groupUri)295         public Bundle captureDeletionUndoData(Uri groupUri) {
296             return mDelegate.captureDeletionUndoData(groupUri);
297         }
298 
299         @Override
undoDeletion(Bundle undoData)300         public Uri undoDeletion(Bundle undoData) {
301             final Uri result = mDelegate.undoDeletion(undoData);
302             mTestRecords.add(result);
303             return result;
304         }
305     }
306 }
307