From 27f4ca52a94e501715953a8dc3b004571b1e5e32 Mon Sep 17 00:00:00 2001 From: Dave Mankoff Date: Fri, 17 Mar 2023 20:08:45 +0000 Subject: [PATCH] A new Client-Side FeatureFlags library. This is just a stub at the moment. Follow-on cls contain more implementation. This cl describes the basic shape for usage: BooleanFlag FOOBAR = FeatureFlags.booleanFlag("foo", "bar", false); FeatureFlags.getInstance().isEnabled(FOOBAR); Defined are 4 flags types: - BooleanFlag: can be either true or false. Held constant until device restart. - DynamicBooleanFlag: can be either true or false. Can change while a process is running. - FusedOnFlag: Always true. - FusedOffFlag: Always false. More flag types are intended (Resource & SystemProperty backed, Strings, Floats, Ints, etc). but this cl represents the MVP. More about the details of this client can be read at: http://go/android-flagging-dd Test: m Android-Flags Bug: 279054964 Change-Id: I0e455bf69f01dc675ec8bf64e584e2beff4e72ce --- core/java/android/flags/BooleanFlag.java | 67 +++++++++ .../android/flags/DynamicBooleanFlag.java | 62 +++++++++ core/java/android/flags/DynamicFlag.java | 27 ++++ core/java/android/flags/FeatureFlags.java | 131 ++++++++++++++++++ core/java/android/flags/FeatureFlagsFake.java | 49 +++++++ core/java/android/flags/Flag.java | 40 ++++++ core/java/android/flags/FusedOffFlag.java | 60 ++++++++ core/java/android/flags/FusedOnFlag.java | 60 ++++++++ core/java/android/flags/OWNERS | 7 + .../src/android/flags/FeatureFlagsTest.java | 54 ++++++++ 10 files changed, 557 insertions(+) create mode 100644 core/java/android/flags/BooleanFlag.java create mode 100644 core/java/android/flags/DynamicBooleanFlag.java create mode 100644 core/java/android/flags/DynamicFlag.java create mode 100644 core/java/android/flags/FeatureFlags.java create mode 100644 core/java/android/flags/FeatureFlagsFake.java create mode 100644 core/java/android/flags/Flag.java create mode 100644 core/java/android/flags/FusedOffFlag.java create mode 100644 core/java/android/flags/FusedOnFlag.java create mode 100644 core/java/android/flags/OWNERS create mode 100644 core/tests/coretests/src/android/flags/FeatureFlagsTest.java diff --git a/core/java/android/flags/BooleanFlag.java b/core/java/android/flags/BooleanFlag.java new file mode 100644 index 0000000000000..5519961a917fa --- /dev/null +++ b/core/java/android/flags/BooleanFlag.java @@ -0,0 +1,67 @@ +/* + * 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 android.flags; + +import android.annotation.NonNull; + +/** + * A flag representing a true or false value. + * + * The value will always be the same during the lifetime of the process it is read in. + * + * @hide + */ +public class BooleanFlag implements Flag { + private final String mNamespace; + private final String mName; + private final boolean mDefault; + + /** + * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}. + * @param name A name for this flag. + * @param defaultValue The value of this flag if no other override is present. + */ + BooleanFlag(String namespace, String name, boolean defaultValue) { + mNamespace = namespace; + mName = name; + mDefault = defaultValue; + } + + @Override + @NonNull + public Boolean getDefault() { + return mDefault; + } + + @Override + @NonNull + public String getNamespace() { + return mNamespace; + } + + @Override + @NonNull + public String getName() { + return mName; + } + + @Override + @NonNull + public String toString() { + return getNamespace() + "." + getName() + "[" + getDefault() + "]"; + } +} diff --git a/core/java/android/flags/DynamicBooleanFlag.java b/core/java/android/flags/DynamicBooleanFlag.java new file mode 100644 index 0000000000000..d76fd9ec9a3c6 --- /dev/null +++ b/core/java/android/flags/DynamicBooleanFlag.java @@ -0,0 +1,62 @@ +/* + * 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 android.flags; + +/** + * A flag representing a true or false value. + * + * The value may be different from one read to the next. + * + * @hide + */ +public class DynamicBooleanFlag implements DynamicFlag { + + private final String mNamespace; + private final String mName; + private final boolean mDefault; + + /** + * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}. + * @param name A name for this flag. + * @param defaultValue The value of this flag if no other override is present. + */ + DynamicBooleanFlag(String namespace, String name, boolean defaultValue) { + mNamespace = namespace; + mName = name; + mDefault = defaultValue; + } + + @Override + public String getNamespace() { + return mNamespace; + } + + @Override + public String getName() { + return mName; + } + + @Override + public Boolean getDefault() { + return mDefault; + } + + @Override + public String toString() { + return getNamespace() + "." + getName() + "[" + getDefault() + "]"; + } +} diff --git a/core/java/android/flags/DynamicFlag.java b/core/java/android/flags/DynamicFlag.java new file mode 100644 index 0000000000000..8583ea6034d89 --- /dev/null +++ b/core/java/android/flags/DynamicFlag.java @@ -0,0 +1,27 @@ +/* + * 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 android.flags; + +/** + * A flag for which the value may be different from one read to the next. + * + * @param The type of value that this flag stores. E.g. Boolean or String. + * + * @hide + */ +public interface DynamicFlag extends Flag { +} diff --git a/core/java/android/flags/FeatureFlags.java b/core/java/android/flags/FeatureFlags.java new file mode 100644 index 0000000000000..c7d9d57e68b59 --- /dev/null +++ b/core/java/android/flags/FeatureFlags.java @@ -0,0 +1,131 @@ +/* + * 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 android.flags; + +import android.annotation.NonNull; + +import java.util.HashSet; +import java.util.Set; + +/** + * A class for querying constants from the system - primarily booleans. + * + * Clients using this class can define their flags and their default values in one place, + * can override those values on running devices for debugging and testing purposes, and can control + * what flags are available to be used on release builds. + * + * TODO(b/279054964): A lot. This is skeleton code right now. + * @hide + */ +public class FeatureFlags { + private static FeatureFlags sInstance; + private static final Object sInstanceLock = new Object(); + + private final Set mListeners = new HashSet<>(); + + /** + * Obtain a per-process instance of FeatureFlags. + * @return + */ + @NonNull + public static FeatureFlags getInstance() { + synchronized (sInstanceLock) { + if (sInstance == null) { + sInstance = new FeatureFlags(); + } + } + + return sInstance; + } + + FeatureFlags() { + } + + /** + * Returns whether the supplied flag is true or not. + * + * {@link BooleanFlag} should only be used in debug builds. They do not get optimized out. + * + * The first time a flag is read, its value is cached for the lifetime of the process. + */ + public boolean isEnabled(@NonNull BooleanFlag flag) { + return flag.getDefault(); + } + + /** + * Returns whether the supplied flag is true or not. + * + * Always returns false. + */ + public boolean isEnabled(@NonNull FusedOffFlag flag) { + return false; + } + + /** + * Returns whether the supplied flag is true or not. + * + * Always returns true; + */ + public boolean isEnabled(@NonNull FusedOnFlag flag) { + return true; + } + + /** + * Returns whether the supplied flag is true or not. + * + * Can return a different value for the flag each time it is called if an override comes in. + */ + public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) { + return flag.getDefault(); + } + + /** + * Add a listener to be alerted when a {@link DynamicFlag} changes. + * + * See also {@link #removeChangeListener(ChangeListener)}. + * + * @param listener The listener to add. + */ + public void addChangeListener(@NonNull ChangeListener listener) { + mListeners.add(listener); + } + + /** + * Remove a listener that was added earlier. + * + * See also {@link #addChangeListener(ChangeListener)}. + * + * @param listener The listener to remove. + */ + public void removeChangeListener(@NonNull ChangeListener listener) { + mListeners.remove(listener); + } + + /** + * A simpler listener that is alerted when a {@link DynamicFlag} changes. + * + * See {@link #addChangeListener(ChangeListener)} + */ + public interface ChangeListener { + /** + * Called when a {@link DynamicFlag} changes. + * + * @param flag The flag that has changed. + */ + void onFlagChanged(DynamicFlag flag); + } +} diff --git a/core/java/android/flags/FeatureFlagsFake.java b/core/java/android/flags/FeatureFlagsFake.java new file mode 100644 index 0000000000000..03bf2b6d33d26 --- /dev/null +++ b/core/java/android/flags/FeatureFlagsFake.java @@ -0,0 +1,49 @@ +/* + * 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 android.flags; + +import android.annotation.NonNull; + +/** + * An implementation of {@link FeatureFlags} for testing. + * @hide + */ +public class FeatureFlagsFake extends FeatureFlags { + public FeatureFlagsFake() { + super(); + } + + @Override + public boolean isEnabled(@NonNull BooleanFlag flag) { + return flag.getDefault(); + } + + @Override + public boolean isEnabled(@NonNull FusedOffFlag flag) { + return false; + } + + @Override + public boolean isEnabled(@NonNull FusedOnFlag flag) { + return true; + } + + @Override + public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) { + return flag.getDefault(); + } +} diff --git a/core/java/android/flags/Flag.java b/core/java/android/flags/Flag.java new file mode 100644 index 0000000000000..ae0a6ba33f44b --- /dev/null +++ b/core/java/android/flags/Flag.java @@ -0,0 +1,40 @@ +/* + * 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 android.flags; + +import android.annotation.NonNull; + +/** + * Base class for constants read via {@link android.flags.FeatureFlags}. + * + * @param The type of value that this flag stores. E.g. Boolean or String. + * + * @hide + */ +public interface Flag { + /** The namespace for a flag. Should combine uniquely with its name. */ + @NonNull + String getNamespace(); + + /** The name of the flag. Should combine uniquely with its namespace. */ + @NonNull + String getName(); + + /** The value of this flag if no override has been set. Null values are not supported. */ + @NonNull + T getDefault(); +} diff --git a/core/java/android/flags/FusedOffFlag.java b/core/java/android/flags/FusedOffFlag.java new file mode 100644 index 0000000000000..999d0f9817801 --- /dev/null +++ b/core/java/android/flags/FusedOffFlag.java @@ -0,0 +1,60 @@ +/* + * 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 android.flags; + +import android.annotation.NonNull; + +/** + * A flag representing a false value. + * + * The flag can never be changed or overridden. It is false at compile time. + * + * @hide + */ +public final class FusedOffFlag implements Flag { + private final String mNamespace; + private final String mName; + + FusedOffFlag(String namespace, String name) { + mNamespace = namespace; + mName = name; + } + + @Override + @NonNull + public Boolean getDefault() { + return false; + } + + @Override + @NonNull + public String getNamespace() { + return mNamespace; + } + + @Override + @NonNull + public String getName() { + return mName; + } + + @Override + @NonNull + public String toString() { + return getNamespace() + "." + getName() + "[false]"; + } +} diff --git a/core/java/android/flags/FusedOnFlag.java b/core/java/android/flags/FusedOnFlag.java new file mode 100644 index 0000000000000..4f33bee8abe12 --- /dev/null +++ b/core/java/android/flags/FusedOnFlag.java @@ -0,0 +1,60 @@ +/* + * 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 android.flags; + +import android.annotation.NonNull; + +/** + * A flag representing a true value. + * + * The flag can never be changed or overridden. It is true at compile time. + * + * @hide + */ +public final class FusedOnFlag implements Flag { + private final String mNamespace; + private final String mName; + + FusedOnFlag(String namespace, String name) { + mNamespace = namespace; + mName = name; + } + + @Override + @NonNull + public Boolean getDefault() { + return true; + } + + @Override + @NonNull + public String getNamespace() { + return mNamespace; + } + + @Override + @NonNull + public String getName() { + return mName; + } + + @Override + @NonNull + public String toString() { + return getNamespace() + "." + getName() + "[true]"; + } +} diff --git a/core/java/android/flags/OWNERS b/core/java/android/flags/OWNERS new file mode 100644 index 0000000000000..fa125c4a159cc --- /dev/null +++ b/core/java/android/flags/OWNERS @@ -0,0 +1,7 @@ +# Bug component: 1306523 + +mankoff@google.com +pixel@google.com + +dsandler@android.com + diff --git a/core/tests/coretests/src/android/flags/FeatureFlagsTest.java b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java new file mode 100644 index 0000000000000..dc6006a8e5fa4 --- /dev/null +++ b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2020 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 android.flags; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.MockitoAnnotations; + +@SmallTest +@Presubmit +public final class FeatureFlagsTest { + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @After + public void tearDown() { + } + + @Test + public void testPass() { + assertThat(true).isTrue(); + } + + @Ignore + @Test + public void testFail() { + assertThat(false).isTrue(); + } +}