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