blob: 322a52bf01cc95f0b7c7ac6356377c11686b28e7 [file] [log] [blame]
/*
* 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);
}
}
}
}