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