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