Define FeatureFlagsService.

This service acts as a central coordinator to ensure that all
cross-process, stable flags maintain the same value, as well as
providing a central mechanism for overriding flags locally on a
device.

This cl contains a the service wholly defined and functional, though
the FeatureFlags class in core, meant to communicate with it, is not
yet implemented.

See http://go/android-flagging-dd for more details.

Test: build && flash && `adb shell cmd flags`.
Bug: 279054964
Change-Id: I930152682f98f3f299e763522e5c7e6b1a550e32
This commit is contained in:
Dave Mankoff
2023-03-29 19:28:25 +00:00
parent 27f4ca52a9
commit 4f7ccd42f1
23 changed files with 1947 additions and 0 deletions

View File

@@ -33,6 +33,15 @@ 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,6 +5297,13 @@ 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

@@ -0,0 +1,65 @@
/*
* 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);
}

View File

@@ -0,0 +1,31 @@
/*
* 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

@@ -0,0 +1,22 @@
/*
* 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

@@ -0,0 +1,99 @@
/*
* 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;
import com.android.internal.annotations.VisibleForTesting;
/**
* @hide
*/
public final class SyncableFlag implements Parcelable {
private final String mNamespace;
private final String mName;
private String mValue;
private final boolean mDynamic;
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public SyncableFlag(
@NonNull String namespace,
@NonNull String name,
@NonNull String value,
boolean dynamic) {
mNamespace = namespace;
mName = name;
mValue = value;
mDynamic = dynamic;
}
public void setValue(@NonNull String value) {
mValue = value;
}
@NonNull
public String getNamespace() {
return mNamespace;
}
@NonNull
public String getName() {
return mName;
}
@NonNull
public String getValue() {
return mValue;
}
@NonNull
public boolean isDynamic() {
return mDynamic;
}
@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());
}
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);
}
@Override
public String toString() {
return getNamespace() + "." + getName() + "[" + getValue() + "]";
}
}

View File

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

18
services/flags/Android.bp Normal file
View File

@@ -0,0 +1,18 @@
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"],
}

6
services/flags/OWNERS Normal file
View File

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

View File

@@ -0,0 +1,273 @@
/*
* 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);
}
void syncDynamicFlag(int pid, SyncableFlag sf) {
if (!sf.isDynamic()) {
return;
}
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.
// If we already have a value cached, just use that.
String value = null;
DynamicFlagData data = mDynamicFlags.getOrNull(ns, name);
if (data != null) {
value = data.getValue();
} else {
// Put the value in the cache for future reference.
data = new DynamicFlagData(ns, name);
mDynamicFlags.setIfChanged(ns, 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(ns, 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(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());
sf.setValue(value);
}
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);
}
}
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

@@ -0,0 +1,86 @@
/*
* 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 java.io.FileOutputStream;
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;
FeatureFlagsBinder(FlagOverrideStore flagStore, FlagsShellCommand shellCommand) {
mFlagStore = flagStore;
mShellCommand = shellCommand;
mDynamicFlagDelegate = new DynamicFlagBinderDelegate(flagStore);
}
@Override
public void registerCallback(IFeatureFlagsCallback callback) {
mDynamicFlagDelegate.registerCallback(getCallingPid(), callback);
}
@Override
public void unregisterCallback(IFeatureFlagsCallback callback) {
mDynamicFlagDelegate.unregisterCallback(getCallingPid(), callback);
}
@Override
public List<SyncableFlag> syncFlags(List<SyncableFlag> incomingFlags) {
int pid = getCallingPid();
for (SyncableFlag sf : incomingFlags) {
String ns = sf.getNamespace();
String name = sf.getName();
if (sf.isDynamic()) {
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);
}
sf.setValue(value);
}
}
}
return incomingFlags;
}
@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

@@ -0,0 +1,59 @@
/*
* 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.Context;
import android.util.Slog;
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");
publishBinderService(
Context.FEATURE_FLAGS_SERVICE, new FeatureFlagsBinder(mFlagStore, mShellCommand));
}
}

View File

@@ -0,0 +1,103 @@
/*
* 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

@@ -0,0 +1,120 @@
/*
* 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. */
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. */
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

@@ -0,0 +1,214 @@
/*
* 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

@@ -0,0 +1,68 @@
/*
* 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

@@ -0,0 +1,381 @@
/*
* 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,6 +132,7 @@ 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;
@@ -1111,6 +1112,12 @@ 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,6 +34,7 @@ android_test {
"services.core",
"services.credentials",
"services.devicepolicy",
"services.flags",
"services.net",
"services.people",
"services.usage",

View File

@@ -0,0 +1,179 @@
/*
* 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.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;
private FeatureFlagsBinder mFeatureFlagsService;
@Before
public void setup() {
when(mIFeatureFlagsCallback.asBinder()).thenReturn(mIFeatureFlagsCallbackAsBinder);
mFeatureFlagsService = new FeatureFlagsBinder(mFlagStore, mFlagCommand);
}
@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 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();
}
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

@@ -0,0 +1,86 @@
/*
* 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

@@ -0,0 +1,111 @@
/*
* 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

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