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