Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b665746a authored by Jeff DeCew's avatar Jeff DeCew Committed by Android (Google) Code Review
Browse files

Merge "Add flag dependencies test and notification" into main

parents 1f90210c 627e370c
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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,
+37 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+158 −0
Original line number Diff line number Diff line
/*
 * 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
}
+3 −0
Original line number Diff line number Diff line
@@ -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
+6 −0
Original line number Diff line number Diff line
@@ -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"})"
}
Loading