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