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