1 /* 2 * Copyright (C) 2022 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.server.appsearch.contactsindexer; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import android.annotation.NonNull; 22 import android.app.appsearch.AppSearchManager; 23 import android.app.appsearch.AppSearchSessionShim; 24 import android.app.appsearch.SetSchemaRequest; 25 import android.app.appsearch.testutil.AppSearchSessionShimImpl; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.ContextWrapper; 29 import android.test.ProviderTestCase2; 30 import android.util.Pair; 31 32 import androidx.test.core.app.ApplicationProvider; 33 34 import com.android.server.appsearch.contactsindexer.ContactsIndexerImpl.ContactsBatcher; 35 import com.android.server.appsearch.contactsindexer.appsearchtypes.Person; 36 37 import com.google.common.collect.ImmutableList; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Objects; 42 import java.util.concurrent.ExecutionException; 43 44 public class ContactsIndexerImplTest extends ProviderTestCase2<FakeContactsProvider> { 45 // TODO(b/203605504) we could just use AppSearchHelper. 46 private FakeAppSearchHelper mAppSearchHelper; 47 private ContactsUpdateStats mUpdateStats; 48 ContactsIndexerImplTest()49 public ContactsIndexerImplTest() { 50 super(FakeContactsProvider.class, FakeContactsProvider.AUTHORITY); 51 } 52 53 @Override setUp()54 public void setUp() throws Exception { 55 super.setUp(); 56 Context context = ApplicationProvider.getApplicationContext(); 57 mContext = new ContextWrapper(context) { 58 @Override 59 public ContentResolver getContentResolver() { 60 return getMockContentResolver(); 61 } 62 }; 63 mAppSearchHelper = new FakeAppSearchHelper(mContext); 64 mUpdateStats = new ContactsUpdateStats(); 65 } 66 67 @Override tearDown()68 public void tearDown() throws Exception { 69 // Wipe the data in AppSearchHelper.DATABASE_NAME. 70 AppSearchManager.SearchContext searchContext = 71 new AppSearchManager.SearchContext.Builder(AppSearchHelper.DATABASE_NAME).build(); 72 AppSearchSessionShim db = AppSearchSessionShimImpl.createSearchSessionAsync( 73 searchContext).get(); 74 SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder() 75 .setForceOverride(true).build(); 76 db.setSchemaAsync(setSchemaRequest).get(); 77 } 78 79 /** 80 * Helper method to run a delta update in the test. 81 * 82 * <p> Get is called on the futures to make this helper method synchronous. 83 * 84 * @param lastUpdatedTimestamp used as the "since" filter for updating the contacts. 85 * @param lastDeletedTimestamp used as the "since" filter for deleting the contacts. 86 * @return new (lastUpdatedTimestamp, lastDeletedTimestamp) pair after the update and deletion. 87 */ runDeltaUpdateOnContactsIndexerImpl( @onNull ContactsIndexerImpl indexerImpl, long lastUpdatedTimestamp, long lastDeletedTimestamp, @NonNull ContactsUpdateStats updateStats)88 private Pair<Long, Long> runDeltaUpdateOnContactsIndexerImpl( 89 @NonNull ContactsIndexerImpl indexerImpl, 90 long lastUpdatedTimestamp, 91 long lastDeletedTimestamp, 92 @NonNull ContactsUpdateStats updateStats) 93 throws ExecutionException, InterruptedException { 94 Objects.requireNonNull(indexerImpl); 95 Objects.requireNonNull(updateStats); 96 List<String> wantedContactIds = new ArrayList<>(); 97 List<String> unWantedContactIds = new ArrayList<>(); 98 99 lastUpdatedTimestamp = ContactsProviderUtil.getUpdatedContactIds(mContext, 100 lastUpdatedTimestamp, ContactsProviderUtil.UPDATE_LIMIT_NONE, 101 wantedContactIds, /*stats=*/ null); 102 lastDeletedTimestamp = ContactsProviderUtil.getDeletedContactIds(mContext, 103 lastDeletedTimestamp, unWantedContactIds, /*stats=*/ null); 104 indexerImpl.updatePersonCorpusAsync(wantedContactIds, unWantedContactIds, 105 updateStats).get(); 106 107 return new Pair<>(lastUpdatedTimestamp, lastDeletedTimestamp); 108 } 109 testBatcher_noFlushBeforeReachingLimit()110 public void testBatcher_noFlushBeforeReachingLimit() throws Exception { 111 int batchSize = 5; 112 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize); 113 114 for (int i = 0; i < batchSize - 1; ++i) { 115 batcher.add(new PersonBuilderHelper(/*id=*/ String.valueOf(i), 116 new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/ 117 String.valueOf(i))).setCreationTimestampMillis(0), mUpdateStats); 118 } 119 batcher.getCompositeFuture().get(); 120 121 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 122 } 123 testBatcher_autoFlush()124 public void testBatcher_autoFlush() throws Exception { 125 int batchSize = 5; 126 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize); 127 128 for (int i = 0; i < batchSize; ++i) { 129 batcher.add( 130 new PersonBuilderHelper( 131 /*id=*/ String.valueOf(i), 132 new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/ 133 String.valueOf(i)) 134 ).setCreationTimestampMillis(0), mUpdateStats); 135 } 136 batcher.getCompositeFuture().get(); 137 138 assertThat(mAppSearchHelper.mIndexedContacts).hasSize(batchSize); 139 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 140 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 141 } 142 testBatcher_contactFingerprintSame_notIndexed()143 public void testBatcher_contactFingerprintSame_notIndexed() throws Exception { 144 int batchSize = 2; 145 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize); 146 PersonBuilderHelper builderHelper1 = new PersonBuilderHelper("id1", 147 new Person.Builder("namespace", "id1", "name1") 148 .setGivenName("given1") 149 ).setCreationTimestampMillis(0); 150 PersonBuilderHelper builderHelper2 = new PersonBuilderHelper("id2", 151 new Person.Builder("namespace", "id2", "name2") 152 .setGivenName("given2") 153 ).setCreationTimestampMillis(0); 154 mAppSearchHelper.setExistingContacts(ImmutableList.of(builderHelper1.buildPerson(), 155 builderHelper2.buildPerson())); 156 157 // Try to add the same contacts 158 batcher.add(builderHelper1, mUpdateStats); 159 batcher.add(builderHelper2, mUpdateStats); 160 batcher.getCompositeFuture().get(); 161 162 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 163 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 164 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 165 } 166 testBatcher_contactFingerprintDifferent_notIndexedButBatched()167 public void testBatcher_contactFingerprintDifferent_notIndexedButBatched() throws Exception { 168 int batchSize = 2; 169 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize); 170 PersonBuilderHelper builderHelper1 = new PersonBuilderHelper("id1", 171 new Person.Builder("namespace", "id1", "name1") 172 .setGivenName("given1") 173 ).setCreationTimestampMillis(0); 174 PersonBuilderHelper builderHelper2 = new PersonBuilderHelper("id2", 175 new Person.Builder("namespace", "id2", "name2") 176 .setGivenName("given2") 177 ).setCreationTimestampMillis(0); 178 mAppSearchHelper.setExistingContacts( 179 ImmutableList.of(builderHelper1.buildPerson(), builderHelper2.buildPerson())); 180 181 PersonBuilderHelper sameAsContact1 = builderHelper1; 182 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 183 // the test does depend on framework-appsearch.impl. 184 PersonBuilderHelper notSameAsContact2 = new PersonBuilderHelper("id2", 185 new Person.Builder("namespace", "id2", "name2").setGivenName( 186 "given2diff") 187 ).setCreationTimestampMillis(0); 188 batcher.add(sameAsContact1, mUpdateStats); 189 batcher.add(notSameAsContact2, mUpdateStats); 190 batcher.getCompositeFuture().get(); 191 192 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 193 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 194 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1); 195 } 196 testBatcher_contactFingerprintDifferent_Indexed()197 public void testBatcher_contactFingerprintDifferent_Indexed() throws Exception { 198 int batchSize = 2; 199 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize); 200 PersonBuilderHelper contact1 = new PersonBuilderHelper("id1", 201 new Person.Builder("namespace", "id1", "name1") 202 .setGivenName("given1") 203 ).setCreationTimestampMillis(0); 204 PersonBuilderHelper contact2 = new PersonBuilderHelper("id2", 205 new Person.Builder("namespace", "id2", "name2") 206 .setGivenName("given2") 207 ).setCreationTimestampMillis(0); 208 PersonBuilderHelper contact3 = new PersonBuilderHelper("id3", 209 new Person.Builder("namespace", "id3", "name3") 210 .setGivenName("given3") 211 ).setCreationTimestampMillis(0); 212 mAppSearchHelper.setExistingContacts( 213 ImmutableList.of(contact1.buildPerson(), contact2.buildPerson(), 214 contact3.buildPerson())); 215 216 PersonBuilderHelper sameAsContact1 = contact1; 217 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 218 // the test does depend on framework-appsearch.impl. 219 PersonBuilderHelper notSameAsContact2 = new PersonBuilderHelper("id2", 220 new Person.Builder("namespace", "id2", "name2").setGivenName( 221 "given2diff") 222 ).setCreationTimestampMillis(0); 223 PersonBuilderHelper notSameAsContact3 = new PersonBuilderHelper("id3", 224 new Person.Builder("namespace", "id3", "name3").setGivenName( 225 "given3diff") 226 ).setCreationTimestampMillis(0); 227 batcher.add(sameAsContact1, mUpdateStats); 228 batcher.add(notSameAsContact2, mUpdateStats); 229 batcher.add(notSameAsContact3, mUpdateStats); 230 batcher.getCompositeFuture().get(); 231 232 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 233 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(1); 234 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1); 235 236 batcher.flushAsync(mUpdateStats).get(); 237 238 assertThat(mAppSearchHelper.mIndexedContacts).containsExactly( 239 notSameAsContact2.buildPerson(), 240 notSameAsContact3.buildPerson()); 241 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 242 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 243 } 244 testBatcher_contactFingerprintDifferent_IndexedWithOriginalCreationTimestamp()245 public void testBatcher_contactFingerprintDifferent_IndexedWithOriginalCreationTimestamp() 246 throws Exception { 247 int batchSize = 2; 248 long originalTs = System.currentTimeMillis(); 249 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize); 250 PersonBuilderHelper contact1 = new PersonBuilderHelper("id1", 251 new Person.Builder("namespace", "id1", "name1") 252 .setGivenName("given1") 253 ).setCreationTimestampMillis(originalTs); 254 PersonBuilderHelper contact2 = new PersonBuilderHelper("id2", 255 new Person.Builder("namespace", "id2", "name2") 256 .setGivenName("given2") 257 ).setCreationTimestampMillis(originalTs); 258 PersonBuilderHelper contact3 = new PersonBuilderHelper("id3", 259 new Person.Builder("namespace", "id3", "name3") 260 .setGivenName("given3") 261 ).setCreationTimestampMillis(originalTs); 262 mAppSearchHelper.setExistingContacts( 263 ImmutableList.of(contact1.buildPerson(), contact2.buildPerson(), 264 contact3.buildPerson())); 265 long updatedTs1 = originalTs + 1; 266 long updatedTs2 = originalTs + 2; 267 long updatedTs3 = originalTs + 3; 268 PersonBuilderHelper sameAsContact1 = new PersonBuilderHelper("id1", 269 new Person.Builder("namespace", "id1", "name1") 270 .setGivenName("given1") 271 ).setCreationTimestampMillis(updatedTs1); 272 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 273 // the test does depend on framework-appsearch.impl. 274 PersonBuilderHelper notSameAsContact2 = new PersonBuilderHelper("id2", 275 new Person.Builder("namespace", "id2", "name2").setGivenName( 276 "given2diff") 277 ).setCreationTimestampMillis(updatedTs2); 278 PersonBuilderHelper notSameAsContact3 = new PersonBuilderHelper("id3", 279 new Person.Builder("namespace", "id3", "name3").setGivenName( 280 "given3diff") 281 ).setCreationTimestampMillis(updatedTs3); 282 283 assertThat(sameAsContact1.buildPerson().getCreationTimestampMillis()).isEqualTo(updatedTs1); 284 assertThat(notSameAsContact2.buildPerson().getCreationTimestampMillis()).isEqualTo( 285 updatedTs2); 286 assertThat(notSameAsContact3.buildPerson().getCreationTimestampMillis()).isEqualTo( 287 updatedTs3); 288 289 batcher.add(sameAsContact1, mUpdateStats); 290 batcher.add(notSameAsContact2, mUpdateStats); 291 batcher.add(notSameAsContact3, mUpdateStats); 292 batcher.flushAsync(mUpdateStats).get(); 293 294 assertThat(mAppSearchHelper.mIndexedContacts).hasSize(2); 295 assertThat(mAppSearchHelper.mExistingContacts.get( 296 "id1").getGivenName()).isEqualTo("given1"); 297 assertThat(mAppSearchHelper.mExistingContacts.get( 298 "id2").getGivenName()).isEqualTo("given2diff"); 299 assertThat(mAppSearchHelper.mExistingContacts.get( 300 "id3").getGivenName()).isEqualTo("given3diff"); 301 // But the timestamps remain same. 302 assertThat(mAppSearchHelper.mExistingContacts.get( 303 "id1").getCreationTimestampMillis()).isEqualTo(originalTs); 304 assertThat(mAppSearchHelper.mExistingContacts.get( 305 "id2").getCreationTimestampMillis()).isEqualTo(originalTs); 306 assertThat(mAppSearchHelper.mExistingContacts.get( 307 "id3").getCreationTimestampMillis()).isEqualTo(originalTs); 308 } 309 testBatcher_contactNew_notIndexedButBatched()310 public void testBatcher_contactNew_notIndexedButBatched() throws Exception { 311 int batchSize = 2; 312 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize); 313 PersonBuilderHelper contact1 = new PersonBuilderHelper("id1", 314 new Person.Builder("namespace", "id1", "name1") 315 .setGivenName("given1") 316 ).setCreationTimestampMillis(0); 317 mAppSearchHelper.setExistingContacts(ImmutableList.of(contact1.buildPerson())); 318 319 PersonBuilderHelper sameAsContact1 = contact1; 320 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 321 // the test does depend on framework-appsearch.impl. 322 PersonBuilderHelper newContact = new PersonBuilderHelper("id2", 323 new Person.Builder("namespace", "id2", "name2").setGivenName( 324 "given2diff") 325 ).setCreationTimestampMillis(0); 326 batcher.add(sameAsContact1, mUpdateStats); 327 batcher.add(newContact, mUpdateStats); 328 batcher.getCompositeFuture().get(); 329 330 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 331 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 332 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1); 333 } 334 testBatcher_contactNew_indexed()335 public void testBatcher_contactNew_indexed() throws Exception { 336 int batchSize = 2; 337 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize); 338 PersonBuilderHelper contact1 = new PersonBuilderHelper("id1", 339 new Person.Builder("namespace", "id1", "name1") 340 .setGivenName("given1")).setCreationTimestampMillis(0); 341 mAppSearchHelper.setExistingContacts(ImmutableList.of(contact1.buildPerson())); 342 343 PersonBuilderHelper sameAsContact1 = contact1; 344 // use toBuilder once it works. Now it is not found due to @hide and not sure how since 345 // the test does depend on framework-appsearch.impl. 346 PersonBuilderHelper newContact1 = new PersonBuilderHelper("id2", 347 new Person.Builder("namespace", "id2", "name2").setGivenName( 348 "given2diff") 349 ).setCreationTimestampMillis(0); 350 PersonBuilderHelper newContact2 = new PersonBuilderHelper("id3", 351 new Person.Builder("namespace", "id3", "name3").setGivenName( 352 "given3diff") 353 ).setCreationTimestampMillis(0); 354 batcher.add(sameAsContact1, mUpdateStats); 355 batcher.add(newContact1, mUpdateStats); 356 batcher.add(newContact2, mUpdateStats); 357 batcher.getCompositeFuture().get(); 358 359 assertThat(mAppSearchHelper.mIndexedContacts).isEmpty(); 360 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(1); 361 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(1); 362 363 batcher.flushAsync(mUpdateStats).get(); 364 365 assertThat(mAppSearchHelper.mIndexedContacts).containsExactly(newContact1.buildPerson(), 366 newContact2.buildPerson()); 367 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 368 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 369 } 370 testBatcher_batchedContactClearedAfterFlush()371 public void testBatcher_batchedContactClearedAfterFlush() throws Exception { 372 int batchSize = 5; 373 ContactsBatcher batcher = new ContactsBatcher(mAppSearchHelper, batchSize); 374 375 // First batch 376 for (int i = 0; i < batchSize; ++i) { 377 batcher.add(new PersonBuilderHelper(/*id=*/ String.valueOf(i), 378 new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/ 379 String.valueOf(i)) 380 ).setCreationTimestampMillis(0), mUpdateStats); 381 } 382 batcher.getCompositeFuture().get(); 383 384 assertThat(mAppSearchHelper.mIndexedContacts).hasSize(batchSize); 385 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 386 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 387 388 389 mAppSearchHelper.mIndexedContacts.clear(); 390 // Second batch. Make sure the first batch has been cleared. 391 for (int i = 0; i < batchSize; ++i) { 392 batcher.add(new PersonBuilderHelper(/*id=*/ String.valueOf(i), 393 new Person.Builder("namespace", /*id=*/ String.valueOf(i), /*name=*/ 394 String.valueOf(i)) 395 // Different from previous ones to bypass the fingerprinting. 396 .addNote("note") 397 ).setCreationTimestampMillis(0), mUpdateStats); 398 } 399 batcher.getCompositeFuture().get(); 400 401 assertThat(mAppSearchHelper.mIndexedContacts).hasSize(batchSize); 402 assertThat(batcher.getPendingDiffContactsCount()).isEqualTo(0); 403 assertThat(batcher.getPendingIndexContactsCount()).isEqualTo(0); 404 } 405 testContactsIndexerImpl_batchRemoveContacts_largerThanBatchSize()406 public void testContactsIndexerImpl_batchRemoveContacts_largerThanBatchSize() throws Exception { 407 ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(mContext, 408 mAppSearchHelper); 409 int totalNum = ContactsIndexerImpl.NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH + 1; 410 List<String> removedIds = new ArrayList<>(totalNum); 411 for (int i = 0; i < totalNum; ++i) { 412 removedIds.add(String.valueOf(i)); 413 } 414 415 contactsIndexerImpl.batchRemoveContactsAsync(removedIds, mUpdateStats).get(); 416 417 assertThat(mAppSearchHelper.mRemovedIds).hasSize(removedIds.size()); 418 assertThat(mAppSearchHelper.mRemovedIds).isEqualTo(removedIds); 419 } 420 testContactsIndexerImpl_batchRemoveContacts_smallerThanBatchSize()421 public void testContactsIndexerImpl_batchRemoveContacts_smallerThanBatchSize() 422 throws Exception { 423 ContactsIndexerImpl contactsIndexerImpl = new ContactsIndexerImpl(mContext, 424 mAppSearchHelper); 425 int totalNum = ContactsIndexerImpl.NUM_DELETED_CONTACTS_PER_BATCH_FOR_APPSEARCH - 1; 426 List<String> removedIds = new ArrayList<>(totalNum); 427 for (int i = 0; i < totalNum; ++i) { 428 removedIds.add(String.valueOf(i)); 429 } 430 431 contactsIndexerImpl.batchRemoveContactsAsync(removedIds, mUpdateStats).get(); 432 433 assertThat(mAppSearchHelper.mRemovedIds).hasSize(removedIds.size()); 434 assertThat(mAppSearchHelper.mRemovedIds).isEqualTo(removedIds); 435 } 436 } 437