diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java new file mode 100644 index 0000000000000..f724e5537ef00 --- /dev/null +++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java @@ -0,0 +1,183 @@ +/** + * 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.internal.config.sysui; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Build; +import android.os.SystemProperties; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Provides a central definition of debug SystemUI's SystemProperties flags, and their defaults. + * + * The main feature of this class is that it encodes a system-wide default for each flag which can + * be updated by engineers with a single-line CL. + * + * NOTE: Because flag values returned by this class are not cached, it is important that developers + * understand the intricacies of changing values and how that applies to their own code. + * Generally, the best practice is to set the property, and then restart the device so that any + * processes with stale state can be updated. However, if your code has no state derived from the + * flag value and queries it any time behavior is relevant, then it may be safe to change the flag + * and not immediately reboot. + * + * To enable flags in debuggable builds, use the following commands: + * + * $ adb shell setprop persist.sysui.whatever_the_flag true + * $ adb reboot + * + * @hide + */ +public class SystemUiSystemPropertiesFlags { + + /** The interface used for resolving SystemUI SystemProperties Flags to booleans. */ + public interface FlagResolver { + /** Is the flag enabled? */ + boolean isEnabled(Flag flag); + } + + /** The primary, immutable resolver returned by getResolver() */ + private static final FlagResolver + MAIN_RESOLVER = + Build.IS_DEBUGGABLE ? new DebugResolver() : new ProdResolver(); + + /** + * On debuggable builds, this can be set to override the resolver returned by getResolver(). + * This can be useful to override flags when testing components that do not allow injecting the + * SystemUiPropertiesFlags resolver they use. + * Always set this to null when tests tear down. + */ + @VisibleForTesting + public static FlagResolver TEST_RESOLVER = null; + + /** Get the resolver for this device configuration. */ + public static FlagResolver getResolver() { + if (Build.IS_DEBUGGABLE && TEST_RESOLVER != null) { + Log.i("SystemUiSystemPropertiesFlags", "Returning debug resolver " + TEST_RESOLVER); + return TEST_RESOLVER; + } + return MAIN_RESOLVER; + } + + /** The teamfood flag allows multiple features to be opted into at once. */ + public static final Flag TEAMFOOD = devFlag("persist.sysui.teamfood"); + + /** + * Flags related to notification features + */ + public static final class NotificationFlags { + + /** + * FOR DEVELOPMENT / TESTING ONLY!!! + * Forcibly demote *ALL* FSI notifications as if no apps have the app op permission. + */ + public static final Flag FSI_FORCE_DEMOTE = + devFlag("persist.sysui.notification.fsi_force_demote"); + + /** Gating the ability for users to dismiss ongoing event notifications */ + public static final Flag ALLOW_DISMISS_ONGOING = + devFlag("persist.sysui.notification.ongoing_dismissal"); + + /** Gating the redaction of OTP notifications on the lockscreen */ + public static final Flag OTP_REDACTION = + devFlag("persist.sysui.notification.otp_redaction"); + + } + + //// == Everything below this line is the implementation == //// + + /** + * Creates a flag that is enabled by default in debuggable builds. + * It can be enabled by setting this flag's SystemProperty to 1. + * + * This flag is ALWAYS disabled in release builds. + */ + @VisibleForTesting + public static Flag devFlag(String name) { + return new Flag(name, false, null); + } + + /** + * Creates a flag that is disabled by default in debuggable builds. + * It can be enabled or force-disabled by setting this flag's SystemProperty to 1 or 0. + * If this flag's SystemProperty is not set, the flag can be enabled by setting the + * TEAMFOOD flag's SystemProperty to 1. + * + * This flag is ALWAYS disabled in release builds. + */ + @VisibleForTesting + public static Flag teamfoodFlag(String name) { + return new Flag(name, false, TEAMFOOD); + } + + /** + * Creates a flag that is enabled by default in debuggable builds. + * It can be enabled by setting this flag's SystemProperty to 0. + * + * This flag is ALWAYS enabled in release builds. + */ + @VisibleForTesting + public static Flag releasedFlag(String name) { + return new Flag(name, true, null); + } + + /** Represents a developer-switchable gate for a feature. */ + public static final class Flag { + public final String mSysPropKey; + public final boolean mDefaultValue; + @Nullable + public final Flag mDebugDefault; + + /** constructs a new flag. only visible for testing the class */ + @VisibleForTesting + public Flag(@NonNull String sysPropKey, boolean defaultValue, @Nullable Flag debugDefault) { + mSysPropKey = sysPropKey; + mDefaultValue = defaultValue; + mDebugDefault = debugDefault; + } + } + + /** Implementation of the interface used in release builds. */ + @VisibleForTesting + public static final class ProdResolver implements + FlagResolver { + @Override + public boolean isEnabled(Flag flag) { + return flag.mDefaultValue; + } + } + + /** Implementation of the interface used in debuggable builds. */ + @VisibleForTesting + public static class DebugResolver implements FlagResolver { + @Override + public final boolean isEnabled(Flag flag) { + if (flag.mDebugDefault == null) { + return getBoolean(flag.mSysPropKey, flag.mDefaultValue); + } + return getBoolean(flag.mSysPropKey, isEnabled(flag.mDebugDefault)); + } + + /** Look up the value; overridable for tests to avoid needing to set SystemProperties */ + @VisibleForTesting + public boolean getBoolean(String key, boolean defaultValue) { + return SystemProperties.getBoolean(key, defaultValue); + } + } +} diff --git a/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS b/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS new file mode 100644 index 0000000000000..2e96c97c8bb35 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/config/sysui/OWNERS @@ -0,0 +1 @@ +include /packages/SystemUI/OWNERS diff --git a/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java b/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java new file mode 100644 index 0000000000000..6b9d39ceb79a8 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/config/sysui/SystemUiSystemPropertiesFlagsTest.java @@ -0,0 +1,110 @@ +/* + * 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.internal.config.sysui; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.filters.SmallTest; + +import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.Flag; +import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.FlagResolver; + +import junit.framework.TestCase; + +import java.util.HashMap; +import java.util.Map; + +@SmallTest +public class SystemUiSystemPropertiesFlagsTest extends TestCase { + + public class TestableDebugResolver extends SystemUiSystemPropertiesFlags.DebugResolver { + final Map mTestData = new HashMap<>(); + + @Override + public boolean getBoolean(String key, boolean defaultValue) { + Boolean testValue = mTestData.get(key); + return testValue == null ? defaultValue : testValue; + } + + public void set(Flag flag, Boolean value) { + mTestData.put(flag.mSysPropKey, value); + } + } + + private FlagResolver mProdResolver; + private TestableDebugResolver mDebugResolver; + + private Flag mReleasedFlag; + private Flag mTeamfoodFlag; + private Flag mDevFlag; + + public void setUp() { + mProdResolver = new SystemUiSystemPropertiesFlags.ProdResolver(); + mDebugResolver = new TestableDebugResolver(); + mReleasedFlag = SystemUiSystemPropertiesFlags.releasedFlag("mReleasedFlag"); + mTeamfoodFlag = SystemUiSystemPropertiesFlags.teamfoodFlag("mTeamfoodFlag"); + mDevFlag = SystemUiSystemPropertiesFlags.devFlag("mDevFlag"); + } + + public void tearDown() { + SystemUiSystemPropertiesFlags.TEST_RESOLVER = null; + } + + public void testProdResolverReturnsDefault() { + assertThat(mProdResolver.isEnabled(mReleasedFlag)).isTrue(); + assertThat(mProdResolver.isEnabled(mTeamfoodFlag)).isFalse(); + assertThat(mProdResolver.isEnabled(mDevFlag)).isFalse(); + } + + public void testDebugResolverAndReleasedFlag() { + assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isTrue(); + + mDebugResolver.set(mReleasedFlag, false); + assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isFalse(); + + mDebugResolver.set(mReleasedFlag, true); + assertThat(mDebugResolver.isEnabled(mReleasedFlag)).isTrue(); + } + + private void assertTeamfoodFlag(Boolean flagValue, Boolean teamfood, boolean expected) { + mDebugResolver.set(mTeamfoodFlag, flagValue); + mDebugResolver.set(SystemUiSystemPropertiesFlags.TEAMFOOD, teamfood); + assertThat(mDebugResolver.isEnabled(mTeamfoodFlag)).isEqualTo(expected); + } + + public void testDebugResolverAndTeamfoodFlag() { + assertTeamfoodFlag(null, null, false); + assertTeamfoodFlag(true, null, true); + assertTeamfoodFlag(false, null, false); + assertTeamfoodFlag(null, true, true); + assertTeamfoodFlag(true, true, true); + assertTeamfoodFlag(false, true, false); + assertTeamfoodFlag(null, false, false); + assertTeamfoodFlag(true, false, true); + assertTeamfoodFlag(false, false, false); + } + + public void testDebugResolverAndDevFlag() { + assertThat(mDebugResolver.isEnabled(mDevFlag)).isFalse(); + + mDebugResolver.set(mDevFlag, true); + assertThat(mDebugResolver.isEnabled(mDevFlag)).isTrue(); + + mDebugResolver.set(mDevFlag, false); + assertThat(mDebugResolver.isEnabled(mDevFlag)).isFalse(); + } +}