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