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