diff options
| author | 2023-10-31 14:00:49 +0000 | |
|---|---|---|
| committer | 2023-10-31 14:00:49 +0000 | |
| commit | b665746a9ee06a6313eb4f03ca450a9c3e51e019 (patch) | |
| tree | 20ad62c96963ad6f98223ba5be20a0ccd266d5e0 | |
| parent | 1f90210c73a5eebbf8436657ef682c12582593b5 (diff) | |
| parent | 627e370c1c7bdf4f4af63eb2302ee27dac64c1b8 (diff) | |
Merge "Add flag dependencies test and notification" into main
8 files changed, 288 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 04b2852db9e2..a41bb2f67ad2 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -54,6 +54,7 @@ import com.android.systemui.doze.dagger.DozeComponent; import com.android.systemui.dreams.dagger.DreamModule; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.FlagDependenciesModule; import com.android.systemui.flags.FlagsModule; import com.android.systemui.keyboard.KeyboardModule; import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule; @@ -182,6 +183,7 @@ import javax.inject.Named; DreamModule.class, FalsingModule.class, FlagsModule.class, + FlagDependenciesModule.class, FooterActionsModule.class, KeyEventRepositoryModule.class, KeyboardModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt new file mode 100644 index 000000000000..730a7a60d5c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.Flags as Classic +import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor +import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor +import javax.inject.Inject + +/** A class in which engineers can define flag dependencies */ +@SysUISingleton +class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, handler: Handler) : + FlagDependenciesBase(featureFlags, handler) { + override fun defineDependencies() { + FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token + NotificationIconContainerRefactor.token dependsOn Classic.NOTIFICATION_SHELF_REFACTOR + + // These two flags are effectively linked. We should migrate them to a single aconfig flag. + Classic.MIGRATE_NSSL dependsOn Classic.MIGRATE_KEYGUARD_STATUS_VIEW + Classic.MIGRATE_KEYGUARD_STATUS_VIEW dependsOn Classic.MIGRATE_NSSL + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt new file mode 100644 index 000000000000..ae3b501d3006 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependenciesBase.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.util.Log +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.util.Compile +import com.android.systemui.util.asIndenting +import com.android.systemui.util.withIncreasedIndent +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import java.io.PrintWriter +import javax.inject.Inject + +/** + * This base class provides the helpers necessary to define dependencies between flags from the + * different flagging systems; classic and aconfig. This class is to be extended + */ +abstract class FlagDependenciesBase( + private val featureFlags: FeatureFlagsClassic, + private val handler: Handler +) : CoreStartable { + protected abstract fun defineDependencies() + + private val workingDependencies = mutableListOf<Dependency>() + private var allDependencies = emptyList<Dependency>() + private var unmetDependencies = emptyList<Dependency>() + + override fun start() { + defineDependencies() + allDependencies = workingDependencies.toList() + unmetDependencies = workingDependencies.filter { !it.isMet } + workingDependencies.clear() + if (unmetDependencies.isNotEmpty()) { + handler.warnAboutBadFlagConfiguration(all = allDependencies, unmet = unmetDependencies) + } + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.asIndenting().run { + println("allDependencies: ${allDependencies.size}") + withIncreasedIndent { allDependencies.forEach(::println) } + println("unmetDependencies: ${unmetDependencies.size}") + withIncreasedIndent { unmetDependencies.forEach(::println) } + } + } + + /** A dependency where enabling the `alpha` feature depends on enabling the `beta` feature */ + class Dependency( + private val alphaName: String, + private val alphaEnabled: Boolean, + private val betaName: String, + private val betaEnabled: Boolean + ) { + val isMet = !alphaEnabled || betaEnabled + override fun toString(): String { + val isMetBullet = if (isMet) "+" else "-" + return "$isMetBullet $alphaName ($alphaEnabled) DEPENDS ON $betaName ($betaEnabled)" + } + } + + protected infix fun UnreleasedFlag.dependsOn(other: UnreleasedFlag) = + addDependency(this.token, other.token) + protected infix fun ReleasedFlag.dependsOn(other: UnreleasedFlag) = + addDependency(this.token, other.token) + protected infix fun ReleasedFlag.dependsOn(other: ReleasedFlag) = + addDependency(this.token, other.token) + protected infix fun FlagToken.dependsOn(other: UnreleasedFlag) = + addDependency(this, other.token) + protected infix fun FlagToken.dependsOn(other: ReleasedFlag) = addDependency(this, other.token) + protected infix fun FlagToken.dependsOn(other: FlagToken) = addDependency(this, other) + + private val UnreleasedFlag.token + get() = FlagToken("classic.$name", featureFlags.isEnabled(this)) + private val ReleasedFlag.token + get() = FlagToken("classic.$name", featureFlags.isEnabled(this)) + + /** Add a dependency to the working list */ + private fun addDependency(first: FlagToken, second: FlagToken) { + if (!Compile.IS_DEBUG) return // `user` builds should omit all this code + workingDependencies.add( + Dependency(first.name, first.isEnabled, second.name, second.isEnabled) + ) + } + + /** An interface which handles a warning about a bad flag configuration. */ + interface Handler { + fun warnAboutBadFlagConfiguration(all: List<Dependency>, unmet: List<Dependency>) + } +} + +/** + * A flag dependencies handler which posts a notification and logs to logcat that the configuration + * is invalid. + */ +@SysUISingleton +class FlagDependenciesNotifier +@Inject +constructor( + private val context: Context, + private val notifManager: NotificationManager, +) : FlagDependenciesBase.Handler { + override fun warnAboutBadFlagConfiguration( + all: List<FlagDependenciesBase.Dependency>, + unmet: List<FlagDependenciesBase.Dependency> + ) { + val title = "Invalid flag dependencies: ${unmet.size} of ${all.size}" + val details = unmet.joinToString("\n") + Log.e("FlagDependencies", "$title:\n$details") + val channel = NotificationChannel("FLAGS", "Flags", NotificationManager.IMPORTANCE_DEFAULT) + val notification = + Notification.Builder(context, channel.id) + .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) + .setContentTitle(title) + .setContentText(details) + .setStyle(Notification.BigTextStyle().bigText(details)) + .build() + notifManager.createNotificationChannel(channel) + notifManager.notify("flags", 0, notification) + } +} + +@Module +abstract class FlagDependenciesModule { + + /** Inject into FlagDependencies. */ + @Binds + @IntoMap + @ClassKey(FlagDependencies::class) + abstract fun bindFlagDependencies(sysui: FlagDependencies): CoreStartable + + /** Bind the flag dependencies handler */ + @Binds + abstract fun bindFlagDependenciesHandler( + handler: FlagDependenciesNotifier + ): FlagDependenciesBase.Handler +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/OWNERS b/packages/SystemUI/src/com/android/systemui/flags/OWNERS index c9d2db1d8acb..57ebccbd6b8b 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/OWNERS +++ b/packages/SystemUI/src/com/android/systemui/flags/OWNERS @@ -10,3 +10,6 @@ cinek@google.com alexflo@google.com dsandler@android.com adamcohen@google.com + +# Anyone in System UI can declare dependencies between flags +per-file FlagDependencies.kt = file:../../../../../OWNERS diff --git a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt index db31e36a9625..ae67e60b75f7 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/RefactorFlagUtils.kt @@ -26,6 +26,7 @@ import android.util.Log * ``` * object SomeRefactor { * const val FLAG_NAME = Flags.SOME_REFACTOR + * val token: FlagToken get() = FlagToken(FLAG_NAME, isEnabled) * @JvmStatic inline val isEnabled get() = Flags.someRefactor() * @JvmStatic inline fun isUnexpectedlyInLegacyMode() = * RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) @@ -105,3 +106,8 @@ object RefactorFlagUtils { /** Tag used for non-crashing logs or when the [ASSERT_TAG] has been silenced. */ private const val STANDARD_TAG = "RefactorFlag" } + +/** An object which allows dependency tracking */ +data class FlagToken(val name: String, val isEnabled: Boolean) { + override fun toString(): String = "$name (${if (isEnabled) "enabled" else "disabled"})" +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt index 94e70e5521c3..7e6044eb6869 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/shared/FooterViewRefactor.kt @@ -17,13 +17,19 @@ package com.android.systemui.statusbar.notification.footer.shared import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the FooterView refactor flag state. */ @Suppress("NOTHING_TO_INLINE") object FooterViewRefactor { + /** The aconfig flag name */ const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + /** Is the refactor enabled */ @JvmStatic inline val isEnabled diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt index dee609cd8105..a08af7554467 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationIconContainerRefactor.kt @@ -16,13 +16,19 @@ package com.android.systemui.statusbar.notification.shared import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken import com.android.systemui.flags.RefactorFlagUtils /** Helper for reading or using the NotificationIconContainer refactor flag state. */ @Suppress("NOTHING_TO_INLINE") object NotificationIconContainerRefactor { + /** The aconfig flag name */ const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_ICON_CONTAINER_REFACTOR + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + /** Is the refactor enabled? */ @JvmStatic inline val isEnabled diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt new file mode 100644 index 000000000000..936aa8bb819c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FlagDependenciesTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import java.io.PrintWriter +import kotlin.test.fail +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class FlagDependenciesTest : SysuiTestCase() { + @Test + fun testRelease() { + testFlagDependencies(teamfood = false).start() + } + + @Test + fun testTeamfood() { + testFlagDependencies(teamfood = true).start() + } + + private fun testFlagDependencies(teamfood: Boolean) = + FlagDependencies(TestFeatureFlags(teamfood = teamfood), TestHandler()) + + private class TestHandler : FlagDependenciesBase.Handler { + override fun warnAboutBadFlagConfiguration( + all: List<FlagDependenciesBase.Dependency>, + unmet: List<FlagDependenciesBase.Dependency> + ) { + val title = "${unmet.size} invalid of ${all.size} flag dependencies" + val details = unmet.joinToString("\n") + fail("$title:\n$details") + } + } + + private class TestFeatureFlags(val teamfood: Boolean) : FeatureFlagsClassic { + private val unsupported: Nothing + get() = fail("Unsupported") + + override fun isEnabled(flag: ReleasedFlag): Boolean = true + override fun isEnabled(flag: UnreleasedFlag): Boolean = teamfood && flag.teamfood + override fun isEnabled(flag: ResourceBooleanFlag): Boolean = unsupported + override fun isEnabled(flag: SysPropBooleanFlag): Boolean = unsupported + override fun getString(flag: StringFlag): String = unsupported + override fun getString(flag: ResourceStringFlag): String = unsupported + override fun getInt(flag: IntFlag): Int = unsupported + override fun getInt(flag: ResourceIntFlag): Int = unsupported + override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) = unsupported + override fun removeListener(listener: FlagListenable.Listener) = unsupported + override fun dump(writer: PrintWriter, args: Array<out String>?) = unsupported + } +} |