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 package android.car.test.mocks; 17 18 import static org.mockito.ArgumentMatchers.any; 19 import static org.mockito.ArgumentMatchers.anyInt; 20 import static org.mockito.ArgumentMatchers.anyString; 21 import static org.mockito.Mockito.when; 22 23 import android.annotation.Nullable; 24 import android.car.test.mocks.AbstractExtendedMockitoTestCase.CustomMockitoSessionBuilder; 25 import android.provider.Settings; 26 import android.util.ArrayMap; 27 import android.util.Log; 28 29 import com.android.internal.util.Preconditions; 30 31 import org.mockito.invocation.InvocationOnMock; 32 import org.mockito.stubbing.Answer; 33 34 35 // TODO (b/156033195): Clean settings API. For example, don't mock xyzForUser() methods (as 36 // they should not be used due to mainline) and explicitly use a MockSettings per user or 37 // something like that (to make sure the code being test is passing the writer userId to 38 // Context.createContextAsUser()) 39 public final class MockSettings { 40 41 private static final String TAG = MockSettings.class.getSimpleName(); 42 private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); 43 44 private static final int INVALID_DEFAULT_INDEX = -1; 45 46 private final ArrayMap<String, Object> mSettingsMapping = new ArrayMap<>(); 47 MockSettings(CustomMockitoSessionBuilder sessionBuilder)48 public MockSettings(CustomMockitoSessionBuilder sessionBuilder) { 49 // TODO (b/156033195): change from mock to spy - or don't use mock at all 50 sessionBuilder 51 .mockStatic(Settings.Global.class) 52 .mockStatic(Settings.System.class) 53 .mockStatic(Settings.Secure.class); 54 sessionBuilder.setSessionCallback(() -> setExpectations()); 55 } 56 setExpectations()57 private void setExpectations() { 58 Answer<Object> insertObjectAnswer = 59 invocation -> insertObjectFromInvocation(invocation, 1, 2); 60 Answer<Integer> getIntAnswer = invocation -> 61 getAnswer(invocation, Integer.class, 1, 2); 62 Answer<String> getStringAnswer = invocation -> 63 getAnswer(invocation, String.class, 1, INVALID_DEFAULT_INDEX); 64 65 when(Settings.Global.putInt(any(), any(), anyInt())).thenAnswer(insertObjectAnswer); 66 67 when(Settings.Global.getInt(any(), any(), anyInt())).thenAnswer(getIntAnswer); 68 69 when(Settings.System.putInt(any(), any(), anyInt())).thenAnswer(insertObjectAnswer); 70 71 when(Settings.System.getInt(any(), any(), anyInt())).thenAnswer(getIntAnswer); 72 73 when(Settings.Secure.putIntForUser(any(), any(), anyInt(), anyInt())) 74 .thenAnswer(insertObjectAnswer); 75 76 when(Settings.Secure.getIntForUser(any(), any(), anyInt(), anyInt())) 77 .thenAnswer(getIntAnswer); 78 79 when(Settings.Secure.getInt(any(), any(), anyInt())).thenAnswer(getIntAnswer); 80 81 when(Settings.Secure.putStringForUser(any(), anyString(), anyString(), anyInt())) 82 .thenAnswer(insertObjectAnswer); 83 84 when(Settings.Global.putString(any(), any(), any())) 85 .thenAnswer(insertObjectAnswer); 86 87 when(Settings.Global.getString(any(), any())).thenAnswer(getStringAnswer); 88 89 when(Settings.System.putIntForUser(any(), any(), anyInt(), anyInt())) 90 .thenAnswer(insertObjectAnswer); 91 92 when(Settings.System.getIntForUser(any(), any(), anyInt(), anyInt())) 93 .thenAnswer(getIntAnswer); 94 95 when(Settings.System.putStringForUser(any(), any(), anyString(), anyInt())) 96 .thenAnswer(insertObjectAnswer); 97 98 when(Settings.System.putString(any(), any(), any())) 99 .thenAnswer(insertObjectAnswer); 100 } 101 insertObjectFromInvocation(InvocationOnMock invocation, int keyIndex, int valueIndex)102 private Object insertObjectFromInvocation(InvocationOnMock invocation, int keyIndex, 103 int valueIndex) { 104 String key = (String) invocation.getArguments()[keyIndex]; 105 Object value = invocation.getArguments()[valueIndex]; 106 insertObject(key, value); 107 // NOTE: the return value is not really used but it's needed so it can be used as a lambda 108 // for Answer<REAL_TYPE>. In fact, returning `value` would cause tests to fail due to 109 // invalid casts at runtime. 110 return null; 111 } 112 insertObject(String key, Object value)113 private void insertObject(String key, Object value) { 114 if (VERBOSE) { 115 Log.v(TAG, "Inserting Setting " + key + ": " + value); 116 } 117 mSettingsMapping.put(key, value); 118 } 119 getAnswer(InvocationOnMock invocation, Class<T> clazz, int keyIndex, int defaultValueIndex)120 private <T> T getAnswer(InvocationOnMock invocation, Class<T> clazz, int keyIndex, 121 int defaultValueIndex) { 122 String key = (String) invocation.getArguments()[keyIndex]; 123 T defaultValue = null; 124 if (defaultValueIndex > INVALID_DEFAULT_INDEX) { 125 defaultValue = safeCast(invocation.getArguments()[defaultValueIndex], clazz); 126 } 127 return get(key, defaultValue, clazz); 128 } 129 130 @Nullable get(String key, T defaultValue, Class<T> clazz)131 private <T> T get(String key, T defaultValue, Class<T> clazz) { 132 if (VERBOSE) { 133 Log.v(TAG, "get(): key=" + key + ", default=" + defaultValue + ", class=" + clazz); 134 } 135 Object value = mSettingsMapping.get(key); 136 if (value == null) { 137 if (VERBOSE) { 138 Log.v(TAG, "not found"); 139 } 140 return defaultValue; 141 } 142 143 if (VERBOSE) { 144 Log.v(TAG, "returning " + value); 145 } 146 return safeCast(value, clazz); 147 } 148 safeCast(Object value, Class<T> clazz)149 private static <T> T safeCast(Object value, Class<T> clazz) { 150 if (value == null) { 151 return null; 152 } 153 Preconditions.checkArgument(value.getClass() == clazz, 154 "Setting value has class %s but requires class %s", 155 value.getClass(), clazz); 156 return clazz.cast(value); 157 } 158 159 /** 160 * Adds key-value(int) pair in mocked Settings.Global and Settings.Secure 161 */ putInt(String key, int value)162 public void putInt(String key, int value) { 163 insertObject(key, value); 164 } 165 166 /** 167 * Adds key-value(String) pair in mocked Settings.Global and Settings.Secure 168 */ putString(String key, String value)169 public void putString(String key, String value) { 170 insertObject(key, value); 171 } 172 getString(String key)173 public String getString(String key) { 174 return get(key, null, String.class); 175 } 176 getInt(String key)177 public int getInt(String key) { 178 return get(key, null, Integer.class); 179 } 180 assertDoesNotContainsKey(String key)181 public void assertDoesNotContainsKey(String key) { 182 if (mSettingsMapping.containsKey(key)) { 183 throw new AssertionError("Should not have key " + key + ", but has: " 184 + mSettingsMapping.get(key)); 185 } 186 } 187 } 188