1 /* 2 * Copyright (C) 2021 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 com.android.systemui.flags 17 18 import android.content.Context 19 import android.database.ContentObserver 20 import android.net.Uri 21 import android.os.Handler 22 import android.test.suitebuilder.annotation.SmallTest 23 import com.android.systemui.SysuiTestCase 24 import com.android.systemui.util.mockito.any 25 import com.android.systemui.util.mockito.eq 26 import com.android.systemui.util.mockito.mock 27 import com.android.systemui.util.mockito.withArgCaptor 28 import com.google.common.truth.Truth.assertThat 29 import java.util.function.Consumer 30 import org.junit.Assert.assertThrows 31 import org.junit.Before 32 import org.junit.Test 33 import org.mockito.Mock 34 import org.mockito.Mockito.times 35 import org.mockito.Mockito.verify 36 import org.mockito.Mockito.verifyNoMoreInteractions 37 import org.mockito.Mockito.`when` as whenever 38 import org.mockito.MockitoAnnotations 39 40 /** 41 * NOTE: This test is for the version of FeatureFlagManager in src-release, which should not allow 42 * overriding, and should never return any value other than the one provided as the default. 43 */ 44 @SmallTest 45 class FlagManagerTest : SysuiTestCase() { 46 private lateinit var mFlagManager: FlagManager 47 48 @Mock private lateinit var mMockContext: Context 49 @Mock private lateinit var mFlagSettingsHelper: FlagSettingsHelper 50 @Mock private lateinit var mHandler: Handler 51 52 @Before 53 fun setup() { 54 MockitoAnnotations.initMocks(this) 55 mFlagManager = FlagManager(mMockContext, mFlagSettingsHelper, mHandler) 56 } 57 58 @Test 59 fun testContentObserverAddedAndRemoved() { 60 val listener1 = mock<FlagListenable.Listener>() 61 val listener2 = mock<FlagListenable.Listener>() 62 63 // no interactions before adding listener 64 verifyNoMoreInteractions(mFlagSettingsHelper) 65 66 // adding the first listener registers the observer 67 mFlagManager.addListener(ReleasedFlag("1", "test"), listener1) 68 val observer = withArgCaptor<ContentObserver> { 69 verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture()) 70 } 71 verifyNoMoreInteractions(mFlagSettingsHelper) 72 73 // adding another listener does nothing 74 mFlagManager.addListener(ReleasedFlag("2", "test"), listener2) 75 verifyNoMoreInteractions(mFlagSettingsHelper) 76 77 // removing the original listener does nothing with second one still present 78 mFlagManager.removeListener(listener1) 79 verifyNoMoreInteractions(mFlagSettingsHelper) 80 81 // removing the final listener unregisters the observer 82 mFlagManager.removeListener(listener2) 83 verify(mFlagSettingsHelper).unregisterContentObserver(eq(observer)) 84 verifyNoMoreInteractions(mFlagSettingsHelper) 85 } 86 87 @Test 88 fun testObserverClearsCache() { 89 val listener = mock<FlagListenable.Listener>() 90 val clearCacheAction = mock<Consumer<String>>() 91 mFlagManager.clearCacheAction = clearCacheAction 92 mFlagManager.addListener(ReleasedFlag("1", "test"), listener) 93 val observer = withArgCaptor<ContentObserver> { 94 verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture()) 95 } 96 observer.onChange(false, flagUri(1)) 97 verify(clearCacheAction).accept(eq("1")) 98 } 99 100 @Test 101 fun testObserverInvokesListeners() { 102 val listener1 = mock<FlagListenable.Listener>() 103 val listener10 = mock<FlagListenable.Listener>() 104 mFlagManager.addListener(ReleasedFlag("1", "test"), listener1) 105 mFlagManager.addListener(ReleasedFlag("10", "test"), listener10) 106 val observer = withArgCaptor<ContentObserver> { 107 verify(mFlagSettingsHelper).registerContentObserver(any(), any(), capture()) 108 } 109 observer.onChange(false, flagUri(1)) 110 val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> { 111 verify(listener1).onFlagChanged(capture()) 112 } 113 assertThat(flagEvent1.flagName).isEqualTo("1") 114 verifyNoMoreInteractions(listener1, listener10) 115 116 observer.onChange(false, flagUri(10)) 117 val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> { 118 verify(listener10).onFlagChanged(capture()) 119 } 120 assertThat(flagEvent10.flagName).isEqualTo("10") 121 verifyNoMoreInteractions(listener1, listener10) 122 } 123 124 fun flagUri(id: Int): Uri = Uri.parse("content://settings/system/systemui/flags/$id") 125 126 @Test 127 fun testOnlySpecificFlagListenerIsInvoked() { 128 val listener1 = mock<FlagListenable.Listener>() 129 val listener10 = mock<FlagListenable.Listener>() 130 mFlagManager.addListener(ReleasedFlag("1", "test"), listener1) 131 mFlagManager.addListener(ReleasedFlag("10", "test"), listener10) 132 133 mFlagManager.dispatchListenersAndMaybeRestart("1", null) 134 val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> { 135 verify(listener1).onFlagChanged(capture()) 136 } 137 assertThat(flagEvent1.flagName).isEqualTo("1") 138 verifyNoMoreInteractions(listener1, listener10) 139 140 mFlagManager.dispatchListenersAndMaybeRestart("10", null) 141 val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> { 142 verify(listener10).onFlagChanged(capture()) 143 } 144 assertThat(flagEvent10.flagName).isEqualTo("10") 145 verifyNoMoreInteractions(listener1, listener10) 146 } 147 148 @Test 149 fun testSameListenerCanBeUsedForMultipleFlags() { 150 val listener = mock<FlagListenable.Listener>() 151 mFlagManager.addListener(ReleasedFlag("1", "test"), listener) 152 mFlagManager.addListener(ReleasedFlag("10", "test"), listener) 153 154 mFlagManager.dispatchListenersAndMaybeRestart("1", null) 155 val flagEvent1 = withArgCaptor<FlagListenable.FlagEvent> { 156 verify(listener).onFlagChanged(capture()) 157 } 158 assertThat(flagEvent1.flagName).isEqualTo("1") 159 verifyNoMoreInteractions(listener) 160 161 mFlagManager.dispatchListenersAndMaybeRestart("10", null) 162 val flagEvent10 = withArgCaptor<FlagListenable.FlagEvent> { 163 verify(listener, times(2)).onFlagChanged(capture()) 164 } 165 assertThat(flagEvent10.flagName).isEqualTo("10") 166 verifyNoMoreInteractions(listener) 167 } 168 169 @Test 170 fun testRestartWithNoListeners() { 171 val restartAction = mock<Consumer<Boolean>>() 172 mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction) 173 verify(restartAction).accept(eq(false)) 174 verifyNoMoreInteractions(restartAction) 175 } 176 177 @Test 178 fun testListenerCanSuppressRestart() { 179 val restartAction = mock<Consumer<Boolean>>() 180 mFlagManager.addListener(ReleasedFlag("1", "test")) { event -> 181 event.requestNoRestart() 182 } 183 mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction) 184 verify(restartAction).accept(eq(true)) 185 verifyNoMoreInteractions(restartAction) 186 } 187 188 @Test 189 fun testListenerOnlySuppressesRestartForOwnFlag() { 190 val restartAction = mock<Consumer<Boolean>>() 191 mFlagManager.addListener(ReleasedFlag("10", "test")) { event -> 192 event.requestNoRestart() 193 } 194 mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction) 195 verify(restartAction).accept(eq(false)) 196 verifyNoMoreInteractions(restartAction) 197 } 198 199 @Test 200 fun testRestartWhenNotAllListenersRequestSuppress() { 201 val restartAction = mock<Consumer<Boolean>>() 202 mFlagManager.addListener(ReleasedFlag("10", "test")) { event -> 203 event.requestNoRestart() 204 } 205 mFlagManager.addListener(ReleasedFlag("10", "test")) { 206 // do not request 207 } 208 mFlagManager.dispatchListenersAndMaybeRestart("1", restartAction) 209 verify(restartAction).accept(eq(false)) 210 verifyNoMoreInteractions(restartAction) 211 } 212 213 @Test 214 fun testReadBooleanFlag() { 215 // test that null string returns null 216 whenever(mFlagSettingsHelper.getString(any())).thenReturn(null) 217 assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isNull() 218 219 // test that empty string returns null 220 whenever(mFlagSettingsHelper.getString(any())).thenReturn("") 221 assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isNull() 222 223 // test false 224 whenever(mFlagSettingsHelper.getString(any())) 225 .thenReturn("{\"type\":\"boolean\",\"value\":false}") 226 assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isFalse() 227 228 // test true 229 whenever(mFlagSettingsHelper.getString(any())) 230 .thenReturn("{\"type\":\"boolean\",\"value\":true}") 231 assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isTrue() 232 233 // Reading a value of a different type should just return null 234 whenever(mFlagSettingsHelper.getString(any())) 235 .thenReturn("{\"type\":\"string\",\"value\":\"foo\"}") 236 assertThat(mFlagManager.readFlagValue("1", BooleanFlagSerializer)).isNull() 237 238 // Reading a value that isn't json should throw an exception 239 assertThrows(InvalidFlagStorageException::class.java) { 240 whenever(mFlagSettingsHelper.getString(any())).thenReturn("1") 241 mFlagManager.readFlagValue("1", BooleanFlagSerializer) 242 } 243 } 244 245 @Test 246 fun testSerializeBooleanFlag() { 247 // test false 248 assertThat(BooleanFlagSerializer.toSettingsData(false)) 249 .isEqualTo("{\"type\":\"boolean\",\"value\":false}") 250 251 // test true 252 assertThat(BooleanFlagSerializer.toSettingsData(true)) 253 .isEqualTo("{\"type\":\"boolean\",\"value\":true}") 254 } 255 256 @Test 257 fun testReadStringFlag() { 258 // test that null string returns null 259 whenever(mFlagSettingsHelper.getString(any())).thenReturn(null) 260 assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isNull() 261 262 // test that empty string returns null 263 whenever(mFlagSettingsHelper.getString(any())).thenReturn("") 264 assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isNull() 265 266 // test json with the empty string value returns empty string 267 whenever(mFlagSettingsHelper.getString(any())) 268 .thenReturn("{\"type\":\"string\",\"value\":\"\"}") 269 assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isEqualTo("") 270 271 // test string with value is returned 272 whenever(mFlagSettingsHelper.getString(any())) 273 .thenReturn("{\"type\":\"string\",\"value\":\"foo\"}") 274 assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isEqualTo("foo") 275 276 // Reading a value of a different type should just return null 277 whenever(mFlagSettingsHelper.getString(any())) 278 .thenReturn("{\"type\":\"boolean\",\"value\":false}") 279 assertThat(mFlagManager.readFlagValue("1", StringFlagSerializer)).isNull() 280 281 // Reading a value that isn't json should throw an exception 282 assertThrows(InvalidFlagStorageException::class.java) { 283 whenever(mFlagSettingsHelper.getString(any())).thenReturn("1") 284 mFlagManager.readFlagValue("1", StringFlagSerializer) 285 } 286 } 287 288 @Test 289 fun testSerializeStringFlag() { 290 // test empty string 291 assertThat(StringFlagSerializer.toSettingsData("")) 292 .isEqualTo("{\"type\":\"string\",\"value\":\"\"}") 293 294 // test string "foo" 295 assertThat(StringFlagSerializer.toSettingsData("foo")) 296 .isEqualTo("{\"type\":\"string\",\"value\":\"foo\"}") 297 } 298 } 299