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:
@@ -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"],
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
65
core/java/android/flags/IFeatureFlags.aidl
Normal file
65
core/java/android/flags/IFeatureFlags.aidl
Normal 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);
|
||||
}
|
||||
31
core/java/android/flags/IFeatureFlagsCallback.aidl
Normal file
31
core/java/android/flags/IFeatureFlagsCallback.aidl
Normal 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);
|
||||
}
|
||||
22
core/java/android/flags/SyncableFlag.aidl
Normal file
22
core/java/android/flags/SyncableFlag.aidl
Normal 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;
|
||||
99
core/java/android/flags/SyncableFlag.java
Normal file
99
core/java/android/flags/SyncableFlag.java
Normal 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() + "]";
|
||||
}
|
||||
}
|
||||
@@ -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
18
services/flags/Android.bp
Normal 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
6
services/flags/OWNERS
Normal file
@@ -0,0 +1,6 @@
|
||||
# Bug component: 1306523
|
||||
|
||||
mankoff@google.com
|
||||
|
||||
pixel@google.com
|
||||
dsandler@android.com
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
103
services/flags/java/com/android/server/flags/FlagCache.java
Normal file
103
services/flags/java/com/android/server/flags/FlagCache.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
381
services/flags/java/com/android/server/flags/SettingsProxy.java
Normal file
381
services/flags/java/com/android/server/flags/SettingsProxy.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -34,6 +34,7 @@ android_test {
|
||||
"services.core",
|
||||
"services.credentials",
|
||||
"services.devicepolicy",
|
||||
"services.flags",
|
||||
"services.net",
|
||||
"services.people",
|
||||
"services.usage",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
include /services/flags/OWNERS
|
||||
Reference in New Issue
Block a user