1 /* 2 * Copyright (C) 2023 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 17 package com.android.systemui.flags 18 19 import android.app.Notification 20 import android.app.NotificationChannel 21 import android.app.NotificationManager 22 import android.app.NotificationManager.IMPORTANCE_DEFAULT 23 import android.content.Context 24 import android.util.Log 25 import com.android.systemui.CoreStartable 26 import com.android.systemui.dagger.SysUISingleton 27 import com.android.systemui.util.Compile 28 import com.android.systemui.util.asIndenting 29 import com.android.systemui.util.printCollection 30 import dagger.Binds 31 import dagger.Module 32 import dagger.multibindings.ClassKey 33 import dagger.multibindings.IntoMap 34 import java.io.PrintWriter 35 import javax.inject.Inject 36 37 /** 38 * This base class provides the helpers necessary to define dependencies between flags from the 39 * different flagging systems; classic and aconfig. This class is to be extended 40 */ 41 abstract class FlagDependenciesBase( 42 private val featureFlags: FeatureFlagsClassic, 43 private val handler: Handler 44 ) : CoreStartable { defineDependenciesnull45 protected abstract fun defineDependencies() 46 47 private val workingDependencies = mutableListOf<Dependency>() 48 private var allDependencies = emptyList<Dependency>() 49 private var unmetDependencies = emptyList<Dependency>() 50 51 override fun start() { 52 if (!handler.enableDependencies) { 53 return 54 } 55 defineDependencies() 56 allDependencies = workingDependencies.toList() 57 unmetDependencies = workingDependencies.filter { !it.isMet } 58 workingDependencies.clear() 59 handler.onCollected(allDependencies) 60 if (unmetDependencies.isNotEmpty()) { 61 handler.warnAboutBadFlagConfiguration(all = allDependencies, unmet = unmetDependencies) 62 } 63 } 64 dumpnull65 override fun dump(pw: PrintWriter, args: Array<out String>) { 66 pw.asIndenting().run { 67 printCollection("allDependencies", allDependencies) 68 printCollection("unmetDependencies", unmetDependencies) 69 } 70 } 71 72 /** A dependency where enabling the `alpha` feature depends on enabling the `beta` feature */ 73 class Dependency( 74 private val alphaName: String, 75 private val alphaEnabled: Boolean, 76 private val betaName: String, 77 private val betaEnabled: Boolean 78 ) { 79 val isMet = !alphaEnabled || betaEnabled toStringnull80 override fun toString(): String { 81 val prefix = 82 when { 83 !isMet -> " [NOT MET]" 84 alphaEnabled -> " [met]" 85 betaEnabled -> " [ready]" 86 else -> "[not ready]" 87 } 88 val alphaState = if (alphaEnabled) "enabled" else "disabled" 89 val betaState = if (betaEnabled) "enabled" else "disabled" 90 return "$prefix $alphaName ($alphaState) DEPENDS ON $betaName ($betaState)" 91 } 92 /** Used whe posting a notification of unmet dependencies */ shortUnmetStringnull93 fun shortUnmetString(): String = "$alphaName DEPENDS ON $betaName" 94 } 95 96 protected infix fun UnreleasedFlag.dependsOn(other: UnreleasedFlag) = 97 addDependency(this.token, other.token) 98 protected infix fun ReleasedFlag.dependsOn(other: UnreleasedFlag) = 99 addDependency(this.token, other.token) 100 protected infix fun ReleasedFlag.dependsOn(other: ReleasedFlag) = 101 addDependency(this.token, other.token) 102 protected infix fun FlagToken.dependsOn(other: UnreleasedFlag) = 103 addDependency(this, other.token) 104 protected infix fun FlagToken.dependsOn(other: ReleasedFlag) = addDependency(this, other.token) 105 protected infix fun FlagToken.dependsOn(other: FlagToken) = addDependency(this, other) 106 107 private val UnreleasedFlag.token 108 get() = FlagToken("classic.$name", featureFlags.isEnabled(this)) 109 private val ReleasedFlag.token 110 get() = FlagToken("classic.$name", featureFlags.isEnabled(this)) 111 112 /** Add a dependency to the working list */ 113 private fun addDependency(first: FlagToken, second: FlagToken) { 114 if (!handler.enableDependencies) return 115 workingDependencies.add( 116 Dependency(first.name, first.isEnabled, second.name, second.isEnabled) 117 ) 118 } 119 120 /** An interface which handles dependency collection. */ 121 interface Handler { 122 /** 123 * Should FlagDependencies do anything? 124 * 125 * @return false for user builds so that we skip this overhead. 126 */ 127 val enableDependencies: Boolean 128 get() = Compile.IS_DEBUG 129 /** Handle the complete list of dependencies. */ onCollectednull130 fun onCollected(all: List<Dependency>) {} 131 /** Handle a bad flag configuration. */ warnAboutBadFlagConfigurationnull132 fun warnAboutBadFlagConfiguration(all: List<Dependency>, unmet: List<Dependency>) 133 } 134 } 135 136 /** 137 * A flag dependencies handler which posts a notification and logs to logcat that the configuration 138 * is invalid. 139 */ 140 @SysUISingleton 141 class FlagDependenciesNotifier 142 @Inject 143 constructor( 144 private val context: Context, 145 private val notifManager: NotificationManager, 146 ) : FlagDependenciesBase.Handler { 147 override fun warnAboutBadFlagConfiguration( 148 all: List<FlagDependenciesBase.Dependency>, 149 unmet: List<FlagDependenciesBase.Dependency> 150 ) { 151 val title = "Invalid flag dependencies: ${unmet.size}" 152 val details = unmet.joinToString("\n") { it.shortUnmetString() } 153 Log.e("FlagDependencies", "$title:\n$details") 154 val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, IMPORTANCE_DEFAULT) 155 val notification = 156 Notification.Builder(context, channel.id) 157 .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb) 158 .setContentTitle(title) 159 .setContentText(details) 160 .setStyle(Notification.BigTextStyle().bigText(details)) 161 .setVisibility(Notification.VISIBILITY_PUBLIC) 162 .build() 163 notifManager.createNotificationChannel(channel) 164 notifManager.notify(NOTIF_TAG, NOTIF_ID, notification) 165 } 166 167 override fun onCollected(all: List<FlagDependenciesBase.Dependency>) { 168 notifManager.cancel(NOTIF_TAG, NOTIF_ID) 169 } 170 171 companion object { 172 private const val CHANNEL_ID = "FLAGS" 173 private const val CHANNEL_NAME = "Flags" 174 private const val NOTIF_TAG = "FlagDependenciesNotifier" 175 private const val NOTIF_ID = 0 176 } 177 } 178 179 @Module 180 abstract class FlagDependenciesModule { 181 182 /** Inject into FlagDependencies. */ 183 @Binds 184 @IntoMap 185 @ClassKey(FlagDependencies::class) bindFlagDependenciesnull186 abstract fun bindFlagDependencies(sysui: FlagDependencies): CoreStartable 187 188 /** Bind the flag dependencies handler */ 189 @Binds 190 abstract fun bindFlagDependenciesHandler( 191 handler: FlagDependenciesNotifier 192 ): FlagDependenciesBase.Handler 193 } 194