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