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