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