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 17 package com.android.compatibility.common.util; 18 19 import static com.android.compatibility.common.util.ShellUtils.runShellCommand; 20 21 import static com.google.common.truth.Truth.assertWithMessage; 22 23 import android.content.Context; 24 import android.text.TextUtils; 25 import android.util.Log; 26 27 import androidx.annotation.Nullable; 28 import androidx.test.InstrumentationRegistry; 29 30 /** 31 * Helper to set user preferences. 32 */ 33 public final class UserSettings { 34 35 private static final String TAG = UserSettings.class.getSimpleName(); 36 37 // Constants below are needed for switch statements 38 public static final String NAMESPACE_SECURE = "secure"; 39 public static final String NAMESPACE_GLOBAL = "global"; 40 public static final String NAMESPACE_SYSTEM = "system"; 41 42 private final Context mContext; 43 private final Namespace mNamespace; 44 private final int mUserId; 45 46 /** 47 * Default constructor, it uses: 48 * 49 * <ul> 50 * <li>target context of the instrumented app 51 * <li>secure namespace 52 * <li>user running the test 53 * </ul> 54 */ UserSettings()55 public UserSettings() { 56 this(InstrumentationRegistry.getTargetContext()); 57 } 58 59 /** 60 * Constructor for the {@link android.app.ActivityManager#getCurrentUser() current foreground 61 * user} and the given context and namespace. 62 */ UserSettings(Context context, Namespace namespace)63 public UserSettings(Context context, Namespace namespace) { 64 this(context, namespace, context.getUser().getIdentifier()); 65 } 66 67 /** 68 * Constructor that uses : 69 * 70 * <ul> 71 * <li>the given context 72 * <li>secure namespace 73 * <li>user running the test 74 * </ul> 75 */ UserSettings(Context context)76 public UserSettings(Context context) { 77 this(context, Namespace.SECURE); 78 } 79 80 /** 81 * Default constructor, it uses: 82 * 83 * <ul> 84 * <li>target context of the instrumented app 85 * <li>secure namespace 86 * <li>the given user 87 * </ul> 88 */ UserSettings(int userId)89 public UserSettings(int userId) { 90 this(InstrumentationRegistry.getTargetContext(), Namespace.SECURE, userId); 91 } 92 93 /** 94 * Constructor that uses: 95 * 96 * <ul> 97 * <li>target context of the instrumented app 98 * <li>the given namespace 99 * <li>user running the test 100 * </ul> 101 */ UserSettings(Namespace namespace)102 public UserSettings(Namespace namespace) { 103 this(InstrumentationRegistry.getTargetContext(), namespace); 104 } 105 106 /** 107 * Full constructor. 108 */ UserSettings(Context context, Namespace namespace, int userId)109 public UserSettings(Context context, Namespace namespace, int userId) { 110 mContext = context; 111 mNamespace = namespace; 112 mUserId = userId; 113 Log.v(TAG, toString()); 114 } 115 116 /** 117 * Sets the value of the given preference, using a Settings listener to block until it's set. 118 */ syncSet(String key, @Nullable String value)119 public void syncSet(String key, @Nullable String value) { 120 logd("syncSet(%s, %s)", key, value); 121 if (value == null) { 122 syncDelete(key); 123 return; 124 } 125 126 String currentValue = get(key); 127 if (value.equals(currentValue)) { 128 // Already set, ignore 129 return; 130 } 131 132 OneTimeSettingsListener observer = new OneTimeSettingsListener(mContext, mNamespace.get(), 133 key); 134 set(key, value); 135 observer.assertCalled(); 136 137 String newValue = get(key); 138 if (TMP_HACK_REMOVE_EMPTY_PROPERTIES && TextUtils.isEmpty(value)) { 139 assertWithMessage("value of '%s'", key).that(newValue).isNull(); 140 } else { 141 assertWithMessage("value of '%s'", key).that(newValue).isEqualTo(value); 142 } 143 } 144 145 /** 146 * Sets the value of the given preference. 147 */ set(String key, @Nullable String value)148 public void set(String key, @Nullable String value) { 149 if (value == null) { 150 delete(key); 151 return; 152 } 153 if (TMP_HACK_REMOVE_EMPTY_PROPERTIES && TextUtils.isEmpty(value)) { 154 Log.w(TAG, "Value of " + mNamespace.get() + ":" + key + " is empty; deleting it " 155 + "instead"); 156 delete(key); 157 return; 158 } 159 runShellCommand("settings put --user %d %s %s %s%s", mUserId, mNamespace.get(), key, 160 value, mNamespace.mDefaultSuffix); 161 } 162 163 /** 164 * Deletes the given preference using a Settings listener to block until it's deleted. 165 */ syncDelete(String key)166 public void syncDelete(String key) { 167 String currentValue = get(key); 168 logd("syncDelete(%s), currentValue=%s", key, currentValue); 169 if (currentValue == null) { 170 // Already set, ignore 171 return; 172 } 173 174 OneTimeSettingsListener observer = new OneTimeSettingsListener(mContext, mNamespace.get(), 175 key); 176 delete(key); 177 observer.assertCalled(); 178 179 String newValue = get(key); 180 assertWithMessage("value of '%s' after it was removed", key).that(newValue).isNull(); 181 } 182 183 /** 184 * Deletes the given preference. 185 */ delete(String key)186 public void delete(String key) { 187 logd("delete(%s)", key); 188 runShellCommand("settings delete --user %d %s %s", mUserId, mNamespace.get(), key); 189 } 190 191 /** 192 * Gets the value of a preference. 193 */ get(String key)194 public String get(String key) { 195 String value = runShellCommand("settings get --user %d %s %s", mUserId, mNamespace.get(), 196 key); 197 String returnedValue = value == null || value.equals("null") ? null : value; 198 logd("get(%s): settings returned '%s', returning '%s'", key, value, returnedValue); 199 return returnedValue; 200 } 201 202 @Override toString()203 public String toString() { 204 return "UserSettings[" + toShortString() + "]"; 205 } 206 logd(String template, Object...args)207 private void logd(String template, Object...args) { 208 Log.d(TAG, "[" + toShortString() + "]: " + String.format(template, args)); 209 } 210 toShortString()211 private String toShortString() { 212 return "namespace=" + mNamespace + ", user=" + mUserId; 213 } 214 215 /** 216 * Abstracts the Settings namespace. 217 */ 218 public enum Namespace { 219 SECURE(NAMESPACE_SECURE, " default"), 220 GLOBAL(NAMESPACE_GLOBAL, " default"), 221 SYSTEM(NAMESPACE_SYSTEM, ""); 222 223 // TODO(b/123885378): remove if it's not used anymore (after using Settings APIs) 224 private final String mName; 225 private final String mDefaultSuffix; 226 Namespace(String name, String defaultSuffix)227 Namespace(String name, String defaultSuffix) { 228 mName = name; 229 mDefaultSuffix = defaultSuffix; 230 } 231 232 /** 233 * Gets the enum for the given namespace. 234 */ of(String namespace)235 public static Namespace of(String namespace) { 236 switch (namespace.toLowerCase()) { 237 case NAMESPACE_SECURE: 238 return SECURE; 239 case NAMESPACE_GLOBAL: 240 return GLOBAL; 241 case NAMESPACE_SYSTEM: 242 return SYSTEM; 243 default: 244 throw new IllegalArgumentException("Unknown namespace: " + namespace); 245 } 246 } 247 248 /** 249 * Gets the namespace as used by the {@code settings} shell command. 250 */ get()251 public String get() { 252 return mName; 253 } 254 } 255 256 // TODO(b/123885378): we cannot pass an empty value when using 'cmd settings', so we need 257 // to remove the property instead. Once we use the Settings API directly, we can remove this 258 // constant and all if() statements that uses it 259 static final boolean TMP_HACK_REMOVE_EMPTY_PROPERTIES = true; 260 } 261