• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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 android.adservices.test.scenario.adservices.topics;
18 
19 import android.adservices.clients.topics.AdvertisingTopicsClient;
20 import android.content.Context;
21 import android.content.SharedPreferences;
22 import android.util.Log;
23 
24 import com.google.common.base.Preconditions;
25 import com.google.common.collect.ImmutableList;
26 
27 import java.io.BufferedReader;
28 import java.io.IOException;
29 import java.io.InputStreamReader;
30 import java.time.Instant;
31 import java.time.ZoneId;
32 import java.time.format.DateTimeFormatter;
33 import java.util.concurrent.ExecutionException;
34 import java.util.concurrent.Executors;
35 
36 /**
37  * A helper that helps to sleep until reached start of an Epoch based on the fetched origin and test
38  * epoch job period.
39  */
40 final class EpochSleeper {
41     private static final String TAG = "EpochSleeper";
42     private static final String ORIGIN_TIMESTAMP_MS = "origin_timestamp_ms";
43     private static final String SHARED_PREFERENCE_NAME = "epoch_sync";
44     private static final String SETUP_SDK_NAME = "setup_origin";
45     private static final String RETRIEVED_EPOCH_ORIGIN_LOG = "retrieved Epoch origin";
46     private static final DateTimeFormatter DEFAULT_DATETIME_FORMATTER =
47             DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault());
48 
49     private static EpochSleeper sSleeper;
50 
51     private final Instant mFetchOriginStartTime;
52     private final Context mContext;
53     private final SharedPreferences mSharedPreferences;
54     private final long mTestEpochPeriodMs;
55 
EpochSleeper(Context context, long testEpochPeriodMs)56     private EpochSleeper(Context context, long testEpochPeriodMs) {
57         Preconditions.checkArgument(
58                 testEpochPeriodMs > 0L, "epoch period should be a positive long.");
59         this.mContext = context;
60 
61         // A Crystal Ball test runs tests in iterations. Using a SharedPreference can effectively
62         // share common data across the processes. The stored data will be cleaned up as test
63         // application cycle ends, so it won't affect other tests.
64         this.mSharedPreferences =
65                 context.getSharedPreferences(SHARED_PREFERENCE_NAME, Context.MODE_MULTI_PROCESS);
66         this.mFetchOriginStartTime = Instant.now();
67         this.mTestEpochPeriodMs = testEpochPeriodMs;
68     }
69 
getInstance(Context context, long testEpochPeriodMs)70     static EpochSleeper getInstance(Context context, long testEpochPeriodMs) {
71         if (sSleeper == null) {
72             sSleeper = new EpochSleeper(context, testEpochPeriodMs);
73         }
74         return sSleeper;
75     }
76 
77     // Makes a call to getTopics API to trigger a log that tells the stored origin.
triggerOriginLog()78     void triggerOriginLog() {
79         if (getOrigin() == -1) {
80             try {
81                 AdvertisingTopicsClient client =
82                         new AdvertisingTopicsClient.Builder()
83                                 .setContext(mContext)
84                                 .setSdkName(SETUP_SDK_NAME)
85                                 .setExecutor(Executors.newCachedThreadPool())
86                                 .build();
87                 client.getTopics().get();
88             } catch (InterruptedException | ExecutionException e) {
89                 Log.i(TAG, "Failed to setup a Topics origin, keep on the test.");
90             }
91         }
92     }
93 
94     // Fetches a valid origin and perform sleep till start of an Epoch.
sleepUntilNextEpoch()95     void sleepUntilNextEpoch() throws Exception {
96         long origin = fetchOrigin();
97         if (origin != -1) {
98 
99             // Uses origin to make sure every test invoke starts at the beginning of an Epoch.
100             long calculatedSleepTimeMs =
101                     mTestEpochPeriodMs - (System.currentTimeMillis() - origin) % mTestEpochPeriodMs;
102             Thread.sleep(calculatedSleepTimeMs);
103         }
104     }
105 
getOrigin()106     private long getOrigin() {
107         return mSharedPreferences.getLong(ORIGIN_TIMESTAMP_MS, -1L);
108     }
109 
110     // Stores and shares the origin with other iterations.
putOrigin(long origin)111     private void putOrigin(long origin) {
112         mSharedPreferences.edit().putLong(ORIGIN_TIMESTAMP_MS, origin).commit();
113     }
114 
115     // Fetches the origin from the SharedPreference firstly. If it does not exist, fetches again
116     // from the log and updates the SharedPreference properly.
fetchOrigin()117     private long fetchOrigin() {
118         long origin = getOrigin();
119         try {
120 
121             // Fetches and stores origin from log if not set.
122             if (origin == -1) {
123                 origin = fetchOriginFromLog();
124                 putOrigin(origin);
125             }
126         } catch (IOException e) {
127             Log.i(TAG, "Failed to fetch a Topics origin from logs, keep on the test.");
128         }
129         return origin;
130     }
131 
fetchOriginFromLog()132     private long fetchOriginFromLog() throws IOException {
133         ProcessBuilder pb =
134                 new ProcessBuilder(
135                         ImmutableList.of(
136                                 "logcat",
137                                 "-s",
138                                 "adservices.topics",
139                                 "-t",
140                                 DEFAULT_DATETIME_FORMATTER.format(mFetchOriginStartTime),
141                                 "|",
142                                 "grep",
143                                 RETRIEVED_EPOCH_ORIGIN_LOG));
144         BufferedReader bufferedReader =
145                 new BufferedReader(new InputStreamReader(pb.start().getInputStream()));
146         String[] arr =
147                 bufferedReader
148                         .lines()
149                         .filter(l -> l.contains(RETRIEVED_EPOCH_ORIGIN_LOG))
150                         .findFirst()
151                         .orElse(String.format("%s %d", RETRIEVED_EPOCH_ORIGIN_LOG, -1))
152                         .split(" ");
153         return Long.valueOf(arr[arr.length - 1].trim());
154     }
155 }
156