1 /* 2 * Copyright (C) 2017 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 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import android.content.Context; 23 import android.provider.Settings; 24 import android.text.TextUtils; 25 import android.util.Log; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 30 /** 31 * Provides utilities to interact with the device's {@link Settings}. 32 */ 33 public final class SettingsUtils { 34 35 private static final String TAG = SettingsUtils.class.getSimpleName(); 36 37 public static final String NAMESPACE_SECURE = "secure"; 38 public static final String NAMESPACE_GLOBAL = "global"; 39 40 // TODO(b/123885378): we cannot pass an empty value when using 'cmd settings', so we need 41 // to remove the property instead. Once we use the Settings API directly, we can remove this 42 // constant and all if() statements that ues it 43 static final boolean TMP_HACK_REMOVE_EMPTY_PROPERTIES = true; 44 45 /** 46 * Uses a Shell command to set the given preference. 47 */ set(@onNull String namespace, @NonNull String key, @Nullable String value)48 public static void set(@NonNull String namespace, @NonNull String key, @Nullable String value) { 49 if (value == null) { 50 delete(namespace, key); 51 return; 52 } 53 if (TMP_HACK_REMOVE_EMPTY_PROPERTIES && TextUtils.isEmpty(value)) { 54 Log.w(TAG, "Value of " + namespace + ":" + key + " is empty; deleting it instead"); 55 delete(namespace, key); 56 return; 57 } 58 runShellCommand("settings put %s %s %s default", namespace, key, value); 59 } 60 61 /** 62 * Sets a preference in the {@link #NAMESPACE_SECURE} namespace. 63 */ set(@onNull String key, @Nullable String value)64 public static void set(@NonNull String key, @Nullable String value) { 65 set(NAMESPACE_SECURE, key, value); 66 } 67 68 /** 69 * Uses a Shell command to set the given preference, and verifies it was correctly set. 70 */ syncSet(@onNull Context context, @NonNull String namespace, @NonNull String key, @Nullable String value)71 public static void syncSet(@NonNull Context context, @NonNull String namespace, 72 @NonNull String key, @Nullable String value) { 73 if (value == null) { 74 syncDelete(context, namespace, key); 75 return; 76 } 77 78 final String currentValue = get(namespace, key); 79 if (value.equals(currentValue)) { 80 // Already set, ignore 81 return; 82 } 83 84 final OneTimeSettingsListener observer = 85 new OneTimeSettingsListener(context, namespace, key); 86 set(namespace, key, value); 87 observer.assertCalled(); 88 89 final String newValue = get(namespace, key); 90 if (TMP_HACK_REMOVE_EMPTY_PROPERTIES && TextUtils.isEmpty(value)) { 91 assertWithMessage("invalid value for '%s' settings", key).that(newValue) 92 .isNull(); 93 } else { 94 assertWithMessage("invalid value for '%s' settings", key).that(newValue) 95 .isEqualTo(value); 96 } 97 } 98 99 /** 100 * Sets a preference in the {@link #NAMESPACE_SECURE} namespace, using a Settings listener to 101 * block until it's set. 102 */ syncSet(@onNull Context context, @NonNull String key, @Nullable String value)103 public static void syncSet(@NonNull Context context, @NonNull String key, 104 @Nullable String value) { 105 syncSet(context, NAMESPACE_SECURE, key, value); 106 } 107 108 /** 109 * Uses a Shell command to delete the given preference. 110 */ delete(@onNull String namespace, @NonNull String key)111 public static void delete(@NonNull String namespace, @NonNull String key) { 112 runShellCommand("settings delete %s %s", namespace, key); 113 } 114 115 /** 116 * Deletes a preference in the {@link #NAMESPACE_SECURE} namespace. 117 */ delete(@onNull String key)118 public static void delete(@NonNull String key) { 119 delete(NAMESPACE_SECURE, key); 120 } 121 122 /** 123 * Uses a Shell command to delete the given preference, and verifies it was correctly deleted. 124 */ syncDelete(@onNull Context context, @NonNull String namespace, @NonNull String key)125 public static void syncDelete(@NonNull Context context, @NonNull String namespace, 126 @NonNull String key) { 127 128 final String currentValue = get(namespace, key); 129 if (currentValue == null) { 130 // Already set, ignore 131 return; 132 } 133 134 final OneTimeSettingsListener observer = new OneTimeSettingsListener(context, namespace, 135 key); 136 delete(namespace, key); 137 observer.assertCalled(); 138 139 final String newValue = get(namespace, key); 140 assertWithMessage("invalid value for '%s' settings", key).that(newValue).isNull(); 141 } 142 143 /** 144 * Deletes a preference in the {@link #NAMESPACE_SECURE} namespace, using a Settings listener to 145 * block until it's deleted. 146 */ syncDelete(@onNull Context context, @NonNull String key)147 public static void syncDelete(@NonNull Context context, @NonNull String key) { 148 syncDelete(context, NAMESPACE_SECURE, key); 149 } 150 151 /** 152 * Gets the value of a given preference using Shell command. 153 */ 154 @Nullable get(@onNull String namespace, @NonNull String key)155 public static String get(@NonNull String namespace, @NonNull String key) { 156 final String value = runShellCommand("settings get %s %s", namespace, key); 157 if (value == null || value.equals("null")) { 158 return null; 159 } else { 160 return value; 161 } 162 } 163 164 /** 165 * Gets the value of a preference in the {@link #NAMESPACE_SECURE} namespace. 166 */ 167 @NonNull get(@onNull String key)168 public static String get(@NonNull String key) { 169 return get(NAMESPACE_SECURE, key); 170 } 171 SettingsUtils()172 private SettingsUtils() { 173 throw new UnsupportedOperationException("contain static methods only"); 174 } 175 176 /** 177 * @deprecated - use {@link #set(String, String, String)} with {@link #NAMESPACE_GLOBAL} 178 */ 179 @Deprecated putGlobalSetting(String key, String value)180 public static void putGlobalSetting(String key, String value) { 181 set(SettingsUtils.NAMESPACE_GLOBAL, key, value); 182 183 } 184 185 /** 186 * @deprecated - use {@link #set(String, String, String)} with {@link #NAMESPACE_GLOBAL} 187 */ 188 @Deprecated putSecureSetting(String key, String value)189 public static void putSecureSetting(String key, String value) { 190 set(SettingsUtils.NAMESPACE_SECURE, key, value); 191 192 } 193 194 /** 195 * Get a global setting for the current (foreground) user. Trims ending new line. 196 */ getSecureSetting(String key)197 public static String getSecureSetting(String key) { 198 return SystemUtil.runShellCommand("settings --user current get secure " + key).trim(); 199 } 200 201 /** 202 * Get a global setting for the given user. Trims ending new line. 203 */ getSecureSettingAsUser(int userId, String key)204 public static String getSecureSettingAsUser(int userId, String key) { 205 return SystemUtil.runShellCommand( 206 String.format("settings --user %d get secure %s", userId, key)).trim(); 207 } 208 209 public static class SettingResetter implements AutoCloseable { 210 private final String mNamespace; 211 private final String mKey; 212 private final String mOldValue; 213 SettingResetter(String namespace, String key, String value)214 public SettingResetter(String namespace, String key, String value) { 215 mNamespace = namespace; 216 mKey = key; 217 mOldValue = get(namespace, key); 218 set(namespace, key, value); 219 } 220 221 @Override close()222 public void close() { 223 if (mOldValue != null) { 224 set(mNamespace, mKey, mOldValue); 225 } else { 226 delete(mNamespace, mKey); 227 } 228 } 229 } 230 } 231