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.util.Log
20 import com.android.systemui.Dependency
21 
22 /**
23  * This class promotes best practices for flag guarding System UI view refactors.
24  * * [isEnabled] allows changing an implementation.
25  * * [assertDisabled] allows authors to flag code as being "dead" when the flag gets enabled and
26  *   ensure that it is not being invoked accidentally in the post-flag refactor.
27  * * [expectEnabled] allows authors to guard new code with a "safe" alternative when invoked on
28  *   flag-disabled builds, but with a check that should crash eng builds or tests when the
29  *   expectation is violated.
30  *
31  * The constructors prefer that you provide a [FeatureFlags] instance, but does not require it,
32  * falling back to [Dependency.get]. This fallback should ONLY be used to flag-guard code changes
33  * inside views where injecting flag values after initialization can be error-prone.
34  */
35 class ViewRefactorFlag
36 private constructor(
37     private val injectedFlags: FeatureFlags?,
38     private val flag: BooleanFlag,
39     private val readFlagValue: (FeatureFlags) -> Boolean
40 ) {
41     @JvmOverloads
42     constructor(
43         flags: FeatureFlags? = null,
44         flag: UnreleasedFlag
45     ) : this(flags, flag, { it.isEnabled(flag) })
46 
47     @JvmOverloads
48     constructor(
49         flags: FeatureFlags? = null,
50         flag: ReleasedFlag
51     ) : this(flags, flag, { it.isEnabled(flag) })
52 
53     /** Whether the flag is enabled. Called to switch between an old behavior and a new behavior. */
54     val isEnabled by lazy {
55         @Suppress("DEPRECATION")
56         val featureFlags = injectedFlags ?: Dependency.get(FeatureFlags::class.java)
57         readFlagValue(featureFlags)
58     }
59 
60     /**
61      * Called to ensure code is only run when the flag is disabled. This will throw an exception if
62      * the flag is enabled to ensure that the refactor author catches issues in testing.
63      *
64      * Example usage:
65      * ```
66      * public void setController(NotificationShelfController notificationShelfController) {
67      *     mShelfRefactor.assertDisabled();
68      *     mController = notificationShelfController;
69      * }
70      * ````
71      */
72     fun assertDisabled() = check(!isEnabled) { "Code path not supported when $flag is enabled." }
73 
74     /**
75      * Called to ensure code is only run when the flag is enabled. This protects users from the
76      * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
77      * build to ensure that the refactor author catches issues in testing.
78      *
79      * Example usage:
80      * ```
81      * public void setShelfIcons(NotificationIconContainer icons) {
82      *     if (mShelfRefactor.expectEnabled()) {
83      *         mShelfIcons = icons;
84      *     }
85      * }
86      * ```
87      */
88     fun expectEnabled(): Boolean {
89         if (!isEnabled) {
90             val message = "Code path not supported when $flag is disabled."
91             Log.wtf(TAG, message, Exception(message))
92         }
93         return isEnabled
94     }
95 
96     private companion object {
97         private const val TAG = "ViewRefactorFlag"
98     }
99 }
100