• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.adservices.service.customaudience;
18 
19 import static android.adservices.common.AdServicesStatusUtils.STATUS_INTERNAL_ERROR;
20 import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
21 
22 import static com.android.adservices.common.CommonFlagsValues.EXTENDED_FLEDGE_BACKGROUND_FETCH_NETWORK_CONNECT_TIMEOUT_MS;
23 import static com.android.adservices.common.CommonFlagsValues.EXTENDED_FLEDGE_BACKGROUND_FETCH_NETWORK_READ_TIMEOUT_MS;
24 
25 import static com.google.common.truth.Truth.assertThat;
26 import static com.google.common.util.concurrent.Futures.immediateFuture;
27 import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
28 
29 import static org.junit.Assert.assertThrows;
30 import static org.junit.Assert.assertTrue;
31 import static org.mockito.ArgumentMatchers.anyInt;
32 import static org.mockito.ArgumentMatchers.eq;
33 import static org.mockito.Mockito.any;
34 import static org.mockito.Mockito.anyLong;
35 import static org.mockito.Mockito.doAnswer;
36 import static org.mockito.Mockito.doReturn;
37 import static org.mockito.Mockito.doThrow;
38 import static org.mockito.Mockito.never;
39 import static org.mockito.Mockito.times;
40 import static org.mockito.Mockito.verify;
41 import static org.mockito.Mockito.when;
42 
43 import android.adservices.common.CommonFixture;
44 import android.annotation.NonNull;
45 import android.content.Context;
46 import android.content.pm.PackageManager;
47 
48 import androidx.room.Room;
49 import androidx.test.core.app.ApplicationProvider;
50 
51 import com.android.adservices.LoggerFactory;
52 import com.android.adservices.common.AdServicesDeviceSupportedRule;
53 import com.android.adservices.concurrency.AdServicesExecutors;
54 import com.android.adservices.customaudience.DBCustomAudienceBackgroundFetchDataFixture;
55 import com.android.adservices.data.adselection.AppInstallDao;
56 import com.android.adservices.data.adselection.SharedStorageDatabase;
57 import com.android.adservices.data.customaudience.CustomAudienceDao;
58 import com.android.adservices.data.customaudience.CustomAudienceDatabase;
59 import com.android.adservices.data.customaudience.DBCustomAudience;
60 import com.android.adservices.data.customaudience.DBCustomAudienceBackgroundFetchData;
61 import com.android.adservices.data.enrollment.EnrollmentDao;
62 import com.android.adservices.service.FakeFlagsFactory;
63 import com.android.adservices.service.Flags;
64 import com.android.adservices.service.customaudience.BackgroundFetchRunner.UpdateResultType;
65 import com.android.adservices.service.stats.AdServicesLoggerImpl;
66 import com.android.adservices.service.stats.BackgroundFetchExecutionLogger;
67 import com.android.adservices.service.stats.CustomAudienceLoggerFactory;
68 import com.android.adservices.service.stats.UpdateCustomAudienceExecutionLogger;
69 import com.android.adservices.shared.testing.AnswerSyncCallback;
70 import com.android.adservices.shared.testing.concurrency.SimpleSyncCallback;
71 
72 import com.google.common.util.concurrent.FluentFuture;
73 import com.google.common.util.concurrent.ListenableFuture;
74 
75 import org.junit.Before;
76 import org.junit.Rule;
77 import org.junit.Test;
78 import org.mockito.Mock;
79 import org.mockito.Mockito;
80 import org.mockito.junit.MockitoJUnit;
81 import org.mockito.junit.MockitoRule;
82 
83 import java.time.Clock;
84 import java.time.Instant;
85 import java.util.ArrayList;
86 import java.util.Arrays;
87 import java.util.List;
88 import java.util.concurrent.CountDownLatch;
89 import java.util.concurrent.ExecutionException;
90 import java.util.concurrent.ExecutorService;
91 import java.util.concurrent.Executors;
92 import java.util.concurrent.TimeUnit;
93 import java.util.concurrent.TimeoutException;
94 import java.util.concurrent.atomic.AtomicInteger;
95 
96 public class BackgroundFetchWorkerTest {
97     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
98     private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
99 
100     private final Flags mFlags = new BackgroundFetchWorkerTestFlags(true);
101     private final ExecutorService mExecutorService = Executors.newFixedThreadPool(8);
102 
103     @Rule(order = 1)
104     public final AdServicesDeviceSupportedRule deviceSupportRule =
105             new AdServicesDeviceSupportedRule();
106 
107     @Rule(order = 2)
108     public final MockitoRule mockitoRule = MockitoJUnit.rule();
109 
110     @Mock private PackageManager mPackageManagerMock;
111     @Mock private EnrollmentDao mEnrollmentDaoMock;
112     @Mock private Clock mClockMock;
113     @Mock private AdServicesLoggerImpl mAdServicesLoggerImplMock;
114     @Mock private UpdateCustomAudienceExecutionLogger mUpdateCustomAudienceExecutionLoggerMock;
115     private BackgroundFetchExecutionLogger mBackgroundFetchExecutionLoggerSpy;
116     @Mock private CustomAudienceLoggerFactory mCustomAudienceLoggerFactoryMock;
117 
118     private CustomAudienceDao mCustomAudienceDaoSpy;
119     private AppInstallDao mAppInstallDaoSpy;
120     private BackgroundFetchRunner mBackgroundFetchRunnerSpy;
121     private BackgroundFetchWorker mBackgroundFetchWorker;
122 
123     @Before
setup()124     public void setup() {
125         when(mCustomAudienceLoggerFactoryMock.getUpdateCustomAudienceExecutionLogger())
126                 .thenReturn(mUpdateCustomAudienceExecutionLoggerMock);
127         mBackgroundFetchExecutionLoggerSpy =
128                 Mockito.spy(
129                         new BackgroundFetchExecutionLogger(
130                                 com.android.adservices.shared.util.Clock.getInstance(),
131                                 mAdServicesLoggerImplMock));
132         when(mCustomAudienceLoggerFactoryMock.getBackgroundFetchExecutionLogger())
133                 .thenReturn(mBackgroundFetchExecutionLoggerSpy);
134 
135         mCustomAudienceDaoSpy =
136                 Mockito.spy(
137                         Room.inMemoryDatabaseBuilder(CONTEXT, CustomAudienceDatabase.class)
138                                 .addTypeConverter(new DBCustomAudience.Converters(true, true, true))
139                                 .build()
140                                 .customAudienceDao());
141         mAppInstallDaoSpy =
142                 Mockito.spy(
143                         Room.inMemoryDatabaseBuilder(CONTEXT, SharedStorageDatabase.class)
144                                 .build()
145                                 .appInstallDao());
146 
147         mBackgroundFetchRunnerSpy =
148                 Mockito.spy(
149                         new BackgroundFetchRunner(
150                                 mCustomAudienceDaoSpy,
151                                 mAppInstallDaoSpy,
152                                 mPackageManagerMock,
153                                 mEnrollmentDaoMock,
154                                 mFlags,
155                                 mCustomAudienceLoggerFactoryMock));
156 
157         mBackgroundFetchWorker =
158                 new BackgroundFetchWorker(
159                         mCustomAudienceDaoSpy,
160                         mFlags,
161                         mBackgroundFetchRunnerSpy,
162                         mClockMock,
163                         mCustomAudienceLoggerFactoryMock);
164     }
165 
166     @Test
testBackgroundFetchWorkerNullInputsCauseFailure()167     public void testBackgroundFetchWorkerNullInputsCauseFailure() {
168         assertThrows(
169                 NullPointerException.class,
170                 () ->
171                         new BackgroundFetchWorker(
172                                 null,
173                                 FakeFlagsFactory.getFlagsForTest(),
174                                 mBackgroundFetchRunnerSpy,
175                                 mClockMock,
176                                 mCustomAudienceLoggerFactoryMock));
177 
178         assertThrows(
179                 NullPointerException.class,
180                 () ->
181                         new BackgroundFetchWorker(
182                                 mCustomAudienceDaoSpy,
183                                 null,
184                                 mBackgroundFetchRunnerSpy,
185                                 mClockMock,
186                                 mCustomAudienceLoggerFactoryMock));
187 
188         assertThrows(
189                 NullPointerException.class,
190                 () ->
191                         new BackgroundFetchWorker(
192                                 mCustomAudienceDaoSpy,
193                                 FakeFlagsFactory.getFlagsForTest(),
194                                 null,
195                                 mClockMock,
196                                 mCustomAudienceLoggerFactoryMock));
197 
198         assertThrows(
199                 NullPointerException.class,
200                 () ->
201                         new BackgroundFetchWorker(
202                                 mCustomAudienceDaoSpy,
203                                 FakeFlagsFactory.getFlagsForTest(),
204                                 mBackgroundFetchRunnerSpy,
205                                 null,
206                                 mCustomAudienceLoggerFactoryMock));
207         assertThrows(
208                 NullPointerException.class,
209                 () ->
210                         new BackgroundFetchWorker(
211                                 mCustomAudienceDaoSpy,
212                                 FakeFlagsFactory.getFlagsForTest(),
213                                 mBackgroundFetchRunnerSpy,
214                                 mClockMock,
215                                 null));
216     }
217 
218     @Test
testRunBackgroundFetchThrowsTimeoutDuringUpdates()219     public void testRunBackgroundFetchThrowsTimeoutDuringUpdates() throws InterruptedException {
220         class FlagsWithSmallTimeout implements Flags {
221             @Override
222             public long getFledgeBackgroundFetchJobMaxRuntimeMs() {
223                 return 100L;
224             }
225         }
226 
227         class BackgroundFetchRunnerWithSleep extends BackgroundFetchRunner {
228             BackgroundFetchRunnerWithSleep(
229                     @NonNull CustomAudienceDao customAudienceDao, @NonNull Flags flags) {
230                 super(
231                         customAudienceDao,
232                         mAppInstallDaoSpy,
233                         mPackageManagerMock,
234                         mEnrollmentDaoMock,
235                         flags,
236                         mCustomAudienceLoggerFactoryMock);
237             }
238 
239             @Override
240             public void deleteExpiredCustomAudiences(@NonNull Instant jobStartTime) {
241                 // Do nothing
242             }
243 
244             @Override
245             public FluentFuture<UpdateResultType> updateCustomAudience(
246                     @NonNull Instant jobStartTime,
247                     @NonNull DBCustomAudienceBackgroundFetchData fetchData) {
248 
249                 return FluentFuture.from(
250                         AdServicesExecutors.getBlockingExecutor()
251                                 .submit(
252                                         () -> {
253                                             try {
254                                                 Thread.sleep(500L);
255                                             } catch (InterruptedException e) {
256                                                 e.printStackTrace();
257                                             }
258                                             return null;
259                                         }));
260             }
261         }
262 
263         Flags flagsWithSmallTimeout = new FlagsWithSmallTimeout();
264         BackgroundFetchRunner backgroundFetchRunnerWithSleep =
265                 new BackgroundFetchRunnerWithSleep(mCustomAudienceDaoSpy, flagsWithSmallTimeout);
266         BackgroundFetchWorker backgroundFetchWorkerThatTimesOut =
267                 new BackgroundFetchWorker(
268                         mCustomAudienceDaoSpy,
269                         flagsWithSmallTimeout,
270                         backgroundFetchRunnerWithSleep,
271                         mClockMock,
272                         mCustomAudienceLoggerFactoryMock);
273 
274         // Mock a custom audience eligible for update
275         DBCustomAudienceBackgroundFetchData fetchData =
276                 DBCustomAudienceBackgroundFetchDataFixture.getValidBuilderByBuyer(
277                                 CommonFixture.VALID_BUYER_1)
278                         .setEligibleUpdateTime(CommonFixture.FIXED_NOW)
279                         .build();
280         List<DBCustomAudienceBackgroundFetchData> fetchDataList = Arrays.asList(fetchData);
281         doReturn(fetchDataList)
282                 .when(mCustomAudienceDaoSpy)
283                 .getActiveEligibleCustomAudienceBackgroundFetchData(any(), anyLong());
284 
285         when(mClockMock.instant()).thenReturn(Instant.now());
286 
287         // Ensure that log is closed after timeout.
288         CountDownLatch closeLoggerLatch = new CountDownLatch(1);
289         doAnswer(
290                         unusedInovcation -> {
291                             closeLoggerLatch.countDown();
292                             return null;
293                         })
294                 .when(mBackgroundFetchExecutionLoggerSpy)
295                 .close(anyInt(), anyInt());
296 
297         // Time out while updating custom audiences
298         ExecutionException expected =
299                 assertThrows(
300                         ExecutionException.class,
301                         () -> backgroundFetchWorkerThatTimesOut.runBackgroundFetch().get());
302         assertThat(expected.getCause()).isInstanceOf(TimeoutException.class);
303         // Wait for logger to close.
304         assertThat(closeLoggerLatch.await(200, TimeUnit.MILLISECONDS)).isTrue();
305         // Test can timeout during cleanup also hence only verify internal error due to failure in
306         // runner.
307         verify(mBackgroundFetchExecutionLoggerSpy).close(anyInt(), eq(STATUS_INTERNAL_ERROR));
308     }
309 
310     @Test
311     public void testRunBackgroundFetchNothingToUpdate()
312             throws ExecutionException, InterruptedException {
313         assertTrue(
314                 mCustomAudienceDaoSpy
315                         .getActiveEligibleCustomAudienceBackgroundFetchData(
316                                 CommonFixture.FIXED_NOW, 1)
317                         .isEmpty());
318 
319         CountDownLatch latchForExecutionLoggerClose = new CountDownLatch(1);
320         setLatchToCountdownOnLogClose(latchForExecutionLoggerClose);
321         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW);
322 
323         mBackgroundFetchWorker.runBackgroundFetch().get();
324 
325         // ensures that we verify only after BackgroundExecuterLoggerClose#close is executed.
326         latchForExecutionLoggerClose.await();
327         verify(mBackgroundFetchRunnerSpy).deleteExpiredCustomAudiences(any());
328         verify(mCustomAudienceDaoSpy).deleteAllExpiredCustomAudienceData(any());
329         verify(mBackgroundFetchRunnerSpy).deleteDisallowedOwnerCustomAudiences();
330         verify(mCustomAudienceDaoSpy).deleteAllDisallowedOwnerCustomAudienceData(any(), any());
331         verify(mBackgroundFetchRunnerSpy).deleteDisallowedBuyerCustomAudiences();
332         verify(mBackgroundFetchRunnerSpy).deleteDisallowedPackageAppInstallEntries();
333         verify(mCustomAudienceDaoSpy).deleteAllDisallowedBuyerCustomAudienceData(any(), any());
334         verify(mBackgroundFetchRunnerSpy, never()).updateCustomAudience(any(), any());
335         verify(mBackgroundFetchExecutionLoggerSpy).close(0, STATUS_SUCCESS);
336     }
337 
338     @Test
339     public void testRunBackgroundFetchNothingToUpdateNoFilters()
340             throws ExecutionException, InterruptedException {
341         Flags flagsFilteringDisabled = new BackgroundFetchWorkerTestFlags(false);
342         mBackgroundFetchWorker =
343                 new BackgroundFetchWorker(
344                         mCustomAudienceDaoSpy,
345                         flagsFilteringDisabled,
346                         mBackgroundFetchRunnerSpy,
347                         mClockMock,
348                         mCustomAudienceLoggerFactoryMock);
349         assertTrue(
350                 mCustomAudienceDaoSpy
351                         .getActiveEligibleCustomAudienceBackgroundFetchData(
352                                 CommonFixture.FIXED_NOW, 1)
353                         .isEmpty());
354 
355         CountDownLatch latchForExecutionLoggerClose = new CountDownLatch(1);
356         setLatchToCountdownOnLogClose(latchForExecutionLoggerClose);
357         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW);
358 
359         mBackgroundFetchWorker.runBackgroundFetch().get();
360 
361         // ensures that we verify only after BackgroundExecuterLoggerClose#close is executed.
362         latchForExecutionLoggerClose.await();
363         verify(mBackgroundFetchRunnerSpy).deleteExpiredCustomAudiences(any());
364         verify(mCustomAudienceDaoSpy).deleteAllExpiredCustomAudienceData(any());
365         verify(mBackgroundFetchRunnerSpy).deleteDisallowedOwnerCustomAudiences();
366         verify(mCustomAudienceDaoSpy).deleteAllDisallowedOwnerCustomAudienceData(any(), any());
367         verify(mBackgroundFetchRunnerSpy).deleteDisallowedBuyerCustomAudiences();
368         verify(mBackgroundFetchRunnerSpy, times(0)).deleteDisallowedPackageAppInstallEntries();
369         verify(mCustomAudienceDaoSpy).deleteAllDisallowedBuyerCustomAudienceData(any(), any());
370         verify(mBackgroundFetchRunnerSpy, never()).updateCustomAudience(any(), any());
371         verify(mBackgroundFetchExecutionLoggerSpy).close(0, STATUS_SUCCESS);
372     }
373 
374     @Test
375     public void testRunBackgroundFetchUpdateOneCustomAudience()
376             throws ExecutionException, InterruptedException {
377         // Mock a single custom audience eligible for update
378         DBCustomAudienceBackgroundFetchData fetchData =
379                 DBCustomAudienceBackgroundFetchDataFixture.getValidBuilderByBuyer(
380                                 CommonFixture.VALID_BUYER_1)
381                         .setEligibleUpdateTime(CommonFixture.FIXED_NOW)
382                         .build();
383         List<DBCustomAudienceBackgroundFetchData> fetchDataList = Arrays.asList(fetchData);
384         doReturn(fetchDataList)
385                 .when(mCustomAudienceDaoSpy)
386                 .getActiveEligibleCustomAudienceBackgroundFetchData(any(), anyLong());
387         doReturn(FluentFuture.from(immediateFuture(null)))
388                 .when(mBackgroundFetchRunnerSpy)
389                 .updateCustomAudience(any(), any());
390 
391         CountDownLatch latchForExecutionLoggerClose = new CountDownLatch(1);
392         setLatchToCountdownOnLogClose(latchForExecutionLoggerClose);
393         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW);
394         mBackgroundFetchWorker.runBackgroundFetch().get();
395 
396         // ensures that we verify only after BackgroundExecuterLoggerClose#close is executed.
397         latchForExecutionLoggerClose.await();
398         verify(mBackgroundFetchRunnerSpy).deleteExpiredCustomAudiences(any());
399         verify(mCustomAudienceDaoSpy).deleteAllExpiredCustomAudienceData(any());
400         verify(mBackgroundFetchRunnerSpy).deleteDisallowedOwnerCustomAudiences();
401         verify(mCustomAudienceDaoSpy).deleteAllDisallowedOwnerCustomAudienceData(any(), any());
402         verify(mBackgroundFetchRunnerSpy).deleteDisallowedBuyerCustomAudiences();
403         verify(mBackgroundFetchRunnerSpy).deleteDisallowedPackageAppInstallEntries();
404         verify(mCustomAudienceDaoSpy).deleteAllDisallowedBuyerCustomAudienceData(any(), any());
405         verify(mBackgroundFetchRunnerSpy).updateCustomAudience(any(), any());
406         verify(mBackgroundFetchExecutionLoggerSpy).close(fetchDataList.size(), STATUS_SUCCESS);
407     }
408 
409     @Test
410     public void testRunBackgroundFetchUpdateCustomAudiences()
411             throws ExecutionException, InterruptedException {
412         int numEligibleCustomAudiences = 12;
413 
414         // Mock a list of custom audiences eligible for update
415         DBCustomAudienceBackgroundFetchData.Builder fetchDataBuilder =
416                 DBCustomAudienceBackgroundFetchDataFixture.getValidBuilderByBuyer(
417                                 CommonFixture.VALID_BUYER_1)
418                         .setEligibleUpdateTime(CommonFixture.FIXED_NOW);
419         List<DBCustomAudienceBackgroundFetchData> fetchDataList = new ArrayList<>();
420         for (int i = 0; i < numEligibleCustomAudiences; i++) {
421             fetchDataList.add(fetchDataBuilder.setName("ca" + i).build());
422         }
423 
424         doReturn(fetchDataList)
425                 .when(mCustomAudienceDaoSpy)
426                 .getActiveEligibleCustomAudienceBackgroundFetchData(any(), anyLong());
427         doReturn(FluentFuture.from(immediateFuture(null)))
428                 .when(mBackgroundFetchRunnerSpy)
429                 .updateCustomAudience(any(), any());
430 
431         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW);
432         CountDownLatch latchForExecutionLoggerClose = new CountDownLatch(1);
433         setLatchToCountdownOnLogClose(latchForExecutionLoggerClose);
434         mBackgroundFetchWorker.runBackgroundFetch().get();
435 
436         // ensures that we verify only after BackgroundExecuterLoggerClose#close is executed.
437         latchForExecutionLoggerClose.await();
438         verify(mBackgroundFetchRunnerSpy).deleteExpiredCustomAudiences(any());
439         verify(mCustomAudienceDaoSpy).deleteAllExpiredCustomAudienceData(any());
440         verify(mBackgroundFetchRunnerSpy).deleteDisallowedOwnerCustomAudiences();
441         verify(mCustomAudienceDaoSpy).deleteAllDisallowedOwnerCustomAudienceData(any(), any());
442         verify(mBackgroundFetchRunnerSpy).deleteDisallowedBuyerCustomAudiences();
443         verify(mBackgroundFetchRunnerSpy).deleteDisallowedPackageAppInstallEntries();
444         verify(mCustomAudienceDaoSpy).deleteAllDisallowedBuyerCustomAudienceData(any(), any());
445         verify(mBackgroundFetchRunnerSpy, times(numEligibleCustomAudiences))
446                 .updateCustomAudience(any(), any());
447         verify(mBackgroundFetchExecutionLoggerSpy).close(fetchDataList.size(), STATUS_SUCCESS);
448     }
449 
450     @Test
451     public void testRunBackgroundFetchChecksWorkInProgress()
452             throws InterruptedException, ExecutionException {
453         int numEligibleCustomAudiences = 16;
454         CountDownLatch partialCompletionLatch = new CountDownLatch(numEligibleCustomAudiences / 4);
455 
456         // Mock a list of custom audiences eligible for update
457         DBCustomAudienceBackgroundFetchData.Builder fetchDataBuilder =
458                 DBCustomAudienceBackgroundFetchDataFixture.getValidBuilderByBuyer(
459                                 CommonFixture.VALID_BUYER_1)
460                         .setEligibleUpdateTime(CommonFixture.FIXED_NOW);
461         List<DBCustomAudienceBackgroundFetchData> fetchDataList = new ArrayList<>();
462         for (int i = 0; i < numEligibleCustomAudiences; i++) {
463             fetchDataList.add(fetchDataBuilder.setName("ca" + i).build());
464         }
465 
466         doReturn(fetchDataList)
467                 .when(mCustomAudienceDaoSpy)
468                 .getActiveEligibleCustomAudienceBackgroundFetchData(any(), anyLong());
469         doAnswer(
470                         unusedInvocation -> {
471                             Thread.sleep(100);
472                             partialCompletionLatch.countDown();
473                             return FluentFuture.from(immediateFuture(null));
474                         })
475                 .when(mBackgroundFetchRunnerSpy)
476                 .updateCustomAudience(any(), any());
477 
478         // ensures that we verify only after BackgroundExecuterLoggerClose#close is executed.
479         CountDownLatch latchForExecutionLoggerClose = new CountDownLatch(1);
480         setLatchToCountdownOnLogClose(latchForExecutionLoggerClose);
481         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW);
482 
483         CountDownLatch bgfWorkStoppedLatch = new CountDownLatch(1);
484         mExecutorService.execute(
485                 () -> {
486                     try {
487                         mBackgroundFetchWorker.runBackgroundFetch().get();
488                     } catch (Exception exception) {
489                         sLogger.e(
490                                 exception, "Exception encountered while running background fetch");
491                     } finally {
492                         bgfWorkStoppedLatch.countDown();
493                     }
494                 });
495 
496         // Wait til updates are partially complete, then try running background fetch again and
497         // verify nothing is done
498         partialCompletionLatch.await();
499         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW.plusSeconds(1));
500         mBackgroundFetchWorker.runBackgroundFetch().get();
501 
502         bgfWorkStoppedLatch.await();
503         latchForExecutionLoggerClose.await();
504         verify(mBackgroundFetchRunnerSpy).deleteExpiredCustomAudiences(any());
505         verify(mCustomAudienceDaoSpy).deleteAllExpiredCustomAudienceData(any());
506         verify(mBackgroundFetchRunnerSpy).deleteDisallowedOwnerCustomAudiences();
507         verify(mCustomAudienceDaoSpy).deleteAllDisallowedOwnerCustomAudienceData(any(), any());
508         verify(mBackgroundFetchRunnerSpy).deleteDisallowedBuyerCustomAudiences();
509         verify(mBackgroundFetchRunnerSpy).deleteDisallowedPackageAppInstallEntries();
510         verify(mCustomAudienceDaoSpy).deleteAllDisallowedBuyerCustomAudienceData(any(), any());
511         verify(mBackgroundFetchRunnerSpy, times(numEligibleCustomAudiences))
512                 .updateCustomAudience(any(), any());
513         verify(mBackgroundFetchExecutionLoggerSpy).close(fetchDataList.size(), STATUS_SUCCESS);
514     }
515 
516     @Test
517     public void testStopWorkWithoutRunningFetchDoesNothing() {
518         // Verify no errors/exceptions thrown when no work in progress
519         mBackgroundFetchWorker.stopWork();
520     }
521 
522     @Test
523     public void testStopWorkGracefullyStopsBackgroundFetch() throws Exception {
524         int numEligibleCustomAudiences = 16;
525         CountDownLatch partialCompletionLatch = new CountDownLatch(numEligibleCustomAudiences / 4);
526 
527         // Mock a list of custom audiences eligible for update
528         DBCustomAudienceBackgroundFetchData.Builder fetchDataBuilder =
529                 DBCustomAudienceBackgroundFetchDataFixture.getValidBuilderByBuyer(
530                                 CommonFixture.VALID_BUYER_1)
531                         .setEligibleUpdateTime(CommonFixture.FIXED_NOW);
532         List<DBCustomAudienceBackgroundFetchData> fetchDataList = new ArrayList<>();
533         for (int i = 0; i < numEligibleCustomAudiences; i++) {
534             fetchDataList.add(fetchDataBuilder.setName("ca" + i).build());
535         }
536 
537         doReturn(fetchDataList)
538                 .when(mCustomAudienceDaoSpy)
539                 .getActiveEligibleCustomAudienceBackgroundFetchData(any(), anyLong());
540         doAnswer(
541                         unusedInvocation -> {
542                             Thread.sleep(100);
543                             partialCompletionLatch.countDown();
544                             return FluentFuture.from(immediateVoidFuture());
545                         })
546                 .when(mBackgroundFetchRunnerSpy)
547                 .updateCustomAudience(any(), any());
548 
549         // Ensure that logger is closed after stopping work.
550         CountDownLatch closeLoggerLatch = new CountDownLatch(1);
551         doAnswer(
552                         unusedInovcation -> {
553                             closeLoggerLatch.countDown();
554                             return null;
555                         })
556                 .when(mBackgroundFetchExecutionLoggerSpy)
557                 .close(anyInt(), anyInt());
558 
559         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW);
560 
561         ListenableFuture<Void> backgroundFetchResult = mBackgroundFetchWorker.runBackgroundFetch();
562 
563         // Wait til updates are partially complete, then try stopping background fetch
564         partialCompletionLatch.await();
565         mBackgroundFetchWorker.stopWork();
566         // stopWork() should notify to the worker that the work should end so the future
567         // should complete within the time required to update the custom audiences
568         backgroundFetchResult.get(
569                 100 * (numEligibleCustomAudiences * 3 / 4) + 100, TimeUnit.SECONDS);
570         // Wait for logger to close.
571         assertThat(closeLoggerLatch.await(200, TimeUnit.MILLISECONDS)).isTrue();
572         verify(mBackgroundFetchExecutionLoggerSpy)
573                 .close(numEligibleCustomAudiences, STATUS_SUCCESS);
574     }
575 
576     @Test
577     public void testStopWorkPreemptsDataUpdates() throws Exception {
578         int numEligibleCustomAudiences = 16;
579         CountDownLatch beforeUpdatingCasLatch = new CountDownLatch(numEligibleCustomAudiences / 4);
580 
581         // Mock a list of custom audiences eligible for update
582         DBCustomAudienceBackgroundFetchData.Builder fetchDataBuilder =
583                 DBCustomAudienceBackgroundFetchDataFixture.getValidBuilderByBuyer(
584                                 CommonFixture.VALID_BUYER_1)
585                         .setEligibleUpdateTime(CommonFixture.FIXED_NOW);
586         List<DBCustomAudienceBackgroundFetchData> fetchDataList = new ArrayList<>();
587         for (int i = 0; i < numEligibleCustomAudiences; i++) {
588             fetchDataList.add(fetchDataBuilder.setName("ca" + i).build());
589         }
590 
591         // Ensuring that stopWork is called before the data update process
592         doAnswer(
593                         unusedInvocation -> {
594                             beforeUpdatingCasLatch.await();
595                             return fetchDataList;
596                         })
597                 .when(mCustomAudienceDaoSpy)
598                 .getActiveEligibleCustomAudienceBackgroundFetchData(any(), anyLong());
599         doAnswer(
600                         unusedInvocation -> {
601                             Thread.sleep(100);
602                             return null;
603                         })
604                 .when(mBackgroundFetchRunnerSpy)
605                 .updateCustomAudience(any(), any());
606 
607         // Ensure that logger is closed after stopping work.
608         CountDownLatch closeLoggerLatch = new CountDownLatch(1);
609         doAnswer(
610                         unusedInovcation -> {
611                             closeLoggerLatch.countDown();
612                             return null;
613                         })
614                 .when(mBackgroundFetchExecutionLoggerSpy)
615                 .close(anyInt(), anyInt());
616 
617         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW);
618 
619         ListenableFuture<Void> backgroundFetchResult = mBackgroundFetchWorker.runBackgroundFetch();
620 
621         // Wait til updates are partially complete, then try stopping background fetch
622         mBackgroundFetchWorker.stopWork();
623         beforeUpdatingCasLatch.countDown();
624         // stopWork() called before updating the data should cause immediate termination
625         // waiting for 200ms to handle thread scheduling delays.
626         // The important check is that the time is less than the time of updating all CAs
627         backgroundFetchResult.get(200, TimeUnit.MILLISECONDS);
628         // Wait for logger to close.
629         assertThat(closeLoggerLatch.await(200, TimeUnit.MILLISECONDS)).isTrue();
630         verify(mBackgroundFetchExecutionLoggerSpy).close(0, STATUS_SUCCESS);
631     }
632 
633     @Test
634     public void testRunBackgroundFetchInSequence() throws InterruptedException, ExecutionException {
635         int numEligibleCustomAudiences = 16;
636         int expectedUpdateCustomAudienceCalls = numEligibleCustomAudiences / 2;
637 
638         // Mock two lists of custom audiences eligible for update
639         DBCustomAudienceBackgroundFetchData.Builder fetchDataBuilder =
640                 DBCustomAudienceBackgroundFetchDataFixture.getValidBuilderByBuyer(
641                                 CommonFixture.VALID_BUYER_1)
642                         .setEligibleUpdateTime(CommonFixture.FIXED_NOW);
643         List<DBCustomAudienceBackgroundFetchData> fetchDataList1 = new ArrayList<>();
644         List<DBCustomAudienceBackgroundFetchData> fetchDataList2 = new ArrayList<>();
645         for (int i = 0; i < numEligibleCustomAudiences; i++) {
646             DBCustomAudienceBackgroundFetchData fetchData =
647                     fetchDataBuilder.setName("ca" + i).build();
648             if (i < expectedUpdateCustomAudienceCalls) {
649                 fetchDataList1.add(fetchData);
650             } else {
651                 fetchDataList2.add(fetchData);
652             }
653         }
654 
655         // Count the number of times updateCustomAudience is run
656         AtomicInteger completionCount = new AtomicInteger(0);
657 
658         // Return the first list the first time, and the second list in the second call
659         AnswerSyncCallback<FluentFuture<UpdateResultType>> updateCustomAudienceCallback =
660                 AnswerSyncCallback.forMultipleAnswers(
661                         FluentFuture.from(immediateFuture(null)),
662                         expectedUpdateCustomAudienceCalls);
663 
664         doReturn(fetchDataList1)
665                 .doReturn(fetchDataList2)
666                 .when(mCustomAudienceDaoSpy)
667                 .getActiveEligibleCustomAudienceBackgroundFetchData(any(), anyLong());
668         doAnswer(updateCustomAudienceCallback)
669                 .when(mBackgroundFetchRunnerSpy)
670                 .updateCustomAudience(any(), any());
671 
672         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW);
673 
674         // TODO(b/321743128): Clarify the production behavior for the logger and use existed mocking
675         // method setLatchToCountdownOnLogClose().
676         //
677         // Verify the invocations of the logging events.
678         AnswerSyncCallback<Void> loggerCloseCallback =
679                 AnswerSyncCallback.forMultipleVoidAnswers(/* numberOfExpectedCalls= */ 2);
680         doAnswer(loggerCloseCallback)
681                 .when(mBackgroundFetchExecutionLoggerSpy)
682                 .close(anyInt(), anyInt());
683 
684         SimpleSyncCallback bgfWorkStoppedCallback = new SimpleSyncCallback();
685         mExecutorService.execute(
686                 () -> {
687                     try {
688                         mBackgroundFetchWorker.runBackgroundFetch().get();
689                     } catch (Exception exception) {
690                         sLogger.e(
691                                 exception, "Exception encountered while running background fetch");
692                     } finally {
693                         bgfWorkStoppedCallback.setCalled();
694                     }
695                 });
696 
697         // Wait til updates are complete, then try running background fetch again and
698         // verify the second run updates more custom audiences successfully
699         updateCustomAudienceCallback.assertCalled();
700         bgfWorkStoppedCallback.assertCalled();
701         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW.plusSeconds(1));
702         mBackgroundFetchWorker.runBackgroundFetch().get();
703 
704         verify(mBackgroundFetchRunnerSpy, times(2)).deleteExpiredCustomAudiences(any());
705         verify(mCustomAudienceDaoSpy, times(2)).deleteAllExpiredCustomAudienceData(any());
706         verify(mBackgroundFetchRunnerSpy, times(2)).deleteDisallowedOwnerCustomAudiences();
707         verify(mCustomAudienceDaoSpy, times(2))
708                 .deleteAllDisallowedOwnerCustomAudienceData(any(), any());
709         verify(mBackgroundFetchRunnerSpy, times(2)).deleteDisallowedBuyerCustomAudiences();
710         verify(mBackgroundFetchRunnerSpy, times(2)).deleteDisallowedPackageAppInstallEntries();
711         verify(mCustomAudienceDaoSpy, times(2))
712                 .deleteAllDisallowedBuyerCustomAudienceData(any(), any());
713         verify(mBackgroundFetchRunnerSpy, times(numEligibleCustomAudiences))
714                 .updateCustomAudience(any(), any());
715         assertThat(updateCustomAudienceCallback.getNumberActualCalls())
716                 .isEqualTo(numEligibleCustomAudiences);
717         loggerCloseCallback.assertCalled();
718     }
719 
720     @Test
721     public void testRunBackgroundFetchHandlesLoggerCloseError()
722             throws ExecutionException, InterruptedException {
723         // Mock a single custom audience eligible for update
724         DBCustomAudienceBackgroundFetchData fetchData =
725                 DBCustomAudienceBackgroundFetchDataFixture.getValidBuilderByBuyer(
726                                 CommonFixture.VALID_BUYER_1)
727                         .setEligibleUpdateTime(CommonFixture.FIXED_NOW)
728                         .build();
729         List<DBCustomAudienceBackgroundFetchData> fetchDataList = Arrays.asList(fetchData);
730         doReturn(fetchDataList)
731                 .when(mCustomAudienceDaoSpy)
732                 .getActiveEligibleCustomAudienceBackgroundFetchData(any(), anyLong());
733         doReturn(FluentFuture.from(immediateFuture(null)))
734                 .when(mBackgroundFetchRunnerSpy)
735                 .updateCustomAudience(any(), any());
736 
737         when(mClockMock.instant()).thenReturn(CommonFixture.FIXED_NOW);
738 
739         doThrow(new IllegalStateException())
740                 .when(mBackgroundFetchExecutionLoggerSpy)
741                 .close(anyInt(), anyInt());
742 
743         // Background fetch should complete without error
744         mBackgroundFetchWorker.runBackgroundFetch().get();
745     }
746 
747     private void setLatchToCountdownOnLogClose(CountDownLatch latch) {
748         doAnswer(
749                         unusedInvocation -> {
750                             latch.countDown();
751                             return null;
752                         })
753                 .when(mAdServicesLoggerImplMock)
754                 .logBackgroundFetchProcessReportedStats(any());
755     }
756 
757     private static class BackgroundFetchWorkerTestFlags implements Flags {
758         private final boolean mFledgeAppInstallFilteringEnabled;
759 
760         BackgroundFetchWorkerTestFlags(boolean fledgeAppInstallFilteringEnabled) {
761             mFledgeAppInstallFilteringEnabled = fledgeAppInstallFilteringEnabled;
762         }
763 
764         @Override
765         public int getFledgeBackgroundFetchThreadPoolSize() {
766             return 4;
767         }
768 
769         @Override
770         public boolean getFledgeAppInstallFilteringEnabled() {
771             return mFledgeAppInstallFilteringEnabled;
772         }
773 
774         @Override
775         public int getFledgeBackgroundFetchNetworkConnectTimeoutMs() {
776             return EXTENDED_FLEDGE_BACKGROUND_FETCH_NETWORK_CONNECT_TIMEOUT_MS;
777         }
778 
779         @Override
780         public int getFledgeBackgroundFetchNetworkReadTimeoutMs() {
781             return EXTENDED_FLEDGE_BACKGROUND_FETCH_NETWORK_READ_TIMEOUT_MS;
782         }
783     }
784 }
785