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.BroadcastReceiver
19 import android.content.Context
20 import android.content.Intent
21 import android.content.pm.PackageManager.NameNotFoundException
22 import android.content.res.Resources
23 import android.content.res.Resources.NotFoundException
24 import android.test.suitebuilder.annotation.SmallTest
25 import com.android.systemui.SysuiTestCase
26 import com.android.systemui.util.mockito.any
27 import com.android.systemui.util.mockito.eq
28 import com.android.systemui.util.mockito.nullable
29 import com.android.systemui.util.mockito.withArgCaptor
30 import com.android.systemui.util.settings.GlobalSettings
31 import com.google.common.truth.Truth.assertThat
32 import org.junit.Assert
33 import org.junit.Before
34 import org.junit.Test
35 import org.mockito.ArgumentMatchers.anyInt
36 import org.mockito.Mock
37 import org.mockito.Mockito.anyBoolean
38 import org.mockito.Mockito.anyString
39 import org.mockito.Mockito.inOrder
40 import org.mockito.Mockito.never
41 import org.mockito.Mockito.times
42 import org.mockito.Mockito.verify
43 import org.mockito.Mockito.verifyNoMoreInteractions
44 import org.mockito.MockitoAnnotations
45 import java.io.PrintWriter
46 import java.io.Serializable
47 import java.io.StringWriter
48 import java.util.function.Consumer
49 import org.mockito.Mockito.`when` as whenever
50 
51 /**
52  * NOTE: This test is for the version of FeatureFlagManager in src-debug, which allows overriding
53  * the default.
54  */
55 @SmallTest
56 class FeatureFlagsDebugTest : SysuiTestCase() {
57     private lateinit var featureFlagsDebug: FeatureFlagsDebug
58 
59     @Mock
60     private lateinit var flagManager: FlagManager
61     @Mock
62     private lateinit var mockContext: Context
63     @Mock
64     private lateinit var globalSettings: GlobalSettings
65     @Mock
66     private lateinit var systemProperties: SystemPropertiesHelper
67     @Mock
68     private lateinit var resources: Resources
69     @Mock
70     private lateinit var restarter: Restarter
71     private val flagMap = mutableMapOf<String, Flag<*>>()
72     private lateinit var broadcastReceiver: BroadcastReceiver
73     private lateinit var clearCacheAction: Consumer<String>
74     private val serverFlagReader = ServerFlagReaderFake()
75 
76     private val teamfoodableFlagA = UnreleasedFlag(
77         name = "a", namespace = "test", teamfood = true
78     )
79     private val teamfoodableFlagB = ReleasedFlag(
80         name = "b", namespace = "test", teamfood = true
81     )
82 
83     @Before
84     fun setup() {
85         MockitoAnnotations.initMocks(this)
86         flagMap.put(Flags.TEAMFOOD.name, Flags.TEAMFOOD)
87         flagMap.put(teamfoodableFlagA.name, teamfoodableFlagA)
88         flagMap.put(teamfoodableFlagB.name, teamfoodableFlagB)
89         featureFlagsDebug = FeatureFlagsDebug(
90             flagManager,
91             mockContext,
92             globalSettings,
93             systemProperties,
94             resources,
95             serverFlagReader,
96             flagMap,
97             restarter
98         )
99         featureFlagsDebug.init()
100         verify(flagManager).onSettingsChangedAction = any()
101         broadcastReceiver = withArgCaptor {
102             verify(mockContext).registerReceiver(
103                 capture(), any(), nullable(), nullable(),
104                 any()
105             )
106         }
107         clearCacheAction = withArgCaptor {
108             verify(flagManager).clearCacheAction = capture()
109         }
110         whenever(flagManager.nameToSettingsKey(any())).thenAnswer { "key-${it.arguments[0]}" }
111     }
112 
113     @Test
114     fun readBooleanFlag() {
115         // Remember that the TEAMFOOD flag is id#1 and has special behavior.
116         whenever(flagManager.readFlagValue<Boolean>(eq("3"), any())).thenReturn(true)
117         whenever(flagManager.readFlagValue<Boolean>(eq("4"), any())).thenReturn(false)
118 
119         assertThat(
120             featureFlagsDebug.isEnabled(
121                 ReleasedFlag(
122                     name = "2",
123                     namespace = "test"
124                 )
125             )
126         ).isTrue()
127         assertThat(
128             featureFlagsDebug.isEnabled(
129                 UnreleasedFlag(
130                     name = "3",
131                     namespace = "test"
132                 )
133             )
134         ).isTrue()
135         assertThat(
136             featureFlagsDebug.isEnabled(
137                 ReleasedFlag(
138                     name = "4",
139                     namespace = "test"
140                 )
141             )
142         ).isFalse()
143         assertThat(
144             featureFlagsDebug.isEnabled(
145                 UnreleasedFlag(
146                     name = "5",
147                     namespace = "test"
148                 )
149             )
150         ).isFalse()
151     }
152 
153     @Test
154     fun teamFoodFlag_False() {
155         whenever(flagManager.readFlagValue<Boolean>(
156             eq(Flags.TEAMFOOD.name), any())).thenReturn(false)
157         assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagA)).isFalse()
158         assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue()
159 
160         // Regular boolean flags should still test the same.
161         // Only our teamfoodableFlag should change.
162         readBooleanFlag()
163     }
164 
165     @Test
166     fun teamFoodFlag_True() {
167         whenever(flagManager.readFlagValue<Boolean>(
168             eq(Flags.TEAMFOOD.name), any())).thenReturn(true)
169         assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
170         assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagB)).isTrue()
171 
172         // Regular boolean flags should still test the same.
173         // Only our teamfoodableFlag should change.
174         readBooleanFlag()
175     }
176 
177     @Test
178     fun teamFoodFlag_Overridden() {
179         whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagA.name), any()))
180             .thenReturn(true)
181         whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.name), any()))
182             .thenReturn(false)
183         whenever(flagManager.readFlagValue<Boolean>(
184             eq(Flags.TEAMFOOD.name), any())).thenReturn(true)
185         assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagA)).isTrue()
186         assertThat(featureFlagsDebug.isEnabled(teamfoodableFlagB)).isFalse()
187 
188         // Regular boolean flags should still test the same.
189         // Only our teamfoodableFlag should change.
190         readBooleanFlag()
191     }
192 
193     @Test
194     fun readResourceBooleanFlag() {
195         whenever(resources.getBoolean(1001)).thenReturn(false)
196         whenever(resources.getBoolean(1002)).thenReturn(true)
197         whenever(resources.getBoolean(1003)).thenReturn(false)
198         whenever(resources.getBoolean(1004)).thenAnswer { throw NameNotFoundException() }
199         whenever(resources.getBoolean(1005)).thenAnswer { throw NameNotFoundException() }
200 
201         whenever(flagManager.readFlagValue<Boolean>(eq("3"), any())).thenReturn(true)
202         whenever(flagManager.readFlagValue<Boolean>(eq("5"), any())).thenReturn(false)
203 
204         assertThat(
205             featureFlagsDebug.isEnabled(
206                 ResourceBooleanFlag(
207                     "1",
208                     "test",
209                     1001
210                 )
211             )
212         ).isFalse()
213         assertThat(featureFlagsDebug.isEnabled(ResourceBooleanFlag("2", "test", 1002))).isTrue()
214         assertThat(featureFlagsDebug.isEnabled(ResourceBooleanFlag("3", "test", 1003))).isTrue()
215 
216         Assert.assertThrows(NameNotFoundException::class.java) {
217             featureFlagsDebug.isEnabled(ResourceBooleanFlag("4", "test", 1004))
218         }
219         // Test that resource is loaded (and validated) even when the setting is set.
220         //  This prevents developers from not noticing when they reference an invalid resource.
221         Assert.assertThrows(NameNotFoundException::class.java) {
222             featureFlagsDebug.isEnabled(ResourceBooleanFlag("5", "test", 1005))
223         }
224     }
225 
226     @Test
227     fun readSysPropBooleanFlag() {
228         whenever(systemProperties.getBoolean(anyString(), anyBoolean())).thenAnswer {
229             if ("b".equals(it.getArgument<String?>(0))) {
230                 return@thenAnswer true
231             }
232             return@thenAnswer it.getArgument(1)
233         }
234 
235         assertThat(featureFlagsDebug.isEnabled(SysPropBooleanFlag("a", "test"))).isFalse()
236         assertThat(featureFlagsDebug.isEnabled(SysPropBooleanFlag("b", "test"))).isTrue()
237         assertThat(featureFlagsDebug.isEnabled(SysPropBooleanFlag("c", "test", true))).isTrue()
238         assertThat(
239             featureFlagsDebug.isEnabled(
240                 SysPropBooleanFlag(
241                     "d",
242                     "test",
243                     false
244                 )
245             )
246         ).isFalse()
247         assertThat(featureFlagsDebug.isEnabled(SysPropBooleanFlag("e", "test"))).isFalse()
248     }
249 
250     @Test
251     fun readStringFlag() {
252         whenever(flagManager.readFlagValue<String>(eq("3"), any())).thenReturn("foo")
253         whenever(flagManager.readFlagValue<String>(eq("4"), any())).thenReturn("bar")
254         assertThat(featureFlagsDebug.getString(StringFlag("1", "test", "biz"))).isEqualTo("biz")
255         assertThat(featureFlagsDebug.getString(StringFlag("2", "test", "baz"))).isEqualTo("baz")
256         assertThat(featureFlagsDebug.getString(StringFlag("3", "test", "buz"))).isEqualTo("foo")
257         assertThat(featureFlagsDebug.getString(StringFlag("4", "test", "buz"))).isEqualTo("bar")
258     }
259 
260     @Test
261     fun readResourceStringFlag() {
262         whenever(resources.getString(1001)).thenReturn("")
263         whenever(resources.getString(1002)).thenReturn("resource2")
264         whenever(resources.getString(1003)).thenReturn("resource3")
265         whenever(resources.getString(1004)).thenReturn(null)
266         whenever(resources.getString(1005)).thenAnswer { throw NameNotFoundException() }
267         whenever(resources.getString(1006)).thenAnswer { throw NameNotFoundException() }
268 
269         whenever(flagManager.readFlagValue<String>(eq("3"), any())).thenReturn("override3")
270         whenever(flagManager.readFlagValue<String>(eq("4"), any())).thenReturn("override4")
271         whenever(flagManager.readFlagValue<String>(eq("6"), any())).thenReturn("override6")
272 
273         assertThat(
274             featureFlagsDebug.getString(
275                 ResourceStringFlag(
276                     "1",
277                     "test",
278                     1001
279                 )
280             )
281         ).isEqualTo("")
282         assertThat(
283             featureFlagsDebug.getString(
284                 ResourceStringFlag(
285                     "2",
286                     "test",
287                     1002
288                 )
289             )
290         ).isEqualTo("resource2")
291         assertThat(
292             featureFlagsDebug.getString(
293                 ResourceStringFlag(
294                     "3",
295                     "test",
296                     1003
297                 )
298             )
299         ).isEqualTo("override3")
300 
301         Assert.assertThrows(NullPointerException::class.java) {
302             featureFlagsDebug.getString(ResourceStringFlag("4", "test", 1004))
303         }
304         Assert.assertThrows(NameNotFoundException::class.java) {
305             featureFlagsDebug.getString(ResourceStringFlag("5", "test", 1005))
306         }
307         // Test that resource is loaded (and validated) even when the setting is set.
308         //  This prevents developers from not noticing when they reference an invalid resource.
309         Assert.assertThrows(NameNotFoundException::class.java) {
310             featureFlagsDebug.getString(ResourceStringFlag("6", "test", 1005))
311         }
312     }
313 
314     @Test
315     fun readIntFlag() {
316         whenever(flagManager.readFlagValue<Int>(eq("3"), any())).thenReturn(22)
317         whenever(flagManager.readFlagValue<Int>(eq("4"), any())).thenReturn(48)
318         assertThat(featureFlagsDebug.getInt(IntFlag("1", "test", 12))).isEqualTo(12)
319         assertThat(featureFlagsDebug.getInt(IntFlag("2", "test", 93))).isEqualTo(93)
320         assertThat(featureFlagsDebug.getInt(IntFlag("3", "test", 8))).isEqualTo(22)
321         assertThat(featureFlagsDebug.getInt(IntFlag("4", "test", 234))).isEqualTo(48)
322     }
323 
324     @Test
325     fun readResourceIntFlag() {
326         whenever(resources.getInteger(1001)).thenReturn(88)
327         whenever(resources.getInteger(1002)).thenReturn(61)
328         whenever(resources.getInteger(1003)).thenReturn(9342)
329         whenever(resources.getInteger(1004)).thenThrow(NotFoundException("unknown resource"))
330         whenever(resources.getInteger(1005)).thenThrow(NotFoundException("unknown resource"))
331         whenever(resources.getInteger(1006)).thenThrow(NotFoundException("unknown resource"))
332 
333         whenever(flagManager.readFlagValue<Int>(eq("3"), any())).thenReturn(20)
334         whenever(flagManager.readFlagValue<Int>(eq("4"), any())).thenReturn(500)
335         whenever(flagManager.readFlagValue<Int>(eq("5"), any())).thenReturn(9519)
336 
337         assertThat(featureFlagsDebug.getInt(ResourceIntFlag("1", "test", 1001))).isEqualTo(88)
338         assertThat(featureFlagsDebug.getInt(ResourceIntFlag("2", "test", 1002))).isEqualTo(61)
339         assertThat(featureFlagsDebug.getInt(ResourceIntFlag("3", "test", 1003))).isEqualTo(20)
340 
341         Assert.assertThrows(NotFoundException::class.java) {
342             featureFlagsDebug.getInt(ResourceIntFlag("4", "test", 1004))
343         }
344         // Test that resource is loaded (and validated) even when the setting is set.
345         //  This prevents developers from not noticing when they reference an invalid resource.
346         Assert.assertThrows(NotFoundException::class.java) {
347             featureFlagsDebug.getInt(ResourceIntFlag("5", "test", 1005))
348         }
349     }
350 
351     @Test
352     fun broadcastReceiver_IgnoresInvalidData() {
353         addFlag(UnreleasedFlag("1", "test"))
354         addFlag(ResourceBooleanFlag("2", "test", 1002))
355         addFlag(StringFlag("3", "test", "flag3"))
356         addFlag(ResourceStringFlag("4", "test", 1004))
357 
358         broadcastReceiver.onReceive(mockContext, null)
359         broadcastReceiver.onReceive(mockContext, Intent())
360         broadcastReceiver.onReceive(mockContext, Intent("invalid action"))
361         broadcastReceiver.onReceive(mockContext, Intent(FlagManager.ACTION_SET_FLAG))
362         setByBroadcast("0", false) // unknown id does nothing
363         setByBroadcast("1", "string") // wrong type does nothing
364         setByBroadcast("2", 123) // wrong type does nothing
365         setByBroadcast("3", false) // wrong type does nothing
366         setByBroadcast("4", 123) // wrong type does nothing
367         verifyNoMoreInteractions(flagManager, globalSettings)
368     }
369 
370     @Test
371     fun intentWithId_NoValueKeyClears() {
372         addFlag(UnreleasedFlag(name = "1", namespace = "test"))
373 
374         // trying to erase an id not in the map does nothing
375         broadcastReceiver.onReceive(
376             mockContext,
377             Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_NAME, "")
378         )
379         verifyNoMoreInteractions(flagManager, globalSettings)
380 
381         // valid id with no value puts empty string in the setting
382         broadcastReceiver.onReceive(
383             mockContext,
384             Intent(FlagManager.ACTION_SET_FLAG).putExtra(FlagManager.EXTRA_NAME, "1")
385         )
386         verifyPutData("1", "", numReads = 0)
387     }
388 
389     @Test
390     fun setBooleanFlag() {
391         addFlag(UnreleasedFlag("1", "test"))
392         addFlag(UnreleasedFlag("2", "test"))
393         addFlag(ResourceBooleanFlag("3", "test", 1003))
394         addFlag(ResourceBooleanFlag("4", "test", 1004))
395 
396         setByBroadcast("1", false)
397         verifyPutData("1", "{\"type\":\"boolean\",\"value\":false}")
398 
399         setByBroadcast("2", true)
400         verifyPutData("2", "{\"type\":\"boolean\",\"value\":true}")
401 
402         setByBroadcast("3", false)
403         verifyPutData("3", "{\"type\":\"boolean\",\"value\":false}")
404 
405         setByBroadcast("4", true)
406         verifyPutData("4", "{\"type\":\"boolean\",\"value\":true}")
407     }
408 
409     @Test
410     fun setStringFlag() {
411         addFlag(StringFlag("1", "1", "test"))
412         addFlag(ResourceStringFlag("2", "test", 1002))
413 
414         setByBroadcast("1", "override1")
415         verifyPutData("1", "{\"type\":\"string\",\"value\":\"override1\"}")
416 
417         setByBroadcast("2", "override2")
418         verifyPutData("2", "{\"type\":\"string\",\"value\":\"override2\"}")
419     }
420 
421     @Test
422     fun setFlag_ClearsCache() {
423         val flag1 = addFlag(StringFlag("1", "test", "flag1"))
424         whenever(flagManager.readFlagValue<String>(eq("1"), any())).thenReturn("original")
425 
426         // gets the flag & cache it
427         assertThat(featureFlagsDebug.getString(flag1)).isEqualTo("original")
428         verify(flagManager, times(1)).readFlagValue(eq("1"), eq(StringFlagSerializer))
429 
430         // hit the cache
431         assertThat(featureFlagsDebug.getString(flag1)).isEqualTo("original")
432         verifyNoMoreInteractions(flagManager)
433 
434         // set the flag
435         setByBroadcast("1", "new")
436         verifyPutData("1", "{\"type\":\"string\",\"value\":\"new\"}", numReads = 2)
437         whenever(flagManager.readFlagValue<String>(eq("1"), any())).thenReturn("new")
438 
439         assertThat(featureFlagsDebug.getString(flag1)).isEqualTo("new")
440         verify(flagManager, times(3)).readFlagValue(eq("1"), eq(StringFlagSerializer))
441     }
442 
443     @Test
444     fun serverSide_Overrides_MakesFalse() {
445         val flag = ReleasedFlag("100", "test")
446 
447         serverFlagReader.setFlagValue(flag.namespace, flag.name, false)
448 
449         assertThat(featureFlagsDebug.isEnabled(flag)).isFalse()
450     }
451 
452     @Test
453     fun serverSide_Overrides_MakesTrue() {
454         val flag = UnreleasedFlag(name = "100", namespace = "test")
455 
456         serverFlagReader.setFlagValue(flag.namespace, flag.name, true)
457         assertThat(featureFlagsDebug.isEnabled(flag)).isTrue()
458     }
459 
460     @Test
461     fun serverSide_OverrideUncached_NoRestart() {
462         // No one has read the flag, so it's not in the cache.
463         serverFlagReader.setFlagValue(
464             teamfoodableFlagA.namespace, teamfoodableFlagA.name, !teamfoodableFlagA.default)
465         verify(restarter, never()).restartSystemUI(anyString())
466     }
467 
468     @Test
469     fun serverSide_Override_Restarts() {
470         // Read it to put it in the cache.
471         featureFlagsDebug.isEnabled(teamfoodableFlagA)
472         serverFlagReader.setFlagValue(
473             teamfoodableFlagA.namespace, teamfoodableFlagA.name, !teamfoodableFlagA.default)
474         verify(restarter).restartSystemUI(anyString())
475     }
476 
477     @Test
478     fun serverSide_RedundantOverride_NoRestart() {
479         // Read it to put it in the cache.
480         featureFlagsDebug.isEnabled(teamfoodableFlagA)
481         serverFlagReader.setFlagValue(
482             teamfoodableFlagA.namespace, teamfoodableFlagA.name, teamfoodableFlagA.default)
483         verify(restarter, never()).restartSystemUI(anyString())
484     }
485 
486     @Test
487     fun dumpFormat() {
488         val flag1 = ReleasedFlag("1", "test")
489         val flag2 = ResourceBooleanFlag("2", "test", 1002)
490         val flag3 = UnreleasedFlag("3", "test")
491         val flag4 = StringFlag("4", "test", "")
492         val flag5 = StringFlag("5", "test", "flag5default")
493         val flag6 = ResourceStringFlag("6", "test", 1006)
494         val flag7 = ResourceStringFlag("7", "test", 1007)
495 
496         whenever(resources.getBoolean(1002)).thenReturn(true)
497         whenever(resources.getString(1006)).thenReturn("resource1006")
498         whenever(resources.getString(1007)).thenReturn("resource1007")
499         whenever(flagManager.readFlagValue(eq("7"), eq(StringFlagSerializer)))
500             .thenReturn("override7")
501 
502         // WHEN the flags have been accessed
503         assertThat(featureFlagsDebug.isEnabled(flag1)).isTrue()
504         assertThat(featureFlagsDebug.isEnabled(flag2)).isTrue()
505         assertThat(featureFlagsDebug.isEnabled(flag3)).isFalse()
506         assertThat(featureFlagsDebug.getString(flag4)).isEmpty()
507         assertThat(featureFlagsDebug.getString(flag5)).isEqualTo("flag5default")
508         assertThat(featureFlagsDebug.getString(flag6)).isEqualTo("resource1006")
509         assertThat(featureFlagsDebug.getString(flag7)).isEqualTo("override7")
510 
511         // THEN the dump contains the flags and the default values
512         val dump = dumpToString()
513         assertThat(dump).contains(" sysui_flag_1: true\n")
514         assertThat(dump).contains(" sysui_flag_2: true\n")
515         assertThat(dump).contains(" sysui_flag_3: false\n")
516         assertThat(dump).contains(" sysui_flag_4: [length=0] \"\"\n")
517         assertThat(dump).contains(" sysui_flag_5: [length=12] \"flag5default\"\n")
518         assertThat(dump).contains(" sysui_flag_6: [length=12] \"resource1006\"\n")
519         assertThat(dump).contains(" sysui_flag_7: [length=9] \"override7\"\n")
520     }
521 
522     private fun verifyPutData(name: String, data: String, numReads: Int = 1) {
523         inOrder(flagManager, globalSettings).apply {
524             verify(flagManager, times(numReads)).readFlagValue(eq(name), any<FlagSerializer<*>>())
525             verify(flagManager).nameToSettingsKey(eq(name))
526             verify(globalSettings).putStringForUser(eq("key-$name"), eq(data), anyInt())
527             verify(flagManager).dispatchListenersAndMaybeRestart(eq(name), any())
528         }.verifyNoMoreInteractions()
529         verifyNoMoreInteractions(flagManager, globalSettings)
530     }
531 
532     private fun setByBroadcast(name: String, value: Serializable?) {
533         val intent = Intent(FlagManager.ACTION_SET_FLAG)
534         intent.putExtra(FlagManager.EXTRA_NAME, name)
535         intent.putExtra(FlagManager.EXTRA_VALUE, value)
536         broadcastReceiver.onReceive(mockContext, intent)
537     }
538 
539     private fun <F : Flag<*>> addFlag(flag: F): F {
540         val old = flagMap.put(flag.name, flag)
541         check(old == null) { "Flag ${flag.name} already registered" }
542         return flag
543     }
544 
545     private fun dumpToString(): String {
546         val sw = StringWriter()
547         val pw = PrintWriter(sw)
548         featureFlagsDebug.dump(pw, emptyArray<String>())
549         pw.flush()
550         return sw.toString()
551     }
552 }
553