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
This commit is contained in:
Dave Mankoff
2023-03-17 20:08:45 +00:00
parent d756cad817
commit 27f4ca52a9
10 changed files with 557 additions and 0 deletions

View File

@@ -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<Boolean> {
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() + "]";
}
}

View File

@@ -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<Boolean> {
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() + "]";
}
}

View File

@@ -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 <T> The type of value that this flag stores. E.g. Boolean or String.
*
* @hide
*/
public interface DynamicFlag<T> extends Flag<T> {
}

View File

@@ -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<ChangeListener> 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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 <T> The type of value that this flag stores. E.g. Boolean or String.
*
* @hide
*/
public interface Flag<T> {
/** 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();
}

View File

@@ -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<Boolean> {
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]";
}
}

View File

@@ -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<Boolean> {
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]";
}
}

View File

@@ -0,0 +1,7 @@
# Bug component: 1306523
mankoff@google.com
pixel@google.com
dsandler@android.com

View File

@@ -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();
}
}