Merge changes from topic "revert-23727772-b279054964-flag-api-udc-qpr-dev-YOBAYFNKLI" into udc-qpr-dev

* changes:
  Revert "A new Client-Side FeatureFlags library."
  Revert "Define FeatureFlagsService."
  Revert "Connect FeatureFlags with FeatureFlagsService."
  Revert "Add Local FeatureFlags client to system servier."
  Revert "Add permissions to FeatureFlagService and override api."
  Revert "Allow flags to add metadata directly in code."
This commit is contained in:
Dave Mankoff
2023-07-12 19:26:45 +00:00
committed by Android (Google) Code Review
37 changed files with 0 additions and 3518 deletions

View File

@@ -33,15 +33,6 @@ filegroup {
srcs: ["com/android/internal/os/IBinaryTransparencyService.aidl"],
}
filegroup {
name: "feature_flags_aidl",
srcs: [
"android/flags/IFeatureFlags.aidl",
"android/flags/IFeatureFlagsCallback.aidl",
"android/flags/SyncableFlag.aidl",
],
}
filegroup {
name: "ITracingServiceProxy.aidl",
srcs: ["android/tracing/ITracingServiceProxy.aidl"],

View File

@@ -5297,13 +5297,6 @@ public abstract class Context {
@SystemApi
public static final String APP_PREDICTION_SERVICE = "app_prediction";
/**
* Used for reading system-wide, overridable flags.
*
* @hide
*/
public static final String FEATURE_FLAGS_SERVICE = "feature_flags";
/**
* Official published name of the search ui service.
*

View File

@@ -1,52 +0,0 @@
/*
* 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 extends BooleanFlagBase {
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) {
super(namespace, name);
mDefault = defaultValue;
}
@Override
@NonNull
public Boolean getDefault() {
return mDefault;
}
@Override
public BooleanFlag defineMetaData(String label, String description, String categoryName) {
super.defineMetaData(label, description, categoryName);
return this;
}
}

View File

@@ -1,82 +0,0 @@
/*
* 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;
abstract class BooleanFlagBase implements Flag<Boolean> {
private final String mNamespace;
private final String mName;
private String mLabel;
private String mDescription;
private String mCategoryName;
/**
* @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
* @param name A name for this flag.
*/
BooleanFlagBase(String namespace, String name) {
mNamespace = namespace;
mName = name;
mLabel = name;
}
public abstract Boolean getDefault();
@Override
@NonNull
public String getNamespace() {
return mNamespace;
}
@Override
@NonNull
public String getName() {
return mName;
}
@Override
public BooleanFlagBase defineMetaData(String label, String description, String categoryName) {
mLabel = label;
mDescription = description;
mCategoryName = categoryName;
return this;
}
@Override
@NonNull
public String getLabel() {
return mLabel;
}
@Override
public String getDescription() {
return mDescription;
}
@Override
public String getCategoryName() {
return mCategoryName;
}
@Override
@NonNull
public String toString() {
return getNamespace() + "." + getName() + "[" + getDefault() + "]";
}
}

View File

@@ -1,50 +0,0 @@
/*
* 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 extends BooleanFlagBase implements DynamicFlag<Boolean> {
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) {
super(namespace, name);
mDefault = defaultValue;
}
@Override
public Boolean getDefault() {
return mDefault;
}
@Override
public DynamicBooleanFlag defineMetaData(String label, String description, String categoryName) {
super.defineMetaData(label, description, categoryName);
return this;
}
}

View File

@@ -1,31 +0,0 @@
/*
* 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> {
@Override
default boolean isDynamic() {
return true;
}
}

View File

@@ -1,379 +0,0 @@
/*
* 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 android.content.Context;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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 final String TAG = "FeatureFlags";
private static FeatureFlags sInstance;
private static final Object sInstanceLock = new Object();
private final Set<Flag<?>> mKnownFlags = new ArraySet<>();
private final Set<Flag<?>> mDirtyFlags = new ArraySet<>();
private IFeatureFlags mIFeatureFlags;
private final Map<String, Map<String, Boolean>> mBooleanOverrides = new HashMap<>();
private final Set<ChangeListener> mListeners = new HashSet<>();
/**
* Obtain a per-process instance of FeatureFlags.
* @return A singleton instance of {@link FeatureFlags}.
*/
@NonNull
public static FeatureFlags getInstance() {
synchronized (sInstanceLock) {
if (sInstance == null) {
sInstance = new FeatureFlags();
}
}
return sInstance;
}
/** See {@link FeatureFlagsFake}. */
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public static void setInstance(FeatureFlags instance) {
synchronized (sInstanceLock) {
sInstance = instance;
}
}
private final IFeatureFlagsCallback mIFeatureFlagsCallback = new IFeatureFlagsCallback.Stub() {
@Override
public void onFlagChange(SyncableFlag flag) {
for (Flag<?> f : mKnownFlags) {
if (flagEqualsSyncableFlag(f, flag)) {
if (f instanceof DynamicFlag<?>) {
if (f instanceof DynamicBooleanFlag) {
String value = flag.getValue();
if (value == null) { // Null means any existing overrides were erased.
value = ((DynamicBooleanFlag) f).getDefault().toString();
}
addBooleanOverride(flag.getNamespace(), flag.getName(), value);
}
FeatureFlags.this.onFlagChange((DynamicFlag<?>) f);
}
break;
}
}
}
};
private FeatureFlags() {
this(null);
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public FeatureFlags(IFeatureFlags iFeatureFlags) {
mIFeatureFlags = iFeatureFlags;
if (mIFeatureFlags != null) {
try {
mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
} catch (RemoteException e) {
// Shouldn't happen with things passed into tests.
Log.e(TAG, "Could not register callbacks!", e);
}
}
}
/**
* Construct a new {@link BooleanFlag}.
*
* Use this instead of constructing a {@link BooleanFlag} directly, as it registers the flag
* with the internals of the flagging system.
*/
@NonNull
public static BooleanFlag booleanFlag(
@NonNull String namespace, @NonNull String name, boolean def) {
return getInstance().addFlag(new BooleanFlag(namespace, name, def));
}
/**
* Construct a new {@link FusedOffFlag}.
*
* Use this instead of constructing a {@link FusedOffFlag} directly, as it registers the
* flag with the internals of the flagging system.
*/
@NonNull
public static FusedOffFlag fusedOffFlag(@NonNull String namespace, @NonNull String name) {
return getInstance().addFlag(new FusedOffFlag(namespace, name));
}
/**
* Construct a new {@link FusedOnFlag}.
*
* Use this instead of constructing a {@link FusedOnFlag} directly, as it registers the flag
* with the internals of the flagging system.
*/
@NonNull
public static FusedOnFlag fusedOnFlag(@NonNull String namespace, @NonNull String name) {
return getInstance().addFlag(new FusedOnFlag(namespace, name));
}
/**
* Construct a new {@link DynamicBooleanFlag}.
*
* Use this instead of constructing a {@link DynamicBooleanFlag} directly, as it registers
* the flag with the internals of the flagging system.
*/
@NonNull
public static DynamicBooleanFlag dynamicBooleanFlag(
@NonNull String namespace, @NonNull String name, boolean def) {
return getInstance().addFlag(new DynamicBooleanFlag(namespace, name, def));
}
/**
* 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);
}
protected void onFlagChange(@NonNull DynamicFlag<?> flag) {
for (ChangeListener l : mListeners) {
l.onFlagChanged(flag);
}
}
/**
* 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 getBooleanInternal(flag);
}
/**
* 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 getBooleanInternal(flag);
}
private boolean getBooleanInternal(Flag<Boolean> flag) {
sync();
Map<String, Boolean> ns = mBooleanOverrides.get(flag.getNamespace());
Boolean value = null;
if (ns != null) {
value = ns.get(flag.getName());
}
if (value == null) {
throw new IllegalStateException("Boolean flag being read but was not synced: " + flag);
}
return value;
}
private <T extends Flag<?>> T addFlag(T flag) {
synchronized (FeatureFlags.class) {
mDirtyFlags.add(flag);
mKnownFlags.add(flag);
}
return flag;
}
/**
* Sync any known flags that have not yet been synced.
*
* This is called implicitly when any flag is read, and is not generally needed except in
* exceptional circumstances.
*/
public void sync() {
synchronized (FeatureFlags.class) {
if (mDirtyFlags.isEmpty()) {
return;
}
syncInternal(mDirtyFlags);
mDirtyFlags.clear();
}
}
/**
* Called when new flags have been declared. Gives the implementation a chance to act on them.
*
* Guaranteed to be called from a synchronized, thread-safe context.
*/
protected void syncInternal(Set<Flag<?>> dirtyFlags) {
IFeatureFlags iFeatureFlags = bind();
List<SyncableFlag> syncableFlags = new ArrayList<>();
for (Flag<?> f : dirtyFlags) {
syncableFlags.add(flagToSyncableFlag(f));
}
List<SyncableFlag> serverFlags = List.of(); // Need to initialize the list with something.
try {
// New values come back from the service.
serverFlags = iFeatureFlags.syncFlags(syncableFlags);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
for (Flag<?> f : dirtyFlags) {
boolean found = false;
for (SyncableFlag sf : serverFlags) {
if (flagEqualsSyncableFlag(f, sf)) {
if (f instanceof BooleanFlag || f instanceof DynamicBooleanFlag) {
addBooleanOverride(sf.getNamespace(), sf.getName(), sf.getValue());
}
found = true;
break;
}
}
if (!found) {
if (f instanceof BooleanFlag) {
addBooleanOverride(
f.getNamespace(),
f.getName(),
((BooleanFlag) f).getDefault() ? "true" : "false");
}
}
}
}
private void addBooleanOverride(String namespace, String name, String override) {
Map<String, Boolean> nsOverrides = mBooleanOverrides.get(namespace);
if (nsOverrides == null) {
nsOverrides = new HashMap<>();
mBooleanOverrides.put(namespace, nsOverrides);
}
nsOverrides.put(name, parseBoolean(override));
}
private SyncableFlag flagToSyncableFlag(Flag<?> f) {
return new SyncableFlag(
f.getNamespace(),
f.getName(),
f.getDefault().toString(),
f instanceof DynamicFlag<?>);
}
private IFeatureFlags bind() {
if (mIFeatureFlags == null) {
mIFeatureFlags = IFeatureFlags.Stub.asInterface(
ServiceManager.getService(Context.FEATURE_FLAGS_SERVICE));
try {
mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
} catch (RemoteException e) {
Log.e(TAG, "Failed to listen for flag changes!");
}
}
return mIFeatureFlags;
}
static boolean parseBoolean(String value) {
// Check for a truish string.
boolean result = value.equalsIgnoreCase("true")
|| value.equals("1")
|| value.equalsIgnoreCase("t")
|| value.equalsIgnoreCase("on");
if (!result) { // Expect a falsish string, else log an error.
if (!(value.equalsIgnoreCase("false")
|| value.equals("0")
|| value.equalsIgnoreCase("f")
|| value.equalsIgnoreCase("off"))) {
Log.e(TAG,
"Tried parsing " + value + " as boolean but it doesn't look like one. "
+ "Value expected to be one of true|false, 1|0, t|f, on|off.");
}
}
return result;
}
private static boolean flagEqualsSyncableFlag(Flag<?> f, SyncableFlag sf) {
return f.getName().equals(sf.getName()) && f.getNamespace().equals(sf.getNamespace());
}
/**
* 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

@@ -1,115 +0,0 @@
/*
* 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.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* An implementation of {@link FeatureFlags} for testing.
*
* Before you read a flag from using this Fake, you must set that flag using
* {@link #setFlagValue(BooleanFlagBase, boolean)}. This ensures that your tests are deterministic.
*
* If you are relying on {@link FeatureFlags#getInstance()} to access FeatureFlags in your code
* under test, (instead of dependency injection), you can pass an instance of this fake to
* {@link FeatureFlags#setInstance(FeatureFlags)}. Be sure to call that method again, passing null,
* to ensure hermetic testing - you don't want static state persisting between your test methods.
*
* @hide
*/
public class FeatureFlagsFake extends FeatureFlags {
private final Map<BooleanFlagBase, Boolean> mFlagValues = new HashMap<>();
private final Set<BooleanFlagBase> mReadFlags = new HashSet<>();
public FeatureFlagsFake(IFeatureFlags iFeatureFlags) {
super(iFeatureFlags);
}
@Override
public boolean isEnabled(@NonNull BooleanFlag flag) {
return requireFlag(flag);
}
@Override
public boolean isEnabled(@NonNull FusedOffFlag flag) {
return requireFlag(flag);
}
@Override
public boolean isEnabled(@NonNull FusedOnFlag flag) {
return requireFlag(flag);
}
@Override
public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
return requireFlag(flag);
}
@Override
protected void syncInternal(Set<Flag<?>> dirtyFlags) {
}
/**
* Explicitly set a flag's value for reading in tests.
*
* You _must_ call this for every flag your code-under-test will read. Otherwise, an
* {@link IllegalStateException} will be thrown.
*
* You are able to set values for {@link FusedOffFlag} and {@link FusedOnFlag}, despite those
* flags having a fixed value at compile time, since unit tests should still test the state of
* those flags as both true and false. I.e. a flag that is off might be turned on in a future
* build or vice versa.
*
* You can not call this method _after_ a non-dynamic flag has been read. Non-dynamic flags
* are held stable in the system, so changing a value after reading would not match
* real-implementation behavior.
*
* Calling this method will trigger any {@link android.flags.FeatureFlags.ChangeListener}s that
* are registered for the supplied flag if the flag is a {@link DynamicFlag}.
*
* @param flag The BooleanFlag that you want to set a value for.
* @param value The value that the flag should return when accessed.
*/
public void setFlagValue(@NonNull BooleanFlagBase flag, boolean value) {
if (!(flag instanceof DynamicBooleanFlag) && mReadFlags.contains(flag)) {
throw new RuntimeException(
"You can not set the value of a flag after it has been read. Tried to set "
+ flag + " to " + value + " but it already " + mFlagValues.get(flag));
}
mFlagValues.put(flag, value);
if (flag instanceof DynamicBooleanFlag) {
onFlagChange((DynamicFlag<?>) flag);
}
}
private boolean requireFlag(BooleanFlagBase flag) {
if (!mFlagValues.containsKey(flag)) {
throw new IllegalStateException(
"Tried to access " + flag + " in test but no overrided specified. You must "
+ "call #setFlagValue for each flag read in a test.");
}
mReadFlags.add(flag);
return mFlagValues.get(flag);
}
}

View File

@@ -1,82 +0,0 @@
/*
* 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();
/** Returns true if the value of this flag can change at runtime. */
default boolean isDynamic() {
return false;
}
/**
* Add human-readable details to the flag. Flag client's are not required to set this.
*
* See {@link #getLabel()}, {@link #getDescription()}, and {@link #getCategoryName()}.
*
* @return Returns `this`, to make a fluent api.
*/
Flag<T> defineMetaData(String label, String description, String categoryName);
/**
* A human-readable name for the flag. Defaults to {@link #getName()}
*
* See {@link #defineMetaData(String, String, String)}
*/
@NonNull
default String getLabel() {
return getName();
}
/**
* A human-readable description for the flag. Defaults to null if unset.
*
* See {@link #defineMetaData(String, String, String)}
*/
default String getDescription() {
return null;
}
/**
* A human-readable category name for the flag. Defaults to null if unset.
*
* See {@link #defineMetaData(String, String, String)}
*/
default String getCategoryName() {
return null;
}
}

View File

@@ -1,49 +0,0 @@
/*
* 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 android.provider.DeviceConfig;
/**
* 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 extends BooleanFlagBase {
/**
* @param namespace A namespace for this flag. See {@link DeviceConfig}.
* @param name A name for this flag.
*/
FusedOffFlag(String namespace, String name) {
super(namespace, name);
}
@Override
@NonNull
public Boolean getDefault() {
return false;
}
@Override
public FusedOffFlag defineMetaData(String label, String description, String categoryName) {
super.defineMetaData(label, description, categoryName);
return this;
}
}

View File

@@ -1,49 +0,0 @@
/*
* 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 android.provider.DeviceConfig;
/**
* 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 extends BooleanFlagBase {
/**
* @param namespace A namespace for this flag. See {@link DeviceConfig}.
* @param name A name for this flag.
*/
FusedOnFlag(String namespace, String name) {
super(namespace, name);
}
@Override
@NonNull
public Boolean getDefault() {
return true;
}
@Override
public FusedOnFlag defineMetaData(String label, String description, String categoryName) {
super.defineMetaData(label, description, categoryName);
return this;
}
}

View File

@@ -1,89 +0,0 @@
/*
* 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.flags.IFeatureFlagsCallback;
import android.flags.SyncableFlag;
/**
* Binder interface for communicating with {@link com.android.server.flags.FeatureFlagsService}.
*
* This interface is used by {@link android.flags.FeatureFlags} and developers should use that to
* interface with the service. FeatureFlags is the "client" in this documentation.
*
* The methods allow client apps to communicate what flags they care about, and receive back
* current values for those flags. For stable flags, this is the finalized value until the device
* restarts. For {@link DynamicFlag}s, this is the last known value, though it may change in the
* future. Clients can listen for changes to flag values so that it can react accordingly.
* @hide
*/
interface IFeatureFlags {
/**
* Synchronize with the {@link com.android.server.flags.FeatureFlagsService} about flags of
* interest.
*
* The client should pass in a list of flags that it is using as {@link SyncableFlag}s, which
* includes what it thinks the default values of the flags are.
*
* The response will contain a list of matching SyncableFlags, whose values are set to what the
* value of the flags actually are. The client should update its internal state flag data to
* match.
*
* Generally speaking, if a flag that is passed in is new to the FeatureFlagsService, the
* service will cache the passed-in value, and return it back out. If, however, a different
* client has synced that flag with the service previously, FeatureFlagsService will return the
* existing cached value, which may or may not be what the current client passed in. This allows
* FeatureFlagsService to keep clients in agreement with one another.
*/
List<SyncableFlag> syncFlags(in List<SyncableFlag> flagList);
/**
* Pass in an {@link IFeatureFlagsCallback} that will be called whenever a {@link DymamicFlag}
* changes.
*/
void registerCallback(IFeatureFlagsCallback callback);
/**
* Remove a {@link IFeatureFlagsCallback} that was previously registered with
* {@link #registerCallback}.
*/
void unregisterCallback(IFeatureFlagsCallback callback);
/**
* Query the {@link com.android.server.flags.FeatureFlagsService} for flags, but don't
* cache them. See {@link #syncFlags}.
*
* You almost certainly don't want this method. This is intended for the Flag Flipper
* application that needs to query the state of system but doesn't want to affect it by
* doing so. All other clients should use {@link syncFlags}.
*/
List<SyncableFlag> queryFlags(in List<SyncableFlag> flagList);
/**
* Change a flags value in the system.
*
* This is intended for use by the Flag Flipper application.
*/
void overrideFlag(in SyncableFlag flag);
/**
* Restore a flag to its default value.
*
* This is intended for use by the Flag Flipper application.
*/
void resetFlag(in SyncableFlag flag);
}

View File

@@ -1,31 +0,0 @@
/*
* 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.flags.SyncableFlag;
/**
* Callback for {@link IFeatureFlags#registerCallback} to get alerts when a {@link DynamicFlag}
* changes.
*
* DynamicFlags can change at run time. Stable flags will never result in a call to this method.
*
* @hide
*/
oneway interface IFeatureFlagsCallback {
void onFlagChange(in SyncableFlag flag);
}

View File

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

View File

@@ -1,22 +0,0 @@
/*
* 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 parcelable data class for serializing {@link Flag} across a Binder.
*/
parcelable SyncableFlag;

View File

@@ -1,112 +0,0 @@
/*
* 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 android.os.Parcel;
import android.os.Parcelable;
/**
* @hide
*/
public final class SyncableFlag implements Parcelable {
private final String mNamespace;
private final String mName;
private final String mValue;
private final boolean mDynamic;
private final boolean mOverridden;
public SyncableFlag(
@NonNull String namespace,
@NonNull String name,
@NonNull String value,
boolean dynamic) {
this(namespace, name, value, dynamic, false);
}
public SyncableFlag(
@NonNull String namespace,
@NonNull String name,
@NonNull String value,
boolean dynamic,
boolean overridden
) {
mNamespace = namespace;
mName = name;
mValue = value;
mDynamic = dynamic;
mOverridden = overridden;
}
@NonNull
public String getNamespace() {
return mNamespace;
}
@NonNull
public String getName() {
return mName;
}
@NonNull
public String getValue() {
return mValue;
}
public boolean isDynamic() {
return mDynamic;
}
public boolean isOverridden() {
return mOverridden;
}
@NonNull
public static final Parcelable.Creator<SyncableFlag> CREATOR = new Parcelable.Creator<>() {
public SyncableFlag createFromParcel(Parcel in) {
return new SyncableFlag(
in.readString(),
in.readString(),
in.readString(),
in.readBoolean(),
in.readBoolean());
}
public SyncableFlag[] newArray(int size) {
return new SyncableFlag[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(mNamespace);
dest.writeString(mName);
dest.writeString(mValue);
dest.writeBoolean(mDynamic);
dest.writeBoolean(mOverridden);
}
@Override
public String toString() {
return getNamespace() + "." + getName() + "[" + getValue() + "]";
}
}

View File

@@ -1,93 +0,0 @@
/*
* 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.flags;
import android.flags.BooleanFlag;
import android.flags.DynamicBooleanFlag;
import android.flags.FeatureFlags;
import android.flags.FusedOffFlag;
import android.flags.FusedOnFlag;
import android.flags.SyncableFlag;
import java.util.ArrayList;
import java.util.List;
/**
* Flags defined here are can be read by code in core.
*
* Flags not defined here will throw a security exception if third-party processes attempts to read
* them.
*
* DO NOT define a flag here unless you explicitly intend for that flag to be readable by code that
* runs inside a third party process.
*/
public abstract class CoreFlags {
private static final List<SyncableFlag> sKnownFlags = new ArrayList<>();
public static BooleanFlag BOOL_FLAG = booleanFlag("core", "bool_flag", false);
public static FusedOffFlag OFF_FLAG = fusedOffFlag("core", "off_flag");
public static FusedOnFlag ON_FLAG = fusedOnFlag("core", "on_flag");
public static DynamicBooleanFlag DYN_FLAG = dynamicBooleanFlag("core", "dyn_flag", true);
/** Returns true if the passed in flag matches a flag in this class. */
public static boolean isCoreFlag(SyncableFlag flag) {
for (SyncableFlag knownFlag : sKnownFlags) {
if (knownFlag.getName().equals(flag.getName())
&& knownFlag.getNamespace().equals(flag.getNamespace())) {
return true;
}
}
return false;
}
public static List<SyncableFlag> getCoreFlags() {
return sKnownFlags;
}
private static BooleanFlag booleanFlag(String namespace, String name, boolean defaultValue) {
BooleanFlag f = FeatureFlags.booleanFlag(namespace, name, defaultValue);
sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), false));
return f;
}
private static FusedOffFlag fusedOffFlag(String namespace, String name) {
FusedOffFlag f = FeatureFlags.fusedOffFlag(namespace, name);
sKnownFlags.add(new SyncableFlag(namespace, name, "false", false));
return f;
}
private static FusedOnFlag fusedOnFlag(String namespace, String name) {
FusedOnFlag f = FeatureFlags.fusedOnFlag(namespace, name);
sKnownFlags.add(new SyncableFlag(namespace, name, "true", false));
return f;
}
private static DynamicBooleanFlag dynamicBooleanFlag(
String namespace, String name, boolean defaultValue) {
DynamicBooleanFlag f = FeatureFlags.dynamicBooleanFlag(namespace, name, defaultValue);
sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), true));
return f;
}
}

View File

@@ -7655,24 +7655,6 @@
<permission android:name="android.permission.GET_ANY_PROVIDER_TYPE"
android:protectionLevel="signature" />
<!-- @hide Allows internal applications to read and synchronize non-core flags.
Apps without this permission can only read a subset of flags specifically intended
for use in "core", (i.e. third party apps). Apps with this permission can define their
own flags, and federate those values with other system-level apps.
<p>Not for use by third-party applications.
<p>Protection level: signature
-->
<permission android:name="android.permission.SYNC_FLAGS"
android:protectionLevel="signature" />
<!-- @hide Allows internal applications to override flags in the FeatureFlags service.
<p>Not for use by third-party applications.
<p>Protection level: signature
-->
<permission android:name="android.permission.WRITE_FLAGS"
android:protectionLevel="signature" />
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->

View File

@@ -1,155 +0,0 @@
/*
* 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.Before;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@SmallTest
@Presubmit
public class FeatureFlagsTest {
IFeatureFlagsFake mIFeatureFlagsFake = new IFeatureFlagsFake();
FeatureFlags mFeatureFlags = new FeatureFlags(mIFeatureFlagsFake);
@Before
public void setup() {
FeatureFlags.setInstance(mFeatureFlags);
}
@Test
public void testFusedOff_Disabled() {
FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a");
assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
}
@Test
public void testFusedOn_Enabled() {
FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a");
assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
}
@Test
public void testBooleanFlag_DefaultDisabled() {
BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false);
assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
}
@Test
public void testBooleanFlag_DefaultEnabled() {
BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", true);
assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
}
@Test
public void testDynamicBooleanFlag_DefaultDisabled() {
DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse();
}
@Test
public void testDynamicBooleanFlag_DefaultEnabled() {
DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", true);
assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
}
@Test
public void testBooleanFlag_OverrideBeforeRead() {
BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false);
SyncableFlag syncableFlag = new SyncableFlag(
flag.getNamespace(), flag.getName(), "true", false);
mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
}
@Test
public void testFusedOffFlag_OverrideHasNoEffect() {
FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a");
SyncableFlag syncableFlag = new SyncableFlag(
flag.getNamespace(), flag.getName(), "true", false);
mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
}
@Test
public void testFusedOnFlag_OverrideHasNoEffect() {
FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a");
SyncableFlag syncableFlag = new SyncableFlag(
flag.getNamespace(), flag.getName(), "false", false);
mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
}
@Test
public void testDynamicFlag_OverrideBeforeRead() {
DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
SyncableFlag syncableFlag = new SyncableFlag(
flag.getNamespace(), flag.getName(), "true", true);
mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
// Changes to true
assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
}
@Test
public void testDynamicFlag_OverrideAfterRead() {
DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
SyncableFlag syncableFlag = new SyncableFlag(
flag.getNamespace(), flag.getName(), "true", true);
// Starts false
assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse();
mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
// Changes to true
assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
}
@Test
public void testDynamicFlag_FiresListener() {
DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
AtomicBoolean called = new AtomicBoolean(false);
FeatureFlags.ChangeListener listener = flag1 -> called.set(true);
mFeatureFlags.addChangeListener(listener);
SyncableFlag syncableFlag = new SyncableFlag(
flag.getNamespace(), flag.getName(), flag.getDefault().toString(), true);
mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
// Fires listener.
assertThat(called.get()).isTrue();
}
}

View File

@@ -1,113 +0,0 @@
/*
* 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.os.IBinder;
import android.os.RemoteException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class IFeatureFlagsFake implements IFeatureFlags {
private final Set<IFeatureFlagsCallback> mCallbacks = new HashSet<>();
List<SyncableFlag> mOverrides;
@Override
public IBinder asBinder() {
return null;
}
@Override
public List<SyncableFlag> syncFlags(List<SyncableFlag> flagList) {
return mOverrides == null ? flagList : mOverrides;
}
@Override
public List<SyncableFlag> queryFlags(List<SyncableFlag> flagList) {
return mOverrides == null ? flagList : mOverrides; }
@Override
public void overrideFlag(SyncableFlag syncableFlag) {
SyncableFlag match = findFlag(syncableFlag);
if (match != null) {
mOverrides.remove(match);
}
mOverrides.add(syncableFlag);
for (IFeatureFlagsCallback cb : mCallbacks) {
try {
cb.onFlagChange(syncableFlag);
} catch (RemoteException e) {
// does not happen in fakes.
}
}
}
@Override
public void resetFlag(SyncableFlag syncableFlag) {
SyncableFlag match = findFlag(syncableFlag);
if (match != null) {
mOverrides.remove(match);
}
for (IFeatureFlagsCallback cb : mCallbacks) {
try {
cb.onFlagChange(syncableFlag);
} catch (RemoteException e) {
// does not happen in fakes.
}
}
}
private SyncableFlag findFlag(SyncableFlag syncableFlag) {
SyncableFlag match = null;
for (SyncableFlag sf : mOverrides) {
if (sf.getName().equals(syncableFlag.getName())
&& sf.getNamespace().equals(syncableFlag.getNamespace())) {
match = sf;
break;
}
}
return match;
}
@Override
public void registerCallback(IFeatureFlagsCallback callback) {
mCallbacks.add(callback);
}
@Override
public void unregisterCallback(IFeatureFlagsCallback callback) {
mCallbacks.remove(callback);
}
public void setFlagOverrides(List<SyncableFlag> flagList) {
mOverrides = flagList;
for (SyncableFlag sf : flagList) {
for (IFeatureFlagsCallback cb : mCallbacks) {
try {
cb.onFlagChange(sf);
} catch (RemoteException e) {
// does not happen in fakes.
}
}
}
}
}

View File

@@ -159,7 +159,6 @@ java_library {
"services.coverage",
"services.credentials",
"services.devicepolicy",
"services.flags",
"services.midi",
"services.musicsearch",
"services.net",

View File

@@ -1,18 +0,0 @@
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
java_library_static {
name: "services.flags",
defaults: ["platform_service_defaults"],
srcs: [
"java/**/*.java",
":feature_flags_aidl",
],
libs: ["services.core"],
}

View File

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

View File

@@ -1,280 +0,0 @@
/*
* 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.server.flags;
import android.annotation.NonNull;
import android.flags.IFeatureFlagsCallback;
import android.flags.SyncableFlag;
import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.DeviceConfig;
import android.util.Slog;
import com.android.internal.os.BackgroundThread;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
/**
* Handles DynamicFlags for {@link FeatureFlagsBinder}.
*
* Dynamic flags are simultaneously simpler and more complicated than process stable flags. We can
* return whatever value is last known for a flag is, without too much worry about the flags
* changing (they are dynamic after all). However, we have to alert all the relevant clients
* about those flag changes, and need to be able to restore to a default value if the flag gets
* reset/erased during runtime.
*/
class DynamicFlagBinderDelegate {
private final FlagOverrideStore mFlagStore;
private final FlagCache<DynamicFlagData> mDynamicFlags = new FlagCache<>();
private final Map<Integer, Set<IFeatureFlagsCallback>> mCallbacks = new HashMap<>();
private static final Function<Integer, Set<IFeatureFlagsCallback>> NEW_CALLBACK_SET =
k -> new HashSet<>();
private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigListener =
new DeviceConfig.OnPropertiesChangedListener() {
@Override
public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
String ns = properties.getNamespace();
for (String name : properties.getKeyset()) {
// Don't alert for flags we don't care about.
// Don't alert for flags that have been overridden locally.
if (!mDynamicFlags.contains(ns, name) || mFlagStore.contains(ns, name)) {
continue;
}
mFlagChangeCallback.onFlagChanged(
ns, name, properties.getString(name, null));
}
}
};
private final FlagOverrideStore.FlagChangeCallback mFlagChangeCallback =
(namespace, name, value) -> {
// Don't bother with callbacks for non-dynamic flags.
if (!mDynamicFlags.contains(namespace, name)) {
return;
}
// Don't bother with callbacks if nothing changed.
// Handling erasure (null) is special, as we may be restoring back to a value
// we were already at.
DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name);
if (data == null) {
return; // shouldn't happen, but better safe than sorry.
}
if (value == null) {
if (data.getValue().equals(data.getDefaultValue())) {
return;
}
value = data.getDefaultValue();
} else if (data.getValue().equals(value)) {
return;
}
data.setValue(value);
final Set<IFeatureFlagsCallback> cbCopy;
synchronized (mCallbacks) {
cbCopy = new HashSet<>();
for (Integer pid : mCallbacks.keySet()) {
if (data.containsPid(pid)) {
cbCopy.addAll(mCallbacks.get(pid));
}
}
}
SyncableFlag sFlag = new SyncableFlag(namespace, name, value, true);
cbCopy.forEach(cb -> {
try {
cb.onFlagChange(sFlag);
} catch (RemoteException e) {
Slog.w(
FeatureFlagsService.TAG,
"Failed to communicate flag change to client.");
}
});
};
DynamicFlagBinderDelegate(FlagOverrideStore flagStore) {
mFlagStore = flagStore;
mFlagStore.setChangeCallback(mFlagChangeCallback);
}
SyncableFlag syncDynamicFlag(int pid, SyncableFlag sf) {
if (!sf.isDynamic()) {
return sf;
}
String ns = sf.getNamespace();
String name = sf.getName();
// Dynamic flags don't need any special threading or synchronization considerations.
// We simply give them whatever the current value is.
// However, we do need to keep track of dynamic flags, so that we can alert
// about changes coming in from adb, DeviceConfig, or other sources.
// And also so that we can keep flags relatively consistent across processes.
DynamicFlagData data = mDynamicFlags.getOrNull(ns, name);
String value = getFlagValue(ns, name, sf.getValue());
// DeviceConfig listeners are per-namespace.
if (!mDynamicFlags.containsNamespace(ns)) {
DeviceConfig.addOnPropertiesChangedListener(
ns, BackgroundThread.getExecutor(), mDeviceConfigListener);
}
data.addClientPid(pid);
data.setValue(value);
// Store the default value so that if an override gets erased, we can restore
// to something.
data.setDefaultValue(sf.getValue());
return new SyncableFlag(sf.getNamespace(), sf.getName(), value, true);
}
void registerCallback(int pid, IFeatureFlagsCallback callback) {
// Always add callback so that we don't end up with a possible race/leak.
// We remove the callback directly if we fail to call #linkToDeath.
// If we tried to add the callback after we linked, then we could end up in a
// scenario where we link, then the binder dies, firing our BinderGriever which tries
// to remove the callback (which has not yet been added), then finally we add the
// callback, creating a leak.
Set<IFeatureFlagsCallback> callbacks;
synchronized (mCallbacks) {
callbacks = mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET);
callbacks.add(callback);
}
try {
callback.asBinder().linkToDeath(new BinderGriever(pid), 0);
} catch (RemoteException e) {
Slog.e(
FeatureFlagsService.TAG,
"Failed to link to binder death. Callback not registered.");
synchronized (mCallbacks) {
callbacks.remove(callback);
}
}
}
void unregisterCallback(int pid, IFeatureFlagsCallback callback) {
// No need to unlink, since the BinderGriever will essentially be a no-op.
// We would have to track our BinderGriever's in a map otherwise.
synchronized (mCallbacks) {
Set<IFeatureFlagsCallback> callbacks =
mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET);
callbacks.remove(callback);
}
}
String getFlagValue(String namespace, String name, String defaultValue) {
// If we already have a value cached, just use that.
String value = null;
DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name);
if (data != null) {
value = data.getValue();
} else {
// Put the value in the cache for future reference.
data = new DynamicFlagData(namespace, name);
mDynamicFlags.setIfChanged(namespace, name, data);
}
// If we're not in a release build, flags can be overridden locally on device.
if (!Build.IS_USER && value == null) {
value = mFlagStore.get(namespace, name);
}
// If we still don't have a value, maybe DeviceConfig does?
// Fallback to sf.getValue() here as well.
if (value == null) {
value = DeviceConfig.getString(namespace, name, defaultValue);
}
return value;
}
private static class DynamicFlagData {
private final String mNamespace;
private final String mName;
private final Set<Integer> mPids = new HashSet<>();
private String mValue;
private String mDefaultValue;
private DynamicFlagData(String namespace, String name) {
mNamespace = namespace;
mName = name;
}
String getValue() {
return mValue;
}
void setValue(String value) {
mValue = value;
}
String getDefaultValue() {
return mDefaultValue;
}
void setDefaultValue(String value) {
mDefaultValue = value;
}
void addClientPid(int pid) {
mPids.add(pid);
}
boolean containsPid(int pid) {
return mPids.contains(pid);
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof DynamicFlagData)) {
return false;
}
DynamicFlagData o = (DynamicFlagData) other;
return mName.equals(o.mName) && mNamespace.equals(o.mNamespace)
&& mValue.equals(o.mValue) && mDefaultValue.equals(o.mDefaultValue);
}
@Override
public int hashCode() {
return mName.hashCode() + mNamespace.hashCode()
+ mValue.hashCode() + mDefaultValue.hashCode();
}
}
private class BinderGriever implements IBinder.DeathRecipient {
private final int mPid;
private BinderGriever(int pid) {
mPid = pid;
}
@Override
public void binderDied() {
synchronized (mCallbacks) {
mCallbacks.remove(mPid);
}
}
}
}

View File

@@ -1,168 +0,0 @@
/*
* 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.server.flags;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.flags.IFeatureFlags;
import android.flags.IFeatureFlagsCallback;
import android.flags.SyncableFlag;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import com.android.internal.flags.CoreFlags;
import com.android.server.flags.FeatureFlagsService.PermissionsChecker;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
class FeatureFlagsBinder extends IFeatureFlags.Stub {
private final FlagOverrideStore mFlagStore;
private final FlagsShellCommand mShellCommand;
private final FlagCache<String> mFlagCache = new FlagCache<>();
private final DynamicFlagBinderDelegate mDynamicFlagDelegate;
private final PermissionsChecker mPermissionsChecker;
FeatureFlagsBinder(
FlagOverrideStore flagStore,
FlagsShellCommand shellCommand,
PermissionsChecker permissionsChecker) {
mFlagStore = flagStore;
mShellCommand = shellCommand;
mDynamicFlagDelegate = new DynamicFlagBinderDelegate(flagStore);
mPermissionsChecker = permissionsChecker;
}
@Override
public void registerCallback(IFeatureFlagsCallback callback) {
mDynamicFlagDelegate.registerCallback(getCallingPid(), callback);
}
@Override
public void unregisterCallback(IFeatureFlagsCallback callback) {
mDynamicFlagDelegate.unregisterCallback(getCallingPid(), callback);
}
// Note: The internals of this method should be kept in sync with queryFlags
// as they both should return identical results. The difference is that this method
// caches any values it receives and/or reads, whereas queryFlags does not.
@Override
public List<SyncableFlag> syncFlags(List<SyncableFlag> incomingFlags) {
int pid = getCallingPid();
List<SyncableFlag> outputFlags = new ArrayList<>();
boolean hasFullSyncPrivileges = false;
SecurityException permissionFailureException = null;
try {
assertSyncPermission();
hasFullSyncPrivileges = true;
} catch (SecurityException e) {
permissionFailureException = e;
}
for (SyncableFlag sf : incomingFlags) {
if (!hasFullSyncPrivileges && !CoreFlags.isCoreFlag(sf)) {
throw permissionFailureException;
}
String ns = sf.getNamespace();
String name = sf.getName();
SyncableFlag outFlag;
if (sf.isDynamic()) {
outFlag = mDynamicFlagDelegate.syncDynamicFlag(pid, sf);
} else {
synchronized (mFlagCache) {
String value = mFlagCache.getOrNull(ns, name);
if (value == null) {
String overrideValue = Build.IS_USER ? null : mFlagStore.get(ns, name);
value = overrideValue != null ? overrideValue : sf.getValue();
mFlagCache.setIfChanged(ns, name, value);
}
outFlag = new SyncableFlag(sf.getNamespace(), sf.getName(), value, false);
}
}
outputFlags.add(outFlag);
}
return outputFlags;
}
@Override
public void overrideFlag(SyncableFlag flag) {
assertWritePermission();
mFlagStore.set(flag.getNamespace(), flag.getName(), flag.getValue());
}
@Override
public void resetFlag(SyncableFlag flag) {
assertWritePermission();
mFlagStore.erase(flag.getNamespace(), flag.getName());
}
@Override
public List<SyncableFlag> queryFlags(List<SyncableFlag> incomingFlags) {
assertSyncPermission();
List<SyncableFlag> outputFlags = new ArrayList<>();
for (SyncableFlag sf : incomingFlags) {
String ns = sf.getNamespace();
String name = sf.getName();
String value;
String storeValue = mFlagStore.get(ns, name);
boolean overridden = storeValue != null;
if (sf.isDynamic()) {
value = mDynamicFlagDelegate.getFlagValue(ns, name, sf.getValue());
} else {
value = mFlagCache.getOrNull(ns, name);
if (value == null) {
value = Build.IS_USER ? null : storeValue;
if (value == null) {
value = sf.getValue();
}
}
}
outputFlags.add(new SyncableFlag(
sf.getNamespace(), sf.getName(), value, sf.isDynamic(), overridden));
}
return outputFlags;
}
private void assertSyncPermission() {
mPermissionsChecker.assertSyncPermission();
clearCallingIdentity();
}
private void assertWritePermission() {
mPermissionsChecker.assertWritePermission();
clearCallingIdentity();
}
@SystemApi
public int handleShellCommand(
@NonNull ParcelFileDescriptor in,
@NonNull ParcelFileDescriptor out,
@NonNull ParcelFileDescriptor err,
@NonNull String[] args) {
FileOutputStream fout = new FileOutputStream(out.getFileDescriptor());
FileOutputStream ferr = new FileOutputStream(err.getFileDescriptor());
return mShellCommand.process(args, fout, ferr);
}
}

View File

@@ -1,113 +0,0 @@
/*
* 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.server.flags;
import static android.Manifest.permission.SYNC_FLAGS;
import static android.Manifest.permission.WRITE_FLAGS;
import android.content.Context;
import android.content.pm.PackageManager;
import android.flags.FeatureFlags;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemService;
/**
* A service that manages syncing {@link android.flags.FeatureFlags} across processes.
*
* This service holds flags stable for at least the lifetime of a process, meaning that if
* a process comes online with a flag set to true, any other process that connects here and
* tries to read the same flag will also receive the flag as true. The flag will remain stable
* until either all of the interested processes have died, or the device restarts.
*
* TODO(279054964): Add to dumpsys
* @hide
*/
public class FeatureFlagsService extends SystemService {
static final String TAG = "FeatureFlagsService";
private final FlagOverrideStore mFlagStore;
private final FlagsShellCommand mShellCommand;
/**
* Initializes the system service.
*
* @param context The system server context.
*/
public FeatureFlagsService(Context context) {
super(context);
mFlagStore = new FlagOverrideStore(
new GlobalSettingsProxy(context.getContentResolver()));
mShellCommand = new FlagsShellCommand(mFlagStore);
}
@Override
public void onStart() {
Slog.d(TAG, "Started Feature Flag Service");
FeatureFlagsBinder service = new FeatureFlagsBinder(
mFlagStore, mShellCommand, new PermissionsChecker(getContext()));
publishBinderService(
Context.FEATURE_FLAGS_SERVICE, service);
publishLocalService(FeatureFlags.class, new FeatureFlags(service));
}
@Override
public void onBootPhase(int phase) {
super.onBootPhase(phase);
if (phase == PHASE_SYSTEM_SERVICES_READY) {
// Immediately sync our core flags so that they get locked in. We don't want third-party
// apps to override them, and syncing immediately is the easiest way to prevent that.
FeatureFlags.getInstance().sync();
}
}
/**
* Delegate for checking flag permissions.
*/
@VisibleForTesting
public static class PermissionsChecker {
private final Context mContext;
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public PermissionsChecker(Context context) {
mContext = context;
}
/**
* Ensures that the caller has {@link SYNC_FLAGS} permission.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void assertSyncPermission() {
if (mContext.checkCallingOrSelfPermission(SYNC_FLAGS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
"Non-core flag queried. Requires SYNC_FLAGS permission!");
}
}
/**
* Ensures that the caller has {@link WRITE_FLAGS} permission.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public void assertWritePermission() {
if (mContext.checkCallingPermission(WRITE_FLAGS) != PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires WRITE_FLAGS permission!");
}
}
}
}

View File

@@ -1,103 +0,0 @@
/*
* 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.server.flags;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* Threadsafe cache of values that stores the supplied default on cache miss.
*
* @param <V> The type of value to store.
*/
public class FlagCache<V> {
private final Function<String, HashMap<String, V>> mNewHashMap = k -> new HashMap<>();
// Cache is organized first by namespace, then by name. All values are stored as strings.
final Map<String, Map<String, V>> mCache = new HashMap<>();
FlagCache() {
}
/**
* Returns true if the namespace exists in the cache already.
*/
boolean containsNamespace(String namespace) {
synchronized (mCache) {
return mCache.containsKey(namespace);
}
}
/**
* Returns true if the value is stored in the cache.
*/
boolean contains(String namespace, String name) {
synchronized (mCache) {
Map<String, V> nsCache = mCache.get(namespace);
return nsCache != null && nsCache.containsKey(name);
}
}
/**
* Sets the value if it is different from what is currently stored.
*
* If the value is not set, or the current value is null, it will store the value and
* return true.
*
* @return True if the value was set. False if the value is the same.
*/
boolean setIfChanged(String namespace, String name, V value) {
synchronized (mCache) {
Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap);
V curValue = nsCache.get(name);
if (curValue == null || !curValue.equals(value)) {
nsCache.put(name, value);
return true;
}
return false;
}
}
/**
* Gets the current value from the cache, setting it if it is currently absent.
*
* @return The value that is now in the cache after the call to the method.
*/
V getOrSet(String namespace, String name, V defaultValue) {
synchronized (mCache) {
Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap);
V value = nsCache.putIfAbsent(name, defaultValue);
return value == null ? defaultValue : value;
}
}
/**
* Gets the current value from the cache, returning null if not present.
*
* @return The value that is now in the cache if there is one.
*/
V getOrNull(String namespace, String name) {
synchronized (mCache) {
Map<String, V> nsCache = mCache.get(namespace);
if (nsCache == null) {
return null;
}
return nsCache.get(name);
}
}
}

View File

@@ -1,122 +0,0 @@
/*
* 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.server.flags;
import android.database.Cursor;
import android.provider.Settings;
import com.android.internal.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Map;
/**
* Persistent storage for the {@link FeatureFlagsService}.
*
* The implementation stores data in Settings.<store> (generally {@link Settings.Global}
* is expected).
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class FlagOverrideStore {
private static final String KEYNAME_PREFIX = "flag|";
private static final String NAMESPACE_NAME_SEPARATOR = ".";
private final SettingsProxy mSettingsProxy;
private FlagChangeCallback mCallback;
FlagOverrideStore(SettingsProxy settingsProxy) {
mSettingsProxy = settingsProxy;
}
void setChangeCallback(FlagChangeCallback callback) {
mCallback = callback;
}
/** Returns true if a non-null value is in the store. */
boolean contains(String namespace, String name) {
return get(namespace, name) != null;
}
/** Put a value in the store. */
@VisibleForTesting
public void set(String namespace, String name, String value) {
mSettingsProxy.putString(getPropName(namespace, name), value);
mCallback.onFlagChanged(namespace, name, value);
}
/** Read a value out of the store. */
@VisibleForTesting
public String get(String namespace, String name) {
return mSettingsProxy.getString(getPropName(namespace, name));
}
/** Erase a value from the store. */
@VisibleForTesting
public void erase(String namespace, String name) {
set(namespace, name, null);
}
Map<String, Map<String, String>> getFlags() {
return getFlagsForNamespace(null);
}
Map<String, Map<String, String>> getFlagsForNamespace(String namespace) {
Cursor c = mSettingsProxy.getContentResolver().query(
Settings.Global.CONTENT_URI,
new String[]{Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE},
null, // Doesn't support a "LIKE" query
null,
null
);
if (c == null) {
return Map.of();
}
int keynamePrefixLength = KEYNAME_PREFIX.length();
Map<String, Map<String, String>> results = new HashMap<>();
while (c.moveToNext()) {
String key = c.getString(0);
if (!key.startsWith(KEYNAME_PREFIX)
|| key.indexOf(NAMESPACE_NAME_SEPARATOR, keynamePrefixLength) < 0) {
continue;
}
String value = c.getString(1);
if (value == null || value.isEmpty()) {
continue;
}
String ns = key.substring(keynamePrefixLength, key.indexOf(NAMESPACE_NAME_SEPARATOR));
if (namespace != null && !namespace.equals(ns)) {
continue;
}
String name = key.substring(key.indexOf(NAMESPACE_NAME_SEPARATOR) + 1);
results.putIfAbsent(ns, new HashMap<>());
results.get(ns).put(name, value);
}
c.close();
return results;
}
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
static String getPropName(String namespace, String name) {
return KEYNAME_PREFIX + namespace + NAMESPACE_NAME_SEPARATOR + name;
}
interface FlagChangeCallback {
void onFlagChanged(String namespace, String name, String value);
}
}

View File

@@ -1,214 +0,0 @@
/*
* 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.server.flags;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FastPrintWriter;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.Map;
/**
* Process command line input for the flags service.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public class FlagsShellCommand {
private final FlagOverrideStore mFlagStore;
FlagsShellCommand(FlagOverrideStore flagStore) {
mFlagStore = flagStore;
}
/**
* Interpret the command supplied in the constructor.
*
* @return Zero on success or non-zero on error.
*/
public int process(
String[] args,
OutputStream out,
OutputStream err) {
PrintWriter outPw = new FastPrintWriter(out);
PrintWriter errPw = new FastPrintWriter(err);
if (args.length == 0) {
return printHelp(outPw);
}
switch (args[0].toLowerCase(Locale.ROOT)) {
case "help":
return printHelp(outPw);
case "list":
return listCmd(args, outPw, errPw);
case "set":
return setCmd(args, outPw, errPw);
case "get":
return getCmd(args, outPw, errPw);
case "erase":
return eraseCmd(args, outPw, errPw);
default:
return unknownCmd(outPw);
}
}
private int printHelp(PrintWriter outPw) {
outPw.println("Feature Flags command, allowing listing, setting, getting, and erasing of");
outPw.println("local flag overrides on a device.");
outPw.println();
outPw.println("Commands:");
outPw.println(" list [namespace]");
outPw.println(" List all flag overrides. Namespace is optional.");
outPw.println();
outPw.println(" get <namespace> <name>");
outPw.println(" Return the string value of a specific flag, or <unset>");
outPw.println();
outPw.println(" set <namespace> <name> <value>");
outPw.println(" Set a specific flag");
outPw.println();
outPw.println(" erase <namespace> <name>");
outPw.println(" Unset a specific flag");
outPw.flush();
return 0;
}
private int listCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
if (!validateNumArguments(args, 0, 1, args[0], errPw)) {
errPw.println("Expected `" + args[0] + " [namespace]`");
errPw.flush();
return -1;
}
Map<String, Map<String, String>> overrides;
if (args.length == 2) {
overrides = mFlagStore.getFlagsForNamespace(args[1]);
} else {
overrides = mFlagStore.getFlags();
}
if (overrides.isEmpty()) {
outPw.println("No overrides set");
} else {
int longestNamespaceLen = "namespace".length();
int longestFlagLen = "flag".length();
int longestValLen = "value".length();
for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) {
longestNamespaceLen = Math.max(longestNamespaceLen, namespace.getKey().length());
for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) {
longestFlagLen = Math.max(longestFlagLen, flag.getKey().length());
longestValLen = Math.max(longestValLen, flag.getValue().length());
}
}
outPw.print(String.format("%-" + longestNamespaceLen + "s", "namespace"));
outPw.print(' ');
outPw.print(String.format("%-" + longestFlagLen + "s", "flag"));
outPw.print(' ');
outPw.println("value");
for (int i = 0; i < longestNamespaceLen; i++) {
outPw.print('=');
}
outPw.print(' ');
for (int i = 0; i < longestFlagLen; i++) {
outPw.print('=');
}
outPw.print(' ');
for (int i = 0; i < longestValLen; i++) {
outPw.print('=');
}
outPw.println();
for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) {
for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) {
outPw.print(
String.format("%-" + longestNamespaceLen + "s", namespace.getKey()));
outPw.print(' ');
outPw.print(String.format("%-" + longestFlagLen + "s", flag.getKey()));
outPw.print(' ');
outPw.println(flag.getValue());
}
}
}
outPw.flush();
return 0;
}
private int setCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
if (!validateNumArguments(args, 3, args[0], errPw)) {
errPw.println("Expected `" + args[0] + " <namespace> <name> <value>`");
errPw.flush();
return -1;
}
mFlagStore.set(args[1], args[2], args[3]);
outPw.println("Flag " + args[1] + "." + args[2] + " is now " + args[3]);
outPw.flush();
return 0;
}
private int getCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
if (!validateNumArguments(args, 2, args[0], errPw)) {
errPw.println("Expected `" + args[0] + " <namespace> <name>`");
errPw.flush();
return -1;
}
String value = mFlagStore.get(args[1], args[2]);
outPw.print(args[1] + "." + args[2] + " is ");
if (value == null || value.isEmpty()) {
outPw.println("<unset>");
} else {
outPw.println("\"" + value.translateEscapes() + "\"");
}
outPw.flush();
return 0;
}
private int eraseCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
if (!validateNumArguments(args, 2, args[0], errPw)) {
errPw.println("Expected `" + args[0] + " <namespace> <name>`");
errPw.flush();
return -1;
}
mFlagStore.erase(args[1], args[2]);
outPw.println("Erased " + args[1] + "." + args[2]);
return 0;
}
private int unknownCmd(PrintWriter outPw) {
outPw.println("This command is unknown.");
printHelp(outPw);
outPw.flush();
return -1;
}
private boolean validateNumArguments(
String[] args, int exactly, String cmdName, PrintWriter errPw) {
return validateNumArguments(args, exactly, exactly, cmdName, errPw);
}
private boolean validateNumArguments(
String[] args, int min, int max, String cmdName, PrintWriter errPw) {
int len = args.length - 1; // Discount the command itself.
if (len < min) {
errPw.println(
"Less than " + min + " arguments provided for \"" + cmdName + "\" command.");
return false;
} else if (len > max) {
errPw.println(
"More than " + max + " arguments provided for \"" + cmdName + "\" command.");
return false;
}
return true;
}
}

View File

@@ -1,68 +0,0 @@
/*
* 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.server.flags;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.Settings;
class GlobalSettingsProxy implements SettingsProxy {
private final ContentResolver mContentResolver;
GlobalSettingsProxy(ContentResolver contentResolver) {
mContentResolver = contentResolver;
}
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
@Override
public Uri getUriFor(String name) {
return Settings.Global.getUriFor(name);
}
@Override
public String getStringForUser(String name, int userHandle) {
return Settings.Global.getStringForUser(mContentResolver, name, userHandle);
}
@Override
public boolean putString(String name, String value, boolean overrideableByRestore) {
throw new UnsupportedOperationException(
"This method only exists publicly for Settings.System and Settings.Secure");
}
@Override
public boolean putStringForUser(String name, String value, int userHandle) {
return Settings.Global.putStringForUser(mContentResolver, name, value, userHandle);
}
@Override
public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
int userHandle, boolean overrideableByRestore) {
return Settings.Global.putStringForUser(
mContentResolver, name, value, tag, makeDefault, userHandle,
overrideableByRestore);
}
@Override
public boolean putString(String name, String value, String tag, boolean makeDefault) {
return Settings.Global.putString(mContentResolver, name, value, tag, makeDefault);
}
}

View File

@@ -1,381 +0,0 @@
/*
* 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.server.flags;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.net.Uri;
import android.provider.Settings;
/**
* Wrapper class meant to enable hermetic testing of {@link Settings}.
*
* Implementations of this class are expected to be constructed with a {@link ContentResolver} or,
* otherwise have access to an implicit one. All the proxy methods in this class exclude
* {@link ContentResolver} from their signature and rely on an internally defined one instead.
*
* Most methods in the {@link Settings} classes have default implementations defined.
* Implementations of this interfac need only concern themselves with getting and putting Strings.
* They should also override any methods for a class they are proxying that _are not_ defined, and
* throw an appropriate {@link UnsupportedOperationException}. For instance, {@link Settings.Global}
* does not define {@link #putString(String, String, boolean)}, so an implementation of this
* interface that proxies through to it should throw an exception when that method is called.
*
* This class adds in the following helpers as well:
* - {@link #getBool(String)}
* - {@link #putBool(String, boolean)}
* - {@link #registerContentObserver(Uri, ContentObserver)}
*
* ... and similar variations for all of those.
*/
public interface SettingsProxy {
/**
* Returns the {@link ContentResolver} this instance uses.
*/
ContentResolver getContentResolver();
/**
* Construct the content URI for a particular name/value pair,
* useful for monitoring changes with a ContentObserver.
* @param name to look up in the table
* @return the corresponding content URI, or null if not present
*/
Uri getUriFor(String name);
/**See {@link Settings.Secure#getString(ContentResolver, String)} */
String getStringForUser(String name, int userHandle);
/**See {@link Settings.Secure#putString(ContentResolver, String, String, boolean)} */
boolean putString(String name, String value, boolean overrideableByRestore);
/** See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, int)} */
boolean putStringForUser(String name, String value, int userHandle);
/**
* See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, String, boolean,
* int, boolean)}
*/
boolean putStringForUser(@NonNull String name, @Nullable String value, @Nullable String tag,
boolean makeDefault, @UserIdInt int userHandle, boolean overrideableByRestore);
/** See {@link Settings.Secure#putString(ContentResolver, String, String, String, boolean)} */
boolean putString(@NonNull String name, @Nullable String value, @Nullable String tag,
boolean makeDefault);
/**
* Returns the user id for the associated {@link ContentResolver}.
*/
default int getUserId() {
return getContentResolver().getUserId();
}
/** See {@link Settings.Secure#getString(ContentResolver, String)} */
default String getString(String name) {
return getStringForUser(name, getUserId());
}
/** See {@link Settings.Secure#putString(ContentResolver, String, String)} */
default boolean putString(String name, String value) {
return putStringForUser(name, value, getUserId());
}
/** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int, int)} */
default int getIntForUser(String name, int def, int userHandle) {
String v = getStringForUser(name, userHandle);
try {
return v != null ? Integer.parseInt(v) : def;
} catch (NumberFormatException e) {
return def;
}
}
/** See {@link Settings.Secure#getInt(ContentResolver, String)} */
default int getInt(String name) throws Settings.SettingNotFoundException {
return getIntForUser(name, getUserId());
}
/** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int)} */
default int getIntForUser(String name, int userHandle)
throws Settings.SettingNotFoundException {
String v = getStringForUser(name, userHandle);
try {
return Integer.parseInt(v);
} catch (NumberFormatException e) {
throw new Settings.SettingNotFoundException(name);
}
}
/** See {@link Settings.Secure#putInt(ContentResolver, String, int)} */
default boolean putInt(String name, int value) {
return putIntForUser(name, value, getUserId());
}
/** See {@link Settings.Secure#putIntForUser(ContentResolver, String, int, int)} */
default boolean putIntForUser(String name, int value, int userHandle) {
return putStringForUser(name, Integer.toString(value), userHandle);
}
/**
* Convenience function for retrieving a single settings value
* as a boolean. Note that internally setting values are always
* stored as strings; this function converts the string to a boolean
* for you. The default value will be returned if the setting is
* not defined or not a boolean.
*
* @param name The name of the setting to retrieve.
* @param def Value to return if the setting is not defined.
*
* @return The setting's current value, or 'def' if it is not defined
* or not a valid boolean.
*/
default boolean getBool(String name, boolean def) {
return getBoolForUser(name, def, getUserId());
}
/** See {@link #getBool(String, boolean)}. */
default boolean getBoolForUser(String name, boolean def, int userHandle) {
return getIntForUser(name, def ? 1 : 0, userHandle) != 0;
}
/**
* Convenience function for retrieving a single settings value
* as a boolean. Note that internally setting values are always
* stored as strings; this function converts the string to a boolean
* for you.
* <p>
* This version does not take a default value. If the setting has not
* been set, or the string value is not a number,
* it throws {@link Settings.SettingNotFoundException}.
*
* @param name The name of the setting to retrieve.
*
* @throws Settings.SettingNotFoundException Thrown if a setting by the given
* name can't be found or the setting value is not a boolean.
*
* @return The setting's current value.
*/
default boolean getBool(String name) throws Settings.SettingNotFoundException {
return getBoolForUser(name, getUserId());
}
/** See {@link #getBool(String)}. */
default boolean getBoolForUser(String name, int userHandle)
throws Settings.SettingNotFoundException {
return getIntForUser(name, userHandle) != 0;
}
/**
* Convenience function for updating a single settings value as a
* boolean. This will either create a new entry in the table if the
* given name does not exist, or modify the value of the existing row
* with that name. Note that internally setting values are always
* stored as strings, so this function converts the given value to a
* string before storing it.
*
* @param name The name of the setting to modify.
* @param value The new value for the setting.
* @return true if the value was set, false on database errors
*/
default boolean putBool(String name, boolean value) {
return putBoolForUser(name, value, getUserId());
}
/** See {@link #putBool(String, boolean)}. */
default boolean putBoolForUser(String name, boolean value, int userHandle) {
return putIntForUser(name, value ? 1 : 0, userHandle);
}
/** See {@link Settings.Secure#getLong(ContentResolver, String, long)} */
default long getLong(String name, long def) {
return getLongForUser(name, def, getUserId());
}
/** See {@link Settings.Secure#getLongForUser(ContentResolver, String, long, int)} */
default long getLongForUser(String name, long def, int userHandle) {
String valString = getStringForUser(name, userHandle);
long value;
try {
value = valString != null ? Long.parseLong(valString) : def;
} catch (NumberFormatException e) {
value = def;
}
return value;
}
/** See {@link Settings.Secure#getLong(ContentResolver, String)} */
default long getLong(String name) throws Settings.SettingNotFoundException {
return getLongForUser(name, getUserId());
}
/** See {@link Settings.Secure#getLongForUser(ContentResolver, String, int)} */
default long getLongForUser(String name, int userHandle)
throws Settings.SettingNotFoundException {
String valString = getStringForUser(name, userHandle);
try {
return Long.parseLong(valString);
} catch (NumberFormatException e) {
throw new Settings.SettingNotFoundException(name);
}
}
/** See {@link Settings.Secure#putLong(ContentResolver, String, long)} */
default boolean putLong(String name, long value) {
return putLongForUser(name, value, getUserId());
}
/** See {@link Settings.Secure#putLongForUser(ContentResolver, String, long, int)} */
default boolean putLongForUser(String name, long value, int userHandle) {
return putStringForUser(name, Long.toString(value), userHandle);
}
/** See {@link Settings.Secure#getFloat(ContentResolver, String, float)} */
default float getFloat(String name, float def) {
return getFloatForUser(name, def, getUserId());
}
/** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)} */
default float getFloatForUser(String name, float def, int userHandle) {
String v = getStringForUser(name, userHandle);
try {
return v != null ? Float.parseFloat(v) : def;
} catch (NumberFormatException e) {
return def;
}
}
/** See {@link Settings.Secure#getFloat(ContentResolver, String)} */
default float getFloat(String name) throws Settings.SettingNotFoundException {
return getFloatForUser(name, getUserId());
}
/** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)} */
default float getFloatForUser(String name, int userHandle)
throws Settings.SettingNotFoundException {
String v = getStringForUser(name, userHandle);
if (v == null) {
throw new Settings.SettingNotFoundException(name);
}
try {
return Float.parseFloat(v);
} catch (NumberFormatException e) {
throw new Settings.SettingNotFoundException(name);
}
}
/** See {@link Settings.Secure#putFloat(ContentResolver, String, float)} */
default boolean putFloat(String name, float value) {
return putFloatForUser(name, value, getUserId());
}
/** See {@link Settings.Secure#putFloatForUser(ContentResolver, String, float, int)} */
default boolean putFloatForUser(String name, float value, int userHandle) {
return putStringForUser(name, Float.toString(value), userHandle);
}
/**
* Convenience wrapper around
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
*
* Implicitly calls {@link #getUriFor(String)} on the passed in name.
*/
default void registerContentObserver(String name, ContentObserver settingsObserver) {
registerContentObserver(getUriFor(name), settingsObserver);
}
/**
* Convenience wrapper around
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
*/
default void registerContentObserver(Uri uri, ContentObserver settingsObserver) {
registerContentObserverForUser(uri, settingsObserver, getUserId());
}
/**
* Convenience wrapper around
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.
*
* Implicitly calls {@link #getUriFor(String)} on the passed in name.
*/
default void registerContentObserver(String name, boolean notifyForDescendants,
ContentObserver settingsObserver) {
registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver);
}
/**
* Convenience wrapper around
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
*/
default void registerContentObserver(Uri uri, boolean notifyForDescendants,
ContentObserver settingsObserver) {
registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, getUserId());
}
/**
* Convenience wrapper around
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
*
* Implicitly calls {@link #getUriFor(String)} on the passed in name.
*/
default void registerContentObserverForUser(
String name, ContentObserver settingsObserver, int userHandle) {
registerContentObserverForUser(
getUriFor(name), settingsObserver, userHandle);
}
/**
* Convenience wrapper around
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
*/
default void registerContentObserverForUser(
Uri uri, ContentObserver settingsObserver, int userHandle) {
registerContentObserverForUser(
uri, false, settingsObserver, userHandle);
}
/**
* Convenience wrapper around
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
*
* Implicitly calls {@link #getUriFor(String)} on the passed in name.
*/
default void registerContentObserverForUser(
String name, boolean notifyForDescendants, ContentObserver settingsObserver,
int userHandle) {
registerContentObserverForUser(
getUriFor(name), notifyForDescendants, settingsObserver, userHandle);
}
/**
* Convenience wrapper around
* {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
*/
default void registerContentObserverForUser(
Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver,
int userHandle) {
getContentResolver().registerContentObserver(
uri, notifyForDescendants, settingsObserver, userHandle);
}
/** See {@link ContentResolver#unregisterContentObserver(ContentObserver)}. */
default void unregisterContentObserver(ContentObserver settingsObserver) {
getContentResolver().unregisterContentObserver(settingsObserver);
}
}

View File

@@ -132,7 +132,6 @@ import com.android.server.display.DisplayManagerService;
import com.android.server.display.color.ColorDisplayService;
import com.android.server.dreams.DreamManagerService;
import com.android.server.emergency.EmergencyAffordanceService;
import com.android.server.flags.FeatureFlagsService;
import com.android.server.gpu.GpuService;
import com.android.server.grammaticalinflection.GrammaticalInflectionService;
import com.android.server.graphics.fonts.FontManagerService;
@@ -1112,12 +1111,6 @@ public final class SystemServer implements Dumpable {
mSystemServiceManager.startService(DeviceIdentifiersPolicyService.class);
t.traceEnd();
// Starts a service for reading runtime flag overrides, and keeping processes
// in sync with one another.
t.traceBegin("StartFeatureFlagsService");
mSystemServiceManager.startService(FeatureFlagsService.class);
t.traceEnd();
// Uri Grants Manager.
t.traceBegin("UriGrantsManagerService");
mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class);

View File

@@ -34,7 +34,6 @@ android_test {
"services.core",
"services.credentials",
"services.devicepolicy",
"services.flags",
"services.net",
"services.people",
"services.usage",

View File

@@ -1,293 +0,0 @@
/*
* 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.server.flags;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.flags.IFeatureFlagsCallback;
import android.flags.SyncableFlag;
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.List;
@Presubmit
@SmallTest
public class FeatureFlagsServiceTest {
private static final String NS = "ns";
private static final String NAME = "name";
private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME);
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@Mock
private FlagOverrideStore mFlagStore;
@Mock
private FlagsShellCommand mFlagCommand;
@Mock
private IFeatureFlagsCallback mIFeatureFlagsCallback;
@Mock
private IBinder mIFeatureFlagsCallbackAsBinder;
@Mock
private FeatureFlagsService.PermissionsChecker mPermissionsChecker;
private FeatureFlagsBinder mFeatureFlagsService;
@Before
public void setup() {
when(mIFeatureFlagsCallback.asBinder()).thenReturn(mIFeatureFlagsCallbackAsBinder);
mFeatureFlagsService = new FeatureFlagsBinder(
mFlagStore, mFlagCommand, mPermissionsChecker);
}
@Test
public void testRegisterCallback() {
mFeatureFlagsService.registerCallback(mIFeatureFlagsCallback);
try {
verify(mIFeatureFlagsCallbackAsBinder).linkToDeath(any(), eq(0));
} catch (RemoteException e) {
fail("Our mock threw a Remote Exception?");
}
}
@Test
public void testOverrideFlag_requiresWritePermission() {
SecurityException exc = new SecurityException("not allowed");
doThrow(exc).when(mPermissionsChecker).assertWritePermission();
SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
try {
mFeatureFlagsService.overrideFlag(f);
fail("Should have thrown exception");
} catch (SecurityException e) {
assertThat(exc).isEqualTo(e);
} catch (Exception e) {
fail("should have thrown a security exception");
}
}
@Test
public void testResetFlag_requiresWritePermission() {
SecurityException exc = new SecurityException("not allowed");
doThrow(exc).when(mPermissionsChecker).assertWritePermission();
SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
try {
mFeatureFlagsService.resetFlag(f);
fail("Should have thrown exception");
} catch (SecurityException e) {
assertThat(exc).isEqualTo(e);
} catch (Exception e) {
fail("should have thrown a security exception");
}
}
@Test
public void testSyncFlags_noOverrides() {
List<SyncableFlag> inputFlags = List.of(
new SyncableFlag(NS, "a", "false", false),
new SyncableFlag(NS, "b", "true", false),
new SyncableFlag(NS, "c", "false", false)
);
List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags);
assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
for (SyncableFlag inpF: inputFlags) {
boolean found = false;
for (SyncableFlag outF : outputFlags) {
if (compareSyncableFlagsNames(inpF, outF)) {
found = true;
break;
}
}
assertWithMessage("Failed to find input flag " + inpF + " in the output")
.that(found).isTrue();
}
}
@Test
public void testSyncFlags_withSomeOverrides() {
List<SyncableFlag> inputFlags = List.of(
new SyncableFlag(NS, "a", "false", false),
new SyncableFlag(NS, "b", "true", false),
new SyncableFlag(NS, "c", "false", false)
);
assertThat(mFlagStore).isNotNull();
when(mFlagStore.get(NS, "c")).thenReturn("true");
List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags);
assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
for (SyncableFlag inpF: inputFlags) {
boolean found = false;
for (SyncableFlag outF : outputFlags) {
if (compareSyncableFlagsNames(inpF, outF)) {
found = true;
// Once we've found "c", do an extra check
if (outF.getName().equals("c")) {
assertWithMessage("Flag " + outF + "was not returned with an override")
.that(outF.getValue()).isEqualTo("true");
}
break;
}
}
assertWithMessage("Failed to find input flag " + inpF + " in the output")
.that(found).isTrue();
}
}
@Test
public void testSyncFlags_twoCallsWithDifferentDefaults() {
List<SyncableFlag> inputFlagsFirst = List.of(
new SyncableFlag(NS, "a", "false", false)
);
List<SyncableFlag> inputFlagsSecond = List.of(
new SyncableFlag(NS, "a", "true", false),
new SyncableFlag(NS, "b", "false", false)
);
List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.syncFlags(inputFlagsFirst);
List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.syncFlags(inputFlagsSecond);
assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size());
assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size());
// This test only cares that the "a" flag passed in the second time came out with the
// same value that was passed in the first time.
boolean found = false;
for (SyncableFlag second : outputFlagsSecond) {
if (compareSyncableFlagsNames(second, inputFlagsFirst.get(0))) {
found = true;
assertThat(second.getValue()).isEqualTo(inputFlagsFirst.get(0).getValue());
break;
}
}
assertWithMessage(
"Failed to find flag " + inputFlagsFirst.get(0) + " in the second calls output")
.that(found).isTrue();
}
@Test
public void testQueryFlags_onlyOnce() {
List<SyncableFlag> inputFlags = List.of(
new SyncableFlag(NS, "a", "false", false),
new SyncableFlag(NS, "b", "true", false),
new SyncableFlag(NS, "c", "false", false)
);
List<SyncableFlag> outputFlags = mFeatureFlagsService.queryFlags(inputFlags);
assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
for (SyncableFlag inpF: inputFlags) {
boolean found = false;
for (SyncableFlag outF : outputFlags) {
if (compareSyncableFlagsNames(inpF, outF)) {
found = true;
break;
}
}
assertWithMessage("Failed to find input flag " + inpF + " in the output")
.that(found).isTrue();
}
}
@Test
public void testQueryFlags_twoCallsWithDifferentDefaults() {
List<SyncableFlag> inputFlagsFirst = List.of(
new SyncableFlag(NS, "a", "false", false)
);
List<SyncableFlag> inputFlagsSecond = List.of(
new SyncableFlag(NS, "a", "true", false),
new SyncableFlag(NS, "b", "false", false)
);
List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.queryFlags(inputFlagsFirst);
List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.queryFlags(inputFlagsSecond);
assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size());
assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size());
// This test only cares that the "a" flag passed in the second time came out with the
// same value that was passed in (i.e. it wasn't cached).
boolean found = false;
for (SyncableFlag second : outputFlagsSecond) {
if (compareSyncableFlagsNames(second, inputFlagsSecond.get(0))) {
found = true;
assertThat(second.getValue()).isEqualTo(inputFlagsSecond.get(0).getValue());
break;
}
}
assertWithMessage(
"Failed to find flag " + inputFlagsSecond.get(0) + " in the second calls output")
.that(found).isTrue();
}
@Test
public void testOverrideFlag() {
SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
mFeatureFlagsService.overrideFlag(f);
verify(mFlagStore).set(f.getNamespace(), f.getName(), f.getValue());
}
@Test
public void testResetFlag() {
SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
mFeatureFlagsService.resetFlag(f);
verify(mFlagStore).erase(f.getNamespace(), f.getName());
}
private static boolean compareSyncableFlagsNames(SyncableFlag a, SyncableFlag b) {
return a.getNamespace().equals(b.getNamespace())
&& a.getName().equals(b.getName())
&& a.isDynamic() == b.isDynamic();
}
}

View File

@@ -1,86 +0,0 @@
/*
* 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.server.flags;
import static com.google.common.truth.Truth.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class FlagCacheTest {
private static final String NS = "ns";
private static final String NAME = "name";
FlagCache mFlagCache = new FlagCache();
@Test
public void testGetOrNull_unset() {
assertThat(mFlagCache.getOrNull(NS, NAME)).isNull();
}
@Test
public void testGetOrSet_unset() {
assertThat(mFlagCache.getOrSet(NS, NAME, "value")).isEqualTo("value");
}
@Test
public void testGetOrSet_alreadySet() {
mFlagCache.setIfChanged(NS, NAME, "value");
assertThat(mFlagCache.getOrSet(NS, NAME, "newvalue")).isEqualTo("value");
}
@Test
public void testSetIfChanged_unset() {
assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isTrue();
}
@Test
public void testSetIfChanged_noChange() {
mFlagCache.setIfChanged(NS, NAME, "value");
assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isFalse();
}
@Test
public void testSetIfChanged_changing() {
mFlagCache.setIfChanged(NS, NAME, "value");
assertThat(mFlagCache.setIfChanged(NS, NAME, "newvalue")).isTrue();
}
@Test
public void testContainsNamespace_unset() {
assertThat(mFlagCache.containsNamespace(NS)).isFalse();
}
@Test
public void testContainsNamespace_set() {
mFlagCache.setIfChanged(NS, NAME, "value");
assertThat(mFlagCache.containsNamespace(NS)).isTrue();
}
@Test
public void testContains_unset() {
assertThat(mFlagCache.contains(NS, NAME)).isFalse();
}
@Test
public void testContains_set() {
mFlagCache.setIfChanged(NS, NAME, "value");
assertThat(mFlagCache.contains(NS, NAME)).isTrue();
}
}

View File

@@ -1,111 +0,0 @@
/*
* 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.server.flags;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@Presubmit
@SmallTest
public class FlagOverrideStoreTest {
private static final String NS = "ns";
private static final String NAME = "name";
private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME);
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@Mock
private SettingsProxy mSettingsProxy;
@Mock
private FlagOverrideStore.FlagChangeCallback mCallback;
private FlagOverrideStore mFlagStore;
@Before
public void setup() {
mFlagStore = new FlagOverrideStore(mSettingsProxy);
mFlagStore.setChangeCallback(mCallback);
}
@Test
public void testSet_unset() {
mFlagStore.set(NS, NAME, "value");
verify(mSettingsProxy).putString(PROP_NAME, "value");
}
@Test
public void testSet_setTwice() {
mFlagStore.set(NS, NAME, "value");
mFlagStore.set(NS, NAME, "newvalue");
verify(mSettingsProxy).putString(PROP_NAME, "value");
verify(mSettingsProxy).putString(PROP_NAME, "newvalue");
}
@Test
public void testGet_unset() {
assertThat(mFlagStore.get(NS, NAME)).isNull();
}
@Test
public void testGet_set() {
when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value");
assertThat(mFlagStore.get(NS, NAME)).isEqualTo("value");
}
@Test
public void testErase() {
mFlagStore.erase(NS, NAME);
verify(mSettingsProxy).putString(PROP_NAME, null);
}
@Test
public void testContains_unset() {
assertThat(mFlagStore.contains(NS, NAME)).isFalse();
}
@Test
public void testContains_set() {
when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value");
assertThat(mFlagStore.contains(NS, NAME)).isTrue();
}
@Test
public void testCallback_onSet() {
mFlagStore.set(NS, NAME, "value");
verify(mCallback).onFlagChanged(NS, NAME, "value");
}
@Test
public void testCallback_onErase() {
mFlagStore.erase(NS, NAME);
verify(mCallback).onFlagChanged(NS, NAME, null);
}
}

View File

@@ -1 +0,0 @@
include /services/flags/OWNERS