1 /*
2  * Copyright (C) 2023 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 package com.android.adservices.shared.testing;
17 
18 import static com.android.adservices.shared.testing.AndroidSdk.Level.S;
19 import static com.android.adservices.shared.testing.AndroidSdk.Level.T;
20 
21 import com.android.adservices.shared.testing.AndroidSdk.Level;
22 import com.android.adservices.shared.testing.Logger.RealLogger;
23 import com.android.adservices.shared.testing.device.DeviceConfig;
24 
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Map;
30 import java.util.Map.Entry;
31 import java.util.Objects;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
34 
35 // TODO(b/294423183): add unit tests
36 // TODO(b/294423183): use an existing class like DeviceConfigStateManager or DeviceConfigStateHelper
37 // TODO(b/294423183): migrate into the new com.android.adservices.shared.testing.device.DeviceConfig
38 // interface instead (which is being implemented from scratch with unit tests)
39 /**
40  * Helper class to set {@link android.provider.DeviceConfig} flags and properly reset then to their
41  * original values.
42  *
43  * <p><b>NOTE:</b>this class should not have any dependency on Android classes as its used both on
44  * device and host side tests.
45  *
46  * <p><b>NOTE: </b>this class is not thread safe.
47  */
48 public final class DeviceConfigHelper {
49 
50     private static final Pattern FLAG_LINE_PATTERN = Pattern.compile("^(?<name>.*)=(?<value>.*)$");
51 
52     private final String mNamespace;
53     private final Interface mInterface;
54     private final Map<String, String> mFlagsToBeReset = new HashMap<>();
55     private final Logger mLog;
56 
DeviceConfigHelper(InterfaceFactory interfaceFactory, String namespace, RealLogger logger)57     DeviceConfigHelper(InterfaceFactory interfaceFactory, String namespace, RealLogger logger) {
58         mNamespace = Objects.requireNonNull(namespace);
59         mInterface = Objects.requireNonNull(interfaceFactory).getInterface(mNamespace);
60         if (mInterface == null) {
61             throw new IllegalArgumentException(
62                     "factory " + interfaceFactory + " returned null interface");
63         }
64         mLog = mInterface.mLog;
65         mLog.v("Constructor: interface=%s, logger=%s, namespace=%s", mInterface, logger, namespace);
66     }
67 
68     /** Sets the given flag. */
set(String name, String value)69     public void set(String name, String value) {
70         savePreviousValue(name);
71         setOnly(name, value);
72     }
73 
74     /** Sets the given flag as a list (using the given separator). */
setWithSeparator(String name, String value, String separator)75     public void setWithSeparator(String name, String value, String separator) {
76         String oldValue = savePreviousValue(name);
77         String newValue = oldValue == null ? value : oldValue + separator + value;
78         setOnly(name, newValue);
79     }
80 
81     /** Restores the changed flags to their initial values. */
reset()82     public void reset() {
83         int size = mFlagsToBeReset.size();
84         if (size == 0) {
85             mLog.d("reset(): not needed");
86             return;
87         }
88         mLog.v("reset(): restoring %d flags", size);
89         try {
90             for (Entry<String, String> flag : mFlagsToBeReset.entrySet()) {
91                 String name = flag.getKey();
92                 String value = flag.getValue();
93                 if (value == null) {
94                     delete(name);
95                 } else {
96                     setOnly(name, value);
97                 }
98             }
99         } finally {
100             mFlagsToBeReset.clear();
101         }
102     }
103 
104     /** Sets the synchronization mode. */
setSyncDisabledMode(DeviceConfig.SyncDisabledModeForTest mode)105     public void setSyncDisabledMode(DeviceConfig.SyncDisabledModeForTest mode) {
106         mInterface.setSyncDisabledModeForTest(mode);
107     }
108 
109     /** Gets the synchronization mode. */
getSyncDisabledMode()110     public DeviceConfig.SyncDisabledModeForTest getSyncDisabledMode() {
111         return mInterface.getSyncDisabledModeForTest();
112     }
113 
114     /** Clears the value of all flags in the namespace. */
clearFlags()115     public void clearFlags() {
116         mInterface.clear();
117     }
118 
119     @Override
toString()120     public String toString() {
121         return "DeviceConfigHelper[mNamespace="
122                 + mNamespace
123                 + ", mInterface="
124                 + mInterface
125                 + ", mFlagsToBeReset="
126                 + mFlagsToBeReset
127                 + ", mLog="
128                 + mLog
129                 + "]";
130     }
131 
132     // TODO(b/294423183): temporarily exposed as it's used by legacy helper methods on
133     // AdServicesFlagsSetterRule
get(String name)134     String get(String name) {
135         return mInterface.get(name, /* defaultValue= */ null);
136     }
137 
getAll()138     public List<NameValuePair> getAll() {
139         return mInterface.getAll();
140     }
141 
savePreviousValue(String name)142     private String savePreviousValue(String name) {
143         String oldValue = get(name);
144         if (mFlagsToBeReset.containsKey(name)) {
145             mLog.v("Value of %s (%s) already saved for reset()", name, mFlagsToBeReset.get(name));
146             return oldValue;
147         }
148         mLog.v("Saving %s=%s for reset", name, oldValue);
149         mFlagsToBeReset.put(name, oldValue);
150         return oldValue;
151     }
152 
setOnly(String name, String value)153     private void setOnly(String name, String value) {
154         mInterface.syncSet(name, value);
155     }
156 
delete(String name)157     private void delete(String name) {
158         mInterface.syncDelete(name);
159     }
160 
161     // TODO(b/294423183); move to a separate file (and rename it?)?
162     // TODO(b/294423183): add unit tests
163     /**
164      * Low-level interface for {@link android.provider.DeviceConfig}.
165      *
166      * <p>By default it uses {@code cmd device_config} to implement all methods, but subclasses
167      * could override them (for example, device-side implementation could use {@code DeviceConfig}
168      * instead.
169      */
170     public abstract static class Interface extends AbstractDeviceGateway {
171 
172         private static final int CHANGE_CHECK_TIMEOUT_MS = 5_000;
173         private static final int CHANGE_CHECK_SLEEP_TIME_MS = 500;
174 
175         protected final Logger mLog;
176         protected final String mNamespace;
177 
Interface(String namespace, RealLogger logger)178         protected Interface(String namespace, RealLogger logger) {
179             mNamespace = Objects.requireNonNull(namespace);
180             mLog = new Logger(Objects.requireNonNull(logger), DeviceConfigHelper.class);
181         }
182 
183         /** Sets the synchronization mode. */
setSyncDisabledModeForTest(DeviceConfig.SyncDisabledModeForTest mode)184         public void setSyncDisabledModeForTest(DeviceConfig.SyncDisabledModeForTest mode) {
185             String value = mode.name().toLowerCase(Locale.ENGLISH);
186             mLog.v("setSyncDisabledModeForTest(%s)", value);
187 
188             // TODO(b/294423183): figure out a solution for R when needed
189             if (getDeviceApiLevel().isAtLeast(S)) {
190                 // Command supported only on S+.
191                 runShellCommand("device_config set_sync_disabled_for_tests %s", value);
192                 return;
193             }
194         }
195 
196         /** Gets the synchronization mode. */
getSyncDisabledModeForTest()197         public DeviceConfig.SyncDisabledModeForTest getSyncDisabledModeForTest() {
198             mLog.d("getSyncDisabledModeForTest() invoked");
199 
200             if (getDeviceApiLevel().isAtLeast(T)) {
201                 String value = runShellCommand("device_config get_sync_disabled_for_tests").trim();
202                 mLog.v("get_sync_disabled_for_tests=%s using run shell command", value);
203                 return DeviceConfig.SyncDisabledModeForTest.valueOf(
204                         value.toUpperCase(Locale.ENGLISH));
205             } else if (getDeviceApiLevel().isAtLeast(S)) {
206                 String value = runShellCommand("device_config is_sync_disabled_for_tests").trim();
207                 mLog.v("is_sync_disabled_for_tests=%s using run shell command", value);
208                 // If the value is "true", it's not possible to figure out if the mode is
209                 // "persistent" or "until_reboot". Assume "persistent".
210                 return Boolean.parseBoolean(value)
211                         ? DeviceConfig.SyncDisabledModeForTest.PERSISTENT
212                         : DeviceConfig.SyncDisabledModeForTest.NONE;
213             }
214 
215             // TODO(b/294423183): figure out a solution for R when needed
216             return DeviceConfig.SyncDisabledModeForTest.NONE;
217         }
218 
219         /** Gets the value of a property. */
get(String name, String defaultValue)220         public String get(String name, String defaultValue) {
221             mLog.d("get(%s, %s): using runShellCommand", name, defaultValue);
222             String value = runShellCommand("device_config get %s %s", mNamespace, name).trim();
223             mLog.v(
224                     "get(%s, %s): raw value is '%s' (is null: %b)",
225                     name, defaultValue, value, value == null);
226             if (!value.equals("null")) {
227                 return value;
228             }
229             // "null" could mean the value doesn't exist, or it's the string "null", so we need to
230             // check them
231             String allFlags = runShellCommand("device_config list %s", mNamespace);
232             for (String line : allFlags.split("\n")) {
233                 if (line.equals(name + "=null")) {
234                     mLog.v("Value of flag %s is indeed \"%s\"", name, value);
235                     return value;
236                 }
237             }
238             return defaultValue;
239         }
240 
241         /**
242          * Sets the value of a property and blocks until the value is changed.
243          *
244          * @throws IllegalStateException if the value could not be updated.
245          */
syncSet(String name, @Nullable String value)246         public void syncSet(String name, @Nullable String value) {
247             if (value == null) {
248                 syncDelete(name);
249                 return;
250             }
251             // TODO(b/300136201): check current value first and return right away if it matches
252 
253             // TODO(b/294423183): optimize code below (once it's unit tested), there's too much
254             // duplication.
255             String currentValue = get(name, /* defaultValue= */ null);
256             boolean changed = !value.equals(currentValue);
257             if (!changed) {
258                 mLog.v("syncSet(%s, %s): already %s, ignoring", name, value, value);
259                 return;
260                 // TODO(b/294423183): change it to return a boolean instead so the value doesn't
261                 // need to be restored. But there would be many corner cases (for example, what if
262                 // asyncSet() fails? What if the value is the same because it was set by the rule
263                 // before), so it's better to wait until we have unit tests for it.
264             }
265             long deadline = System.currentTimeMillis() + CHANGE_CHECK_TIMEOUT_MS;
266             do {
267                 if (!asyncSet(name, value)) {
268                     mLog.w("syncSet(%s, %s): call to asyncSet() returned false", name, value);
269                     throw new IllegalStateException(
270                             "Low-level call to set " + name + "=" + value + " returned false");
271                 }
272                 currentValue = get(name, /* defaultValue= */ null);
273                 changed = value.equals(currentValue);
274                 if (changed) {
275                     mLog.v("change propagated, returning");
276                     return;
277                 }
278                 if (System.currentTimeMillis() > deadline) {
279                     mLog.e(
280                             "syncSet(%s, %s): value didn't change after %d ms",
281                             name, value, CHANGE_CHECK_TIMEOUT_MS);
282                     throw new IllegalStateException(
283                             "Low-level call to set "
284                                     + name
285                                     + "="
286                                     + value
287                                     + " succeeded, but value change was not propagated after "
288                                     + CHANGE_CHECK_TIMEOUT_MS
289                                     + "ms");
290                 }
291                 mLog.d(
292                         "syncSet(%s, %s): current value is still %s, sleeping %d ms",
293                         name, value, currentValue, CHANGE_CHECK_SLEEP_TIME_MS);
294                 sleepBeforeCheckingAgain(name);
295             } while (true);
296         }
297 
sleepBeforeCheckingAgain(String name)298         private void sleepBeforeCheckingAgain(String name) {
299             mLog.v(
300                     "Sleeping for %dms before checking value of %s again",
301                     CHANGE_CHECK_SLEEP_TIME_MS, name);
302             try {
303                 Thread.sleep(CHANGE_CHECK_SLEEP_TIME_MS);
304             } catch (InterruptedException e) {
305                 Thread.currentThread().interrupt();
306             }
307         }
308 
309         /**
310          * Sets the value of a property, without checking if it changed.
311          *
312          * @return whether the low-level {@code DeviceConfig} call succeeded.
313          */
asyncSet(String name, @Nullable String value)314         public boolean asyncSet(String name, @Nullable String value) {
315             mLog.d("asyncSet(%s, %s): using runShellCommand", name, value);
316             runShellCommand("device_config put %s %s %s", mNamespace, name, value);
317             // TODO(b/294423183): parse result
318             return true;
319         }
320 
321         /**
322          * Deletes a property and blocks until the value is changed.
323          *
324          * @throws IllegalStateException if the value could not be updated.
325          */
syncDelete(String name)326         public void syncDelete(String name) {
327             // TODO(b/294423183): add wait logic here too
328             asyncDelete(name);
329         }
330 
331         /**
332          * Deletes a property, without checking if it changed.
333          *
334          * @return whether the low-level {@code DeviceConfig} call succeeded.
335          */
asyncDelete(String name)336         public boolean asyncDelete(String name) {
337             mLog.d("asyncDelete(%s): using runShellCommand", name);
338             runShellCommand("device_config delete %s %s", mNamespace, name);
339             // TODO(b/294423183): parse result
340             return true;
341         }
342 
343         /** Clears all flags. */
clear()344         public void clear() {
345             // TODO (b/373480101): Remove after aligning on approach for clearing flags in tests.
346             if (true) {
347                 throw new UnsupportedOperationException(
348                         "Flags should not be cleared to avoid interference with flag ramp and "
349                                 + "AOAO testing!");
350             }
351 
352             runShellCommand("device_config reset untrusted_clear %s", mNamespace);
353 
354             // TODO(b/305877958): command above will "delete all settings set by untrusted packages,
355             // which is packages that aren't a part of the system", so it might not delete them
356             // all. In fact, after this method was first called, it cause test breakages because
357             // disable_sdk_sandbox was still set. So, we should also explicitly delete all flags
358             // that remain, but for now clearing those from untrusted packages is enough
359             List<NameValuePair> currentFlags = getAll();
360             if (!currentFlags.isEmpty()) {
361                 mLog.w(
362                         "clear(): not all flags were deleted, which is a known limitation."
363                                 + " Following flags remain:\n\n"
364                                 + "%s",
365                         currentFlags);
366             }
367 
368             // TODO(b/300136201): should wait until they're all cleared
369         }
370 
371         /** Get all properties. */
getAll()372         public List<NameValuePair> getAll() {
373             String dump = runShellCommand("device_config list %s", mNamespace).trim();
374             String[] lines = dump.split("\n");
375             List<NameValuePair> allFlags = new ArrayList<>(lines.length);
376             for (int i = 0; i < lines.length; i++) {
377                 String line = lines[i];
378                 Matcher matcher = FLAG_LINE_PATTERN.matcher(line);
379                 if (matcher.matches()) {
380                     String name = matcher.group("name");
381                     String value = matcher.group("value");
382                     allFlags.add(new NameValuePair(name, value));
383                 }
384             }
385             return allFlags;
386         }
387 
388         @Override
toString()389         public String toString() {
390             return getClass().getSimpleName();
391         }
392 
393         /** Gets the device API level. */
getDeviceApiLevel()394         public abstract Level getDeviceApiLevel();
395     }
396 
397     /** Factory for {@link Interface} objects. */
398     public interface InterfaceFactory {
399 
400         /**
401          * Gets an {@link Interface} for the given {@link android.provider.DeviceConfig} namespace.
402          */
getInterface(String namespace)403         Interface getInterface(String namespace);
404     }
405 }
406