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 android.server.wm.settings;
18 
19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
20 
21 import android.content.ContentResolver;
22 import android.net.Uri;
23 import android.provider.Settings.SettingNotFoundException;
24 import android.server.wm.NestedShellPermission;
25 import android.util.Log;
26 
27 import androidx.annotation.NonNull;
28 
29 import com.android.compatibility.common.util.SystemUtil;
30 
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 
35 import org.junit.rules.TestRule;
36 
37 /**
38  * Helper class to save, set, and restore global system-level preferences.
39  * <p>
40  * To use this class, testing APK must be self-instrumented and have
41  * {@link android.Manifest.permission#WRITE_SECURE_SETTINGS}.
42  * <p>
43  * A test that changes system-level preferences can be written easily and reliably.
44  * <pre>
45  * static class PrefSession extends SettingsSession<String> {
46  *     PrefSession() {
47  *         super(android.provider.Settings.Secure.getUriFor(
48  *                       android.provider.Settings.Secure.PREFERENCE_KEY),
49  *               android.provider.Settings.Secure::getString,
50  *               android.provider.Settings.Secure::putString);
51  *     }
52  * }
53  *
54  * @Test
55  * public void doTest() throws Exception {
56  *     try (final PrefSession prefSession = new PrefSession()) {
57  *         prefSession.set("value 1");
58  *         doTest1();
59  *         prefSession.set("value 2");
60  *         doTest2();
61  *     }
62  * }
63  * </pre>
64  */
65 public class SettingsSession<T> implements AutoCloseable {
66     private static final String TAG = SettingsSession.class.getSimpleName();
67     private static final boolean DEBUG = false;
68 
69     @FunctionalInterface
70     public interface SettingsGetter<T> {
get(ContentResolver cr, String key)71         T get(ContentResolver cr, String key) throws SettingNotFoundException;
72     }
73 
74     @FunctionalInterface
75     public interface SettingsSetter<T> {
set(ContentResolver cr, String key, T value)76         void set(ContentResolver cr, String key, T value);
77     }
78 
79     /**
80      * To debug to detect nested sessions for the same key. Enabled when {@link #DEBUG} is true.
81      * Note that nested sessions can be merged into one session.
82      */
83     private static final SessionCounters sSessionCounters = new SessionCounters();
84 
85     protected final Uri mUri;
86     protected final boolean mHasInitialValue;
87     protected final T mInitialValue;
88     private final SettingsGetter<T> mGetter;
89     private final SettingsSetter<T> mSetter;
90 
SettingsSession(final Uri uri, final SettingsGetter<T> getter, final SettingsSetter<T> setter)91     public SettingsSession(final Uri uri, final SettingsGetter<T> getter,
92             final SettingsSetter<T> setter) {
93         mUri = uri;
94         mGetter = getter;
95         mSetter = setter;
96         T initialValue;
97         boolean hasInitialValue;
98         try {
99             initialValue = get(uri, getter);
100             hasInitialValue = true;
101         } catch (SettingNotFoundException e) {
102             initialValue = null;
103             hasInitialValue = false;
104         }
105         mInitialValue = initialValue;
106         mHasInitialValue = hasInitialValue;
107         if (DEBUG) {
108             Log.i(TAG, "start: uri=" + uri
109                     + (mHasInitialValue ? " value=" + mInitialValue : " undefined"));
110             sSessionCounters.open(uri);
111         }
112     }
113 
overrideForTest(final Uri uri, final SettingsGetter<T> getter, final SettingsSetter<T> setter, final @NonNull T value)114     public static <T> TestRule overrideForTest(final Uri uri, final SettingsGetter<T> getter,
115             final SettingsSetter<T> setter, final @NonNull T value) {
116             return new AutoCloseableRule(() -> {
117                 SettingsSession<T> session = new SettingsSession<>(uri, getter, setter);
118                 session.set(value);
119                 return session;
120             });
121     }
122 
set(final @NonNull T value)123     public void set(final @NonNull T value) {
124         put(mUri, mSetter, value);
125         if (DEBUG) {
126             Log.i(TAG, "  set: uri=" + mUri + " value=" + value);
127         }
128     }
129 
get()130     public T get() {
131         try {
132             return get(mUri, mGetter);
133         } catch (SettingNotFoundException e) {
134             return null;
135         }
136     }
137 
138     @Override
close()139     public void close() {
140         if (mHasInitialValue) {
141             put(mUri, mSetter, mInitialValue);
142             if (DEBUG) {
143                 Log.i(TAG, "close: uri=" + mUri + " value=" + mInitialValue);
144             }
145         } else {
146             delete(mUri);
147             if (DEBUG) {
148                 Log.i(TAG, "close: uri=" + mUri + " deleted");
149             }
150         }
151         if (DEBUG) {
152             sSessionCounters.close(mUri);
153         }
154     }
155 
put(final Uri uri, final SettingsSetter<T> setter, T value)156     private static <T> void put(final Uri uri, final SettingsSetter<T> setter, T value) {
157         NestedShellPermission.run(() -> {
158             setter.set(getContentResolver(), uri.getLastPathSegment(), value);
159         });
160     }
161 
get(final Uri uri, final SettingsGetter<T> getter)162     private static <T> T get(final Uri uri, final SettingsGetter<T> getter)
163             throws SettingNotFoundException {
164         return getter.get(getContentResolver(), uri.getLastPathSegment());
165     }
166 
delete(final Uri uri)167     public static void delete(final Uri uri) {
168         final List<String> segments = uri.getPathSegments();
169         if (segments.size() != 2) {
170             Log.w(TAG, "Unsupported uri for deletion: " + uri, new Throwable());
171             return;
172         }
173         final String namespace = segments.get(0);
174         final String key = segments.get(1);
175         // SystemUtil.runWithShellPermissionIdentity (only applies to the permission checking in
176         // package manager and appops) does not change calling uid which is enforced in
177         // SettingsProvider for deletion, so it requires shell command to pass the restriction.
178         SystemUtil.runShellCommand("settings delete " + namespace + " " + key);
179     }
180 
getContentResolver()181     private static ContentResolver getContentResolver() {
182         return getInstrumentation().getTargetContext().getContentResolver();
183     }
184 
185     private static class SessionCounters {
186         private final Map<Uri, Integer> mOpenSessions = new HashMap<>();
187 
open(final Uri uri)188         void open(final Uri uri) {
189             final Integer count = mOpenSessions.get(uri);
190             if (count == null) {
191                 mOpenSessions.put(uri, 1);
192                 return;
193             }
194             mOpenSessions.put(uri, count + 1);
195             Log.w(TAG, "Open nested session for " + uri, new Throwable());
196         }
197 
close(final Uri uri)198         void close(final Uri uri) {
199             final int count = mOpenSessions.get(uri);
200             if (count == 1) {
201                 mOpenSessions.remove(uri);
202                 return;
203             }
204             mOpenSessions.put(uri, count - 1);
205             Log.w(TAG, "Close nested session for " + uri, new Throwable());
206         }
207     }
208 }
209