/*
* Copyright (C) 2015 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.providers.settings;
import static android.os.Process.FIRST_APPLICATION_UID;
import android.aconfig.Aconfig.flag_permission;
import android.aconfig.Aconfig.flag_state;
import android.aconfig.Aconfig.parsed_flag;
import android.aconfig.Aconfig.parsed_flags;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Build;
import android.os.FileUtils;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.providers.settings.SettingsOperationProto;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.Base64;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.Xml;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FrameworkStatsLog;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
// FOR ACONFIGD TEST MISSION AND ROLLOUT
import java.io.DataInputStream;
import java.io.DataOutputStream;
import android.util.proto.ProtoInputStream;
import android.aconfigd.Aconfigd.StorageRequestMessage;
import android.aconfigd.Aconfigd.StorageRequestMessages;
import android.aconfigd.Aconfigd.StorageReturnMessage;
import android.aconfigd.Aconfigd.StorageReturnMessages;
import android.aconfigd.AconfigdClientSocket;
import android.aconfigd.AconfigdFlagInfo;
import android.aconfigd.AconfigdJavaUtils;
import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon;
/**
* This class contains the state for one type of settings. It is responsible
* for saving the state asynchronously to an XML file after a mutation and
* loading the from an XML file on construction.
*
* This class uses the same lock as the settings provider to ensure that
* multiple changes made by the settings provider, e,g, upgrade, bulk insert,
* etc, are atomically persisted since the asynchronous persistence is using
* the same lock to grab the current state to write to disk.
*
*/
final class SettingsState {
private static final boolean DEBUG = false;
private static final boolean DEBUG_PERSISTENCE = false;
private static final String LOG_TAG = "SettingsState";
static final String SYSTEM_PACKAGE_NAME = "android";
static final int SETTINGS_VERSION_NEW_ENCODING = 121;
// LINT.IfChange
public static final int MAX_LENGTH_PER_STRING = 32768;
// LINT.ThenChange(/services/core/java/com/android/server/audio/AudioDeviceInventory.java:settings_max_length_per_string)
private static final long WRITE_SETTINGS_DELAY_MILLIS = 200;
private static final long MAX_WRITE_SETTINGS_DELAY_MILLIS = 2000;
public static final int MAX_BYTES_PER_APP_PACKAGE_UNLIMITED = -1;
public static final int MAX_BYTES_PER_APP_PACKAGE_LIMITED = 40000;
public static final int VERSION_UNDEFINED = -1;
public static final String FALLBACK_FILE_SUFFIX = ".fallback";
private static final String TAG_SETTINGS = "settings";
private static final String TAG_SETTING = "setting";
private static final String ATTR_PACKAGE = "package";
private static final String ATTR_DEFAULT_SYS_SET = "defaultSysSet";
private static final String ATTR_TAG = "tag";
private static final String ATTR_TAG_BASE64 = "tagBase64";
private static final String ATTR_VERSION = "version";
private static final String ATTR_ID = "id";
private static final String ATTR_NAME = "name";
private static final String TAG_NAMESPACE_HASHES = "namespaceHashes";
private static final String TAG_NAMESPACE_HASH = "namespaceHash";
private static final String ATTR_NAMESPACE = "namespace";
private static final String ATTR_BANNED_HASH = "bannedHash";
private static final String ATTR_PRESERVE_IN_RESTORE = "preserve_in_restore";
/**
* Non-binary value will be written in this attributes.
*/
private static final String ATTR_VALUE = "value";
private static final String ATTR_DEFAULT_VALUE = "defaultValue";
/**
* KXmlSerializer won't like some characters. We encode such characters
* in base64 and store in this attribute.
* NOTE: A null value will have *neither* ATTR_VALUE nor ATTR_VALUE_BASE64.
*/
private static final String ATTR_VALUE_BASE64 = "valueBase64";
private static final String ATTR_DEFAULT_VALUE_BASE64 = "defaultValueBase64";
/**
* In the config table, there are special flags of the form {@code staged/namespace*flagName}.
* On boot, when the XML file is initially parsed, these transform into
* {@code namespace/flagName}, and the special staged flags are deleted.
*/
private static final String CONFIG_STAGED_PREFIX = "staged/";
private static final List sAconfigTextProtoFilesOnDevice = List.of(
"/system/etc/aconfig_flags.pb",
"/system_ext/etc/aconfig_flags.pb",
"/product/etc/aconfig_flags.pb",
"/vendor/etc/aconfig_flags.pb");
private static final String APEX_DIR = "/apex";
private static final String APEX_ACONFIG_PATH_SUFFIX = "/etc/aconfig_flags.pb";
private static final String STORAGE_MIGRATION_FLAG =
"core_experiments_team_internal/com.android.providers.settings.storage_test_mission_1";
private static final String STORAGE_MIGRATION_MARKER_FILE =
"/metadata/aconfig_test_missions/mission_1";
/**
* This tag is applied to all aconfig default value-loaded flags.
*/
private static final String BOOT_LOADED_DEFAULT_TAG = "BOOT_LOADED_DEFAULT";
// This was used in version 120 and before.
private static final String NULL_VALUE_OLD_STYLE = "null";
private static final int HISTORICAL_OPERATION_COUNT = 20;
private static final String HISTORICAL_OPERATION_UPDATE = "update";
private static final String HISTORICAL_OPERATION_DELETE = "delete";
private static final String HISTORICAL_OPERATION_PERSIST = "persist";
private static final String HISTORICAL_OPERATION_INITIALIZE = "initialize";
private static final String HISTORICAL_OPERATION_RESET = "reset";
private static final String SHELL_PACKAGE_NAME = "com.android.shell";
private static final String ROOT_PACKAGE_NAME = "root";
private static final String NULL_VALUE = "null";
private static final ArraySet sSystemPackages = new ArraySet<>();
private final Object mWriteLock = new Object();
private final Object mLock;
private final Handler mHandler;
@GuardedBy("mLock")
private final Context mContext;
@GuardedBy("mLock")
private final ArrayMap mSettings = new ArrayMap<>();
@GuardedBy("mLock")
private final ArrayMap mNamespaceBannedHashes = new ArrayMap<>();
@GuardedBy("mLock")
private final ArrayMap mPackageToMemoryUsage;
@GuardedBy("mLock")
private final int mMaxBytesPerAppPackage;
@GuardedBy("mLock")
private final File mStatePersistFile;
@GuardedBy("mLock")
private final String mStatePersistTag;
private final Setting mNullSetting = new Setting(null, null, false, null, null) {
@Override
public boolean isNull() {
return true;
}
};
@GuardedBy("mLock")
private final List mHistoricalOperations;
@GuardedBy("mLock")
public final int mKey;
@GuardedBy("mLock")
private int mVersion = VERSION_UNDEFINED;
@GuardedBy("mLock")
private long mLastNotWrittenMutationTimeMillis;
@GuardedBy("mLock")
private boolean mDirty;
@GuardedBy("mLock")
private boolean mWriteScheduled;
@GuardedBy("mLock")
private long mNextId;
@GuardedBy("mLock")
private int mNextHistoricalOpIdx;
@GuardedBy("mLock")
@NonNull
private Map> mNamespaceDefaults;
// TOBO(b/312444587): remove the comparison logic after Test Mission 2.
@NonNull
private Map mAconfigDefaultFlags;
public static final int SETTINGS_TYPE_GLOBAL = 0;
public static final int SETTINGS_TYPE_SYSTEM = 1;
public static final int SETTINGS_TYPE_SECURE = 2;
public static final int SETTINGS_TYPE_SSAID = 3;
public static final int SETTINGS_TYPE_CONFIG = 4;
public static final int SETTINGS_TYPE_MASK = 0xF0000000;
public static final int SETTINGS_TYPE_SHIFT = 28;
public static int makeKey(int type, int userId) {
return (type << SETTINGS_TYPE_SHIFT) | userId;
}
public static int getTypeFromKey(int key) {
return key >>> SETTINGS_TYPE_SHIFT;
}
public static int getUserIdFromKey(int key) {
return key & ~SETTINGS_TYPE_MASK;
}
public static String settingTypeToString(int type) {
switch (type) {
case SETTINGS_TYPE_CONFIG: {
return "SETTINGS_CONFIG";
}
case SETTINGS_TYPE_GLOBAL: {
return "SETTINGS_GLOBAL";
}
case SETTINGS_TYPE_SECURE: {
return "SETTINGS_SECURE";
}
case SETTINGS_TYPE_SYSTEM: {
return "SETTINGS_SYSTEM";
}
case SETTINGS_TYPE_SSAID: {
return "SETTINGS_SSAID";
}
default: {
return "UNKNOWN";
}
}
}
public static boolean isConfigSettingsKey(int key) {
return getTypeFromKey(key) == SETTINGS_TYPE_CONFIG;
}
public static boolean isGlobalSettingsKey(int key) {
return getTypeFromKey(key) == SETTINGS_TYPE_GLOBAL;
}
public static boolean isSystemSettingsKey(int key) {
return getTypeFromKey(key) == SETTINGS_TYPE_SYSTEM;
}
public static boolean isSecureSettingsKey(int key) {
return getTypeFromKey(key) == SETTINGS_TYPE_SECURE;
}
public static boolean isSsaidSettingsKey(int key) {
return getTypeFromKey(key) == SETTINGS_TYPE_SSAID;
}
public static String keyToString(int key) {
return "Key[user=" + getUserIdFromKey(key) + ";type="
+ settingTypeToString(getTypeFromKey(key)) + "]";
}
public SettingsState(
Context context,
Object lock,
File file,
int key,
int maxBytesPerAppPackage,
Looper looper) {
// It is important that we use the same lock as the settings provider
// to ensure multiple mutations on this state are atomically persisted
// as the async persistence should be blocked while we make changes.
mContext = context;
mLock = lock;
mStatePersistFile = file;
mStatePersistTag = "settings-" + getTypeFromKey(key) + "-" + getUserIdFromKey(key);
mKey = key;
mHandler = new MyHandler(looper);
if (maxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_LIMITED) {
mMaxBytesPerAppPackage = maxBytesPerAppPackage;
mPackageToMemoryUsage = new ArrayMap<>();
} else {
mMaxBytesPerAppPackage = maxBytesPerAppPackage;
mPackageToMemoryUsage = null;
}
mHistoricalOperations =
Build.IS_DEBUGGABLE ? new ArrayList<>(HISTORICAL_OPERATION_COUNT) : null;
mNamespaceDefaults = new HashMap<>();
mAconfigDefaultFlags = new HashMap<>();
ProtoOutputStream requests = null;
synchronized (mLock) {
readStateSyncLocked();
if (Flags.loadAconfigDefaults()) {
if (isConfigSettingsKey(mKey)) {
loadAconfigDefaultValuesLocked(sAconfigTextProtoFilesOnDevice);
}
}
if (Flags.loadApexAconfigProtobufs()) {
if (isConfigSettingsKey(mKey)) {
List apexProtoPaths = listApexProtoPaths();
loadAconfigDefaultValuesLocked(apexProtoPaths);
}
}
if (enableAconfigStorageDaemon()) {
if (isConfigSettingsKey(mKey)) {
getAllAconfigFlagsFromSettings(mAconfigDefaultFlags);
}
}
if (isConfigSettingsKey(mKey)) {
requests = handleBulkSyncToNewStorage(mAconfigDefaultFlags);
}
}
if (enableAconfigStorageDaemon()) {
if (isConfigSettingsKey(mKey)){
AconfigdClientSocket localSocket = AconfigdJavaUtils.getAconfigdClientSocket();
if (requests != null) {
InputStream res = localSocket.send(requests.getBytes());
if (res == null) {
Slog.w(LOG_TAG, "Bulk sync request to acongid failed.");
}
}
// TOBO(b/312444587): remove the comparison logic after Test Mission 2.
if (mSettings.get("aconfigd_marker/bulk_synced").value.equals("true")
&& requests == null) {
Map aconfigdFlagMap =
AconfigdJavaUtils.listFlagsValueInNewStorage(localSocket);
compareFlagValueInNewStorage(
mAconfigDefaultFlags,
aconfigdFlagMap);
}
}
}
}
// TOBO(b/312444587): remove the comparison logic after Test Mission 2.
public int compareFlagValueInNewStorage(
Map defaultFlagMap,
Map aconfigdFlagMap) {
// Get all defaults from the default map. The mSettings may not contain
// all flags, since it only contains updated flags.
int diffNum = 0;
for (Map.Entry entry : defaultFlagMap.entrySet()) {
String key = entry.getKey();
AconfigdFlagInfo flag = entry.getValue();
AconfigdFlagInfo aconfigdFlag = aconfigdFlagMap.get(key);
if (aconfigdFlag == null) {
Slog.w(LOG_TAG, String.format("Flag %s is missing from aconfigd", key));
diffNum++;
continue;
}
String diff = flag.dumpDiff(aconfigdFlag);
if (!diff.isEmpty()) {
Slog.w(
LOG_TAG,
String.format(
"Flag %s is different in Settings and aconfig: %s", key, diff));
diffNum++;
}
}
for (String key : aconfigdFlagMap.keySet()) {
if (defaultFlagMap.containsKey(key)) continue;
Slog.w(LOG_TAG, String.format("Flag %s is missing from Settings", key));
diffNum++;
}
String compareMarkerName = "aconfigd_marker/compare_diff_num";
synchronized (mLock) {
Setting markerSetting = mSettings.get(compareMarkerName);
if (markerSetting == null) {
markerSetting =
new Setting(
compareMarkerName,
String.valueOf(diffNum),
false,
"aconfig",
"aconfig");
mSettings.put(compareMarkerName, markerSetting);
}
markerSetting.value = String.valueOf(diffNum);
}
if (diffNum == 0) {
Slog.w(LOG_TAG, "Settings and new storage have same flags.");
}
return diffNum;
}
@GuardedBy("mLock")
public int getAllAconfigFlagsFromSettings(
@NonNull Map flagInfoDefault) {
Map ret = new HashMap<>();
int numSettings = mSettings.size();
int num_requests = 0;
for (int i = 0; i < numSettings; i++) {
String name = mSettings.keyAt(i);
Setting setting = mSettings.valueAt(i);
AconfigdFlagInfo flag =
getFlagOverrideToSync(name, setting.getValue(), flagInfoDefault);
if (flag == null) {
continue;
}
if (flag.getIsReadWrite()) {
++num_requests;
}
}
Slog.i(LOG_TAG, num_requests + " flag override requests created");
return num_requests;
}
// TODO(b/341764371): migrate aconfig flag push to GMS core
@VisibleForTesting
@GuardedBy("mLock")
@Nullable
public AconfigdFlagInfo getFlagOverrideToSync(
String name, String value, @NonNull Map flagInfoDefault) {
int slashIdx = name.indexOf("/");
if (slashIdx <= 0 || slashIdx >= name.length() - 1) {
Slog.e(LOG_TAG, "invalid flag name " + name);
return null;
}
String namespace = name.substring(0, slashIdx);
String fullFlagName = name.substring(slashIdx + 1);
boolean isLocal = false;
// get actual fully qualified flag name ., note this is done
// after staged flag is applied, so no need to check staged flags
if (namespace.equals("device_config_overrides")) {
int colonIdx = fullFlagName.indexOf(":");
if (colonIdx == -1) {
Slog.e(LOG_TAG, "invalid local override flag name " + name);
return null;
}
namespace = fullFlagName.substring(0, colonIdx);
fullFlagName = fullFlagName.substring(colonIdx + 1);
isLocal = true;
}
// get package name and flag name
int dotIdx = fullFlagName.lastIndexOf(".");
if (dotIdx == -1) {
Slog.e(LOG_TAG, "invalid override flag name " + name);
return null;
}
AconfigdFlagInfo flag = flagInfoDefault.get(fullFlagName);
if (flag == null) {
return null;
}
if (isLocal) {
flag.setLocalFlagValue(value);
} else {
flag.setServerFlagValue(value);
}
return flag;
}
// TODO(b/341764371): migrate aconfig flag push to GMS core
@VisibleForTesting
@GuardedBy("mLock")
public ProtoOutputStream handleBulkSyncToNewStorage(
Map aconfigFlagMap) {
// get marker or add marker if it does not exist
final String bulkSyncMarkerName = new String("aconfigd_marker/bulk_synced");
Setting markerSetting = mSettings.get(bulkSyncMarkerName);
if (markerSetting == null) {
markerSetting = new Setting(bulkSyncMarkerName, "false", false, "aconfig", "aconfig");
mSettings.put(bulkSyncMarkerName, markerSetting);
}
if (enableAconfigStorageDaemon()) {
if (markerSetting.value.equals("true")) {
// CASE 1, flag is on, bulk sync marker true, nothing to do
return null;
} else {
// CASE 2, flag is on, bulk sync marker false. Do following two tasks
// (1) Do bulk sync here.
// (2) After bulk sync, set marker to true.
// first add storage reset request
ProtoOutputStream requests = new ProtoOutputStream();
AconfigdJavaUtils.writeResetStorageRequest(requests);
// loop over all settings and add flag override requests
for (AconfigdFlagInfo flag : aconfigFlagMap.values()) {
// don't sync read_only flags
if (!flag.getIsReadWrite()) {
continue;
}
if (flag.getHasServerOverride()) {
AconfigdJavaUtils.writeFlagOverrideRequest(
requests,
flag.getPackageName(),
flag.getFlagName(),
flag.getServerFlagValue(),
false);
}
if (flag.getHasLocalOverride()) {
AconfigdJavaUtils.writeFlagOverrideRequest(
requests,
flag.getPackageName(),
flag.getFlagName(),
flag.getLocalFlagValue(),
true);
}
}
// mark sync has been done
markerSetting.value = "true";
scheduleWriteIfNeededLocked();
return requests;
}
} else {
if (markerSetting.value.equals("true")) {
// CASE 3, flag is off, bulk sync marker true, clear the marker
markerSetting.value = "false";
scheduleWriteIfNeededLocked();
return null;
} else {
// CASE 4, flag is off, bulk sync marker false, nothing to do
return null;
}
}
}
@GuardedBy("mLock")
private void loadAconfigDefaultValuesLocked(List filePaths) {
for (String fileName : filePaths) {
try (FileInputStream inputStream = new FileInputStream(fileName)) {
loadAconfigDefaultValues(
inputStream.readAllBytes(), mNamespaceDefaults, mAconfigDefaultFlags);
} catch (IOException e) {
Slog.e(LOG_TAG, "failed to read protobuf", e);
}
}
}
private List listApexProtoPaths() {
LinkedList paths = new LinkedList();
File apexDirectory = new File(APEX_DIR);
if (!apexDirectory.isDirectory()) {
return paths;
}
File[] subdirs = apexDirectory.listFiles();
if (subdirs == null) {
return paths;
}
for (File prefix : subdirs) {
// For each mainline modules, there are two directories, one /,
// and one @/. Just read the former.
if (prefix.getAbsolutePath().contains("@")) {
continue;
}
File protoPath = new File(prefix + APEX_ACONFIG_PATH_SUFFIX);
if (!protoPath.exists()) {
continue;
}
paths.add(protoPath.getAbsolutePath());
}
return paths;
}
@VisibleForTesting
@GuardedBy("mLock")
public void addAconfigDefaultValuesFromMap(
@NonNull Map> defaultMap) {
mNamespaceDefaults.putAll(defaultMap);
}
@VisibleForTesting
@GuardedBy("mLock")
public static void loadAconfigDefaultValues(
byte[] fileContents,
@NonNull Map> defaultMap,
@NonNull Map flagInfoDefault) {
try {
parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents);
for (parsed_flag flag : parsedFlags.getParsedFlagList()) {
if (!defaultMap.containsKey(flag.getNamespace())) {
Map defaults = new HashMap<>();
defaultMap.put(flag.getNamespace(), defaults);
}
String fullFlagName = flag.getPackage() + "." + flag.getName();
String flagName = flag.getNamespace() + "/" + fullFlagName;
String flagValue = flag.getState() == flag_state.ENABLED ? "true" : "false";
boolean isReadWrite = flag.getPermission() == flag_permission.READ_WRITE;
defaultMap.get(flag.getNamespace()).put(flagName, flagValue);
if (!flagInfoDefault.containsKey(fullFlagName)) {
flagInfoDefault.put(
fullFlagName,
AconfigdFlagInfo.newBuilder()
.setPackageName(flag.getPackage())
.setFlagName(flag.getName())
.setDefaultFlagValue(flagValue)
.setIsReadWrite(isReadWrite)
.build());
}
}
} catch (IOException e) {
Slog.e(LOG_TAG, "failed to parse protobuf", e);
}
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public int getVersionLocked() {
return mVersion;
}
public Setting getNullSetting() {
return mNullSetting;
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public void setVersionLocked(int version) {
if (version == mVersion) {
return;
}
mVersion = version;
scheduleWriteIfNeededLocked();
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public void removeSettingsForPackageLocked(String packageName) {
boolean removedSomething = false;
final int settingCount = mSettings.size();
for (int i = settingCount - 1; i >= 0; i--) {
String name = mSettings.keyAt(i);
// Settings defined by us are never dropped.
if (Settings.System.PUBLIC_SETTINGS.contains(name)
|| Settings.System.PRIVATE_SETTINGS.contains(name)) {
continue;
}
Setting setting = mSettings.valueAt(i);
if (packageName.equals(setting.packageName)) {
mSettings.removeAt(i);
removedSomething = true;
}
}
if (removedSomething) {
scheduleWriteIfNeededLocked();
}
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public List getSettingNamesLocked() {
ArrayList names = new ArrayList<>();
final int settingsCount = mSettings.size();
for (int i = 0; i < settingsCount; i++) {
String name = mSettings.keyAt(i);
names.add(name);
}
return names;
}
@NonNull
public Map> getAconfigDefaultValues() {
synchronized (mLock) {
return mNamespaceDefaults;
}
}
@NonNull
public Map getAconfigDefaultFlags() {
synchronized (mLock) {
return mAconfigDefaultFlags;
}
}
// The settings provider must hold its lock when calling here.
public Setting getSettingLocked(String name) {
if (TextUtils.isEmpty(name)) {
return mNullSetting;
}
Setting setting = mSettings.get(name);
if (setting != null) {
return new Setting(setting);
}
return mNullSetting;
}
// The settings provider must hold its lock when calling here.
public boolean updateSettingLocked(String name, String value, String tag,
boolean makeValue, String packageName) {
if (!hasSettingLocked(name)) {
return false;
}
return insertSettingLocked(name, value, tag, makeValue, packageName);
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public void resetSettingDefaultValueLocked(String name) {
Setting oldSetting = getSettingLocked(name);
if (oldSetting != null && !oldSetting.isNull() && oldSetting.getDefaultValue() != null) {
String oldValue = oldSetting.getValue();
String oldDefaultValue = oldSetting.getDefaultValue();
Setting newSetting = new Setting(name, oldSetting.getValue(), null,
oldSetting.getPackageName(), oldSetting.getTag(), false,
oldSetting.getId());
int newSize = getNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), 0,
oldValue, newSetting.getValue(), oldDefaultValue, newSetting.getDefaultValue());
checkNewMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize);
mSettings.put(name, newSetting);
updateMemoryUsagePerPackageLocked(newSetting.getPackageName(), newSize);
scheduleWriteIfNeededLocked();
}
}
// The settings provider must hold its lock when calling here.
public boolean insertSettingOverrideableByRestoreLocked(String name, String value, String tag,
boolean makeDefault, String packageName) {
return insertSettingLocked(name, value, tag, makeDefault, false, packageName,
/* overrideableByRestore */ true);
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public boolean insertSettingLocked(String name, String value, String tag,
boolean makeDefault, String packageName) {
return insertSettingLocked(name, value, tag, makeDefault, false, packageName,
/* overrideableByRestore */ false);
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public boolean insertSettingLocked(String name, String value, String tag,
boolean makeDefault, boolean forceNonSystemPackage, String packageName,
boolean overrideableByRestore) {
if (TextUtils.isEmpty(name)) {
return false;
}
// Aconfig flags are always boot stable, so we anytime we write one, we stage it to be
// applied on reboot.
if (Flags.stageAllAconfigFlags()) {
int slashIndex = name.indexOf("/");
boolean stageFlag = isConfigSettingsKey(mKey)
&& slashIndex != -1
&& slashIndex != 0
&& slashIndex != name.length();
if (stageFlag) {
String namespace = name.substring(0, slashIndex);
String flag = name.substring(slashIndex + 1);
boolean isAconfig = mNamespaceDefaults.containsKey(namespace)
&& mNamespaceDefaults.get(namespace).containsKey(name);
if (isAconfig) {
name = "staged/" + namespace + "*" + flag;
}
}
}
final boolean isNameTooLong = name.length() > SettingsState.MAX_LENGTH_PER_STRING;
final boolean isValueTooLong =
value != null && value.length() > SettingsState.MAX_LENGTH_PER_STRING;
if (isNameTooLong || isValueTooLong) {
// only print the first few bytes of the name in case it is long
final String errorMessage = "The " + (isNameTooLong ? "name" : "value")
+ " of your setting ["
+ (name.length() > 20 ? (name.substring(0, 20) + "...") : name)
+ "] is too long. The max length allowed for the string is "
+ MAX_LENGTH_PER_STRING + ".";
throw new IllegalArgumentException(errorMessage);
}
Setting oldState = mSettings.get(name);
String previousOwningPackage = (oldState != null) ? oldState.packageName : null;
// If the old state doesn't exist, no need to handle the owning package change
final boolean owningPackageChanged = previousOwningPackage != null
&& !previousOwningPackage.equals(packageName);
String oldValue = (oldState != null) ? oldState.value : null;
String oldDefaultValue = (oldState != null) ? oldState.defaultValue : null;
String newDefaultValue = makeDefault ? value : oldDefaultValue;
int newSizeForCurrentPackage = getNewMemoryUsagePerPackageLocked(packageName,
/* deltaKeyLength= */ (oldState == null || owningPackageChanged) ? name.length() : 0,
/* oldValue= */ owningPackageChanged ? null : oldValue,
/* newValue= */ value,
/* oldDefaultValue= */ owningPackageChanged ? null : oldDefaultValue,
/* newDefaultValue = */ newDefaultValue);
// Only check the memory usage for the current package. Even if the owning package
// has changed, the previous owning package will only have a reduced memory usage, so
// there is no need to check its memory usage.
checkNewMemoryUsagePerPackageLocked(packageName, newSizeForCurrentPackage);
Setting newState;
if (oldState != null) {
if (!oldState.update(value, makeDefault, packageName, tag, forceNonSystemPackage,
overrideableByRestore)) {
return false;
}
newState = oldState;
} else {
newState = new Setting(name, value, makeDefault, packageName, tag,
forceNonSystemPackage);
mSettings.put(name, newState);
}
FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, name, value, newState.value,
oldValue, tag, makeDefault, getUserIdFromKey(mKey),
FrameworkStatsLog.SETTING_CHANGED__REASON__UPDATED);
addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, newState);
updateMemoryUsagePerPackageLocked(packageName, newSizeForCurrentPackage);
if (owningPackageChanged) {
int newSizeForPreviousPackage = getNewMemoryUsagePerPackageLocked(previousOwningPackage,
/* deltaKeyLength= */ -name.length(),
/* oldValue= */ oldValue,
/* newValue= */ null,
/* oldDefaultValue= */ oldDefaultValue,
/* newDefaultValue = */ null);
updateMemoryUsagePerPackageLocked(previousOwningPackage, newSizeForPreviousPackage);
}
scheduleWriteIfNeededLocked();
return true;
}
@GuardedBy("mLock")
public boolean isNewConfigBannedLocked(String prefix, Map keyValues) {
// Replaces old style "null" String values with actual null's. This is done to simulate
// what will happen to String "null" values when they are written to Settings. This needs to
// be done here make sure that config hash computed during is banned check matches the
// one computed during banning when values are already stored.
keyValues = removeNullValueOldStyle(keyValues);
String bannedHash = mNamespaceBannedHashes.get(prefix);
if (bannedHash == null) {
return false;
}
return bannedHash.equals(hashCode(keyValues));
}
@GuardedBy("mLock")
public void unbanAllConfigIfBannedConfigUpdatedLocked(String prefix) {
// If the prefix updated is a banned namespace, clear mNamespaceBannedHashes
// to unban all unbanned namespaces.
if (mNamespaceBannedHashes.get(prefix) != null) {
mNamespaceBannedHashes.clear();
scheduleWriteIfNeededLocked();
}
}
@GuardedBy("mLock")
public void banConfigurationLocked(String prefix, Map keyValues) {
if (prefix == null || keyValues.isEmpty()) {
return;
}
// The write is intentionally not scheduled here, banned hashes should and will be written
// when the related setting changes are written
mNamespaceBannedHashes.put(prefix, hashCode(keyValues));
}
@GuardedBy("mLock")
public Set getAllConfigPrefixesLocked() {
Set prefixSet = new HashSet<>();
final int settingsCount = mSettings.size();
for (int i = 0; i < settingsCount; i++) {
String name = mSettings.keyAt(i);
prefixSet.add(name.split("/")[0] + "/");
}
return prefixSet;
}
// The settings provider must hold its lock when calling here.
// Returns the list of keys which changed (added, updated, or deleted).
@GuardedBy("mLock")
public List setSettingsLocked(String prefix, Map keyValues,
String packageName) {
List changedKeys = new ArrayList<>();
final Iterator> iterator = mSettings.entrySet().iterator();
int index = prefix.lastIndexOf('/');
String namespace = index < 0 ? "" : prefix.substring(0, index);
Map trunkFlagMap = (mNamespaceDefaults == null)
? null : mNamespaceDefaults.get(namespace);
// Delete old keys with the prefix that are not part of the new set.
// trunk flags will not be configured with restricted propagation
// trunk flags will be explicitly set, so not removing them here
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
final String key = entry.getKey();
final Setting oldState = entry.getValue();
if (key != null && (trunkFlagMap == null || !trunkFlagMap.containsKey(key))
&& key.startsWith(prefix) && !keyValues.containsKey(key)) {
iterator.remove();
FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, key,
/* value= */ "", /* newValue= */ "", oldState.value, /* tag */ "", false,
getUserIdFromKey(mKey), FrameworkStatsLog.SETTING_CHANGED__REASON__DELETED);
addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState);
changedKeys.add(key); // key was removed
}
}
// Update/add new keys
for (String key : keyValues.keySet()) {
String value = keyValues.get(key);
// Rename key if it's an aconfig flag.
String flagName = key;
if (Flags.stageAllAconfigFlags() && isConfigSettingsKey(mKey)) {
int slashIndex = flagName.indexOf("/");
boolean stageFlag = slashIndex > 0 && slashIndex != flagName.length();
boolean isAconfig = trunkFlagMap != null && trunkFlagMap.containsKey(flagName);
if (stageFlag && isAconfig) {
String flagWithoutNamespace = flagName.substring(slashIndex + 1);
flagName = "staged/" + namespace + "*" + flagWithoutNamespace;
}
}
String oldValue = null;
Setting state = mSettings.get(flagName);
if (state == null) {
state = new Setting(flagName, value, false, packageName, null);
mSettings.put(flagName, state);
changedKeys.add(flagName); // key was added
} else if (state.value != value) {
oldValue = state.value;
state.update(value, false, packageName, null, true,
/* overrideableByRestore */ false);
changedKeys.add(flagName); // key was updated
} else {
// this key/value already exists, no change and no logging necessary
continue;
}
FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, flagName, value, state.value,
oldValue, /* tag */ null, /* make default */ false,
getUserIdFromKey(mKey), FrameworkStatsLog.SETTING_CHANGED__REASON__UPDATED);
addHistoricalOperationLocked(HISTORICAL_OPERATION_UPDATE, state);
}
if (!changedKeys.isEmpty()) {
scheduleWriteIfNeededLocked();
}
return changedKeys;
}
// The settings provider must hold its lock when calling here.
public void persistSettingsLocked() {
mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
// schedule a write operation right away
mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS).sendToTarget();
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public boolean deleteSettingLocked(String name) {
if (TextUtils.isEmpty(name) || !hasSettingLocked(name)) {
return false;
}
Setting oldState = mSettings.remove(name);
if (oldState == null) {
return false;
}
int newSize = getNewMemoryUsagePerPackageLocked(oldState.packageName,
-name.length() /* deltaKeySize */,
oldState.value, null, oldState.defaultValue, null);
FrameworkStatsLog.write(FrameworkStatsLog.SETTING_CHANGED, name, /* value= */ "",
/* newValue= */ "", oldState.value, /* tag */ "", false, getUserIdFromKey(mKey),
FrameworkStatsLog.SETTING_CHANGED__REASON__DELETED);
updateMemoryUsagePerPackageLocked(oldState.packageName, newSize);
addHistoricalOperationLocked(HISTORICAL_OPERATION_DELETE, oldState);
scheduleWriteIfNeededLocked();
return true;
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public boolean resetSettingLocked(String name) {
if (TextUtils.isEmpty(name) || !hasSettingLocked(name)) {
return false;
}
Setting setting = mSettings.get(name);
if (setting == null) {
return false;
}
Setting oldSetting = new Setting(setting);
String oldValue = setting.getValue();
String oldDefaultValue = setting.getDefaultValue();
int newSize = getNewMemoryUsagePerPackageLocked(setting.packageName, 0, oldValue,
oldDefaultValue, oldDefaultValue, oldDefaultValue);
checkNewMemoryUsagePerPackageLocked(setting.packageName, newSize);
if (!setting.reset()) {
return false;
}
updateMemoryUsagePerPackageLocked(setting.packageName, newSize);
addHistoricalOperationLocked(HISTORICAL_OPERATION_RESET, oldSetting);
scheduleWriteIfNeededLocked();
return true;
}
// The settings provider must hold its lock when calling here.
@GuardedBy("mLock")
public void destroyLocked(Runnable callback) {
mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
if (callback != null) {
if (mDirty) {
// Do it without a delay.
mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS,
callback).sendToTarget();
return;
}
callback.run();
}
}
@GuardedBy("mLock")
private void addHistoricalOperationLocked(String type, Setting setting) {
if (mHistoricalOperations == null) {
return;
}
HistoricalOperation operation = new HistoricalOperation(
System.currentTimeMillis(), type,
setting != null ? new Setting(setting) : null);
if (mNextHistoricalOpIdx >= mHistoricalOperations.size()) {
mHistoricalOperations.add(operation);
} else {
mHistoricalOperations.set(mNextHistoricalOpIdx, operation);
}
mNextHistoricalOpIdx++;
if (mNextHistoricalOpIdx >= HISTORICAL_OPERATION_COUNT) {
mNextHistoricalOpIdx = 0;
}
}
/**
* Dump historical operations as a proto buf.
*
* @param proto The proto buf stream to dump to
* @param fieldId The repeated field ID to use to save an operation to.
*/
void dumpHistoricalOperations(@NonNull ProtoOutputStream proto, long fieldId) {
synchronized (mLock) {
if (mHistoricalOperations == null) {
return;
}
final int operationCount = mHistoricalOperations.size();
for (int i = 0; i < operationCount; i++) {
int index = mNextHistoricalOpIdx - 1 - i;
if (index < 0) {
index = operationCount + index;
}
HistoricalOperation operation = mHistoricalOperations.get(index);
final long token = proto.start(fieldId);
proto.write(SettingsOperationProto.TIMESTAMP, operation.mTimestamp);
proto.write(SettingsOperationProto.OPERATION, operation.mOperation);
if (operation.mSetting != null) {
// Only add the name of the setting, since we don't know the historical package
// and values for it so they would be misleading to add here (all we could
// add is what the current data is).
proto.write(SettingsOperationProto.SETTING, operation.mSetting.getName());
}
proto.end(token);
}
}
}
public void dumpHistoricalOperations(PrintWriter pw) {
synchronized (mLock) {
if (mHistoricalOperations == null) {
return;
}
pw.println("Historical operations");
final int operationCount = mHistoricalOperations.size();
for (int i = 0; i < operationCount; i++) {
int index = mNextHistoricalOpIdx - 1 - i;
if (index < 0) {
index = operationCount + index;
}
HistoricalOperation operation = mHistoricalOperations.get(index);
pw.print(TimeUtils.formatForLogging(operation.mTimestamp));
pw.print(" ");
pw.print(operation.mOperation);
if (operation.mSetting != null) {
pw.print(" ");
// Only print the name of the setting, since we don't know the
// historical package and values for it so they would be misleading
// to print here (all we could print is what the current data is).
pw.print(operation.mSetting.getName());
}
pw.println();
}
pw.println();
pw.println();
}
}
@GuardedBy("mLock")
private boolean isExemptFromMemoryUsageCap(String packageName) {
return mMaxBytesPerAppPackage == MAX_BYTES_PER_APP_PACKAGE_UNLIMITED
|| SYSTEM_PACKAGE_NAME.equals(packageName);
}
@GuardedBy("mLock")
private void checkNewMemoryUsagePerPackageLocked(String packageName, int newSize)
throws IllegalStateException {
if (isExemptFromMemoryUsageCap(packageName)) {
return;
}
if (newSize > mMaxBytesPerAppPackage) {
throw new IllegalStateException("You are adding too many system settings. "
+ "You should stop using system settings for app specific data"
+ " package: " + packageName);
}
}
@GuardedBy("mLock")
private int getNewMemoryUsagePerPackageLocked(String packageName, int deltaKeyLength,
String oldValue, String newValue, String oldDefaultValue, String newDefaultValue) {
if (isExemptFromMemoryUsageCap(packageName)) {
return 0;
}
final int currentSize = mPackageToMemoryUsage.getOrDefault(packageName, 0);
final int oldValueLength = (oldValue != null) ? oldValue.length() : 0;
final int newValueLength = (newValue != null) ? newValue.length() : 0;
final int oldDefaultValueLength = (oldDefaultValue != null) ? oldDefaultValue.length() : 0;
final int newDefaultValueLength = (newDefaultValue != null) ? newDefaultValue.length() : 0;
final int deltaSize = (deltaKeyLength + newValueLength + newDefaultValueLength
- oldValueLength - oldDefaultValueLength) * Character.BYTES;
return Math.max(currentSize + deltaSize, 0);
}
@GuardedBy("mLock")
private void updateMemoryUsagePerPackageLocked(String packageName, int newSize) {
if (isExemptFromMemoryUsageCap(packageName)) {
return;
}
if (DEBUG) {
Slog.i(LOG_TAG, "Settings for package: " + packageName
+ " size: " + newSize + " bytes.");
}
mPackageToMemoryUsage.put(packageName, newSize);
}
public boolean hasSetting(String name) {
synchronized (mLock) {
return hasSettingLocked(name);
}
}
@GuardedBy("mLock")
private boolean hasSettingLocked(String name) {
return mSettings.indexOfKey(name) >= 0;
}
@GuardedBy("mLock")
private void scheduleWriteIfNeededLocked() {
// If dirty then we have a write already scheduled.
if (!mDirty) {
mDirty = true;
writeStateAsyncLocked();
}
}
@GuardedBy("mLock")
private void writeStateAsyncLocked() {
final long currentTimeMillis = SystemClock.uptimeMillis();
if (mWriteScheduled) {
mHandler.removeMessages(MyHandler.MSG_PERSIST_SETTINGS);
// If enough time passed, write without holding off anymore.
final long timeSinceLastNotWrittenMutationMillis = currentTimeMillis
- mLastNotWrittenMutationTimeMillis;
if (timeSinceLastNotWrittenMutationMillis >= MAX_WRITE_SETTINGS_DELAY_MILLIS) {
mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS).sendToTarget();
return;
}
// Hold off a bit more as settings are frequently changing.
final long maxDelayMillis = Math.max(mLastNotWrittenMutationTimeMillis
+ MAX_WRITE_SETTINGS_DELAY_MILLIS - currentTimeMillis, 0);
final long writeDelayMillis = Math.min(WRITE_SETTINGS_DELAY_MILLIS, maxDelayMillis);
Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
mHandler.sendMessageDelayed(message, writeDelayMillis);
} else {
mLastNotWrittenMutationTimeMillis = currentTimeMillis;
Message message = mHandler.obtainMessage(MyHandler.MSG_PERSIST_SETTINGS);
mHandler.sendMessageDelayed(message, WRITE_SETTINGS_DELAY_MILLIS);
mWriteScheduled = true;
}
}
private void doWriteState() {
boolean wroteState = false;
String settingFailedToBePersisted = null;
final int version;
final ArrayMap settings;
final ArrayMap namespaceBannedHashes;
synchronized (mLock) {
version = mVersion;
settings = new ArrayMap<>(mSettings);
namespaceBannedHashes = new ArrayMap<>(mNamespaceBannedHashes);
mDirty = false;
mWriteScheduled = false;
}
synchronized (mWriteLock) {
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[PERSIST START]");
}
AtomicFile destination = new AtomicFile(mStatePersistFile, mStatePersistTag);
FileOutputStream out = null;
try {
out = destination.startWrite();
TypedXmlSerializer serializer = Xml.resolveSerializer(out);
serializer.startDocument(null, true);
serializer.startTag(null, TAG_SETTINGS);
serializer.attributeInt(null, ATTR_VERSION, version);
final int settingCount = settings.size();
for (int i = 0; i < settingCount; i++) {
Setting setting = settings.valueAt(i);
if (setting.isTransient()) {
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[SKIPPED PERSISTING]" + setting.getName());
}
continue;
}
try {
if (writeSingleSetting(mVersion, serializer, setting.getId(),
setting.getName(),
setting.getValue(), setting.getDefaultValue(),
setting.getPackageName(),
setting.getTag(), setting.isDefaultFromSystem(),
setting.isValuePreservedInRestore())) {
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[PERSISTED]" + setting.getName() + "="
+ setting.getValue());
}
}
} catch (IOException ex) {
Slog.e(LOG_TAG, "[ABORT PERSISTING]" + setting.getName()
+ " due to error writing to disk", ex);
// A setting failed to be written. Abort the serialization to avoid leaving
// a partially serialized setting on disk, which can cause parsing errors.
// Note down the problematic setting, so that we can delete it before trying
// again to persist the rest of the settings.
settingFailedToBePersisted = setting.getName();
throw ex;
}
}
serializer.endTag(null, TAG_SETTINGS);
serializer.startTag(null, TAG_NAMESPACE_HASHES);
for (int i = 0; i < namespaceBannedHashes.size(); i++) {
String namespace = namespaceBannedHashes.keyAt(i);
String bannedHash = namespaceBannedHashes.get(namespace);
if (writeSingleNamespaceHash(serializer, namespace, bannedHash)) {
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[PERSISTED] namespace=" + namespace
+ ", bannedHash=" + bannedHash);
}
}
}
serializer.endTag(null, TAG_NAMESPACE_HASHES);
serializer.endDocument();
destination.finishWrite(out);
wroteState = true;
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[PERSIST END]");
}
} catch (Throwable t) {
Slog.e(LOG_TAG, "Failed to write settings, restoring old file", t);
if (t instanceof IOException) {
if (t.getMessage().contains("Couldn't create directory")) {
if (DEBUG) {
// we failed to create a directory, so log the permissions and existence
// state for the settings file and directory
logSettingsDirectoryInformation(destination.getBaseFile());
}
// attempt to create the directory with Files.createDirectories, which
// throws more informative errors than File.mkdirs.
Path parentPath = destination.getBaseFile().getParentFile().toPath();
try {
Files.createDirectories(parentPath);
if (DEBUG) {
Slog.i(LOG_TAG, "Successfully created " + parentPath);
}
} catch (Throwable t2) {
Slog.e(LOG_TAG, "Failed to write " + parentPath
+ " with Files.writeDirectories", t2);
}
}
}
destination.failWrite(out);
} finally {
IoUtils.closeQuietly(out);
}
}
if (!wroteState) {
if (settingFailedToBePersisted != null) {
synchronized (mLock) {
// Delete the problematic setting. This will schedule a write as well.
deleteSettingLocked(settingFailedToBePersisted);
}
}
} else {
// success
synchronized (mLock) {
addHistoricalOperationLocked(HISTORICAL_OPERATION_PERSIST, null);
}
}
}
private static void logSettingsDirectoryInformation(File settingsFile) {
File parent = settingsFile.getParentFile();
Slog.i(LOG_TAG, "directory info for directory/file " + settingsFile
+ " with stacktrace ", new Exception());
File ancestorDir = parent;
while (ancestorDir != null) {
if (!ancestorDir.exists()) {
Slog.i(LOG_TAG, "ancestor directory " + ancestorDir
+ " does not exist");
ancestorDir = ancestorDir.getParentFile();
} else {
Slog.i(LOG_TAG, "ancestor directory " + ancestorDir
+ " exists");
Slog.i(LOG_TAG, "ancestor directory " + ancestorDir
+ " permissions: r: " + ancestorDir.canRead() + " w: "
+ ancestorDir.canWrite() + " x: " + ancestorDir.canExecute());
File ancestorParent = ancestorDir.getParentFile();
if (ancestorParent != null) {
Slog.i(LOG_TAG, "ancestor's parent directory " + ancestorParent
+ " permissions: r: " + ancestorParent.canRead() + " w: "
+ ancestorParent.canWrite() + " x: " + ancestorParent.canExecute());
}
break;
}
}
}
static boolean writeSingleSetting(int version, TypedXmlSerializer serializer, String id,
String name, String value, String defaultValue, String packageName,
String tag, boolean defaultSysSet, boolean isValuePreservedInRestore)
throws IOException {
if (id == null || isBinary(id) || name == null || isBinary(name)
|| packageName == null || isBinary(packageName)) {
if (DEBUG_PERSISTENCE) {
Slog.w(LOG_TAG, "Invalid arguments for writeSingleSetting: version=" + version
+ ", id=" + id + ", name=" + name + ", value=" + value
+ ", defaultValue=" + defaultValue + ", packageName=" + packageName
+ ", tag=" + tag + ", defaultSysSet=" + defaultSysSet
+ ", isValuePreservedInRestore=" + isValuePreservedInRestore);
}
return false;
}
serializer.startTag(null, TAG_SETTING);
serializer.attribute(null, ATTR_ID, id);
serializer.attribute(null, ATTR_NAME, name);
setValueAttribute(ATTR_VALUE, ATTR_VALUE_BASE64,
version, serializer, value);
serializer.attribute(null, ATTR_PACKAGE, packageName);
if (defaultValue != null) {
setValueAttribute(ATTR_DEFAULT_VALUE, ATTR_DEFAULT_VALUE_BASE64,
version, serializer, defaultValue);
serializer.attributeBoolean(null, ATTR_DEFAULT_SYS_SET, defaultSysSet);
setValueAttribute(ATTR_TAG, ATTR_TAG_BASE64,
version, serializer, tag);
}
if (isValuePreservedInRestore) {
serializer.attributeBoolean(null, ATTR_PRESERVE_IN_RESTORE, true);
}
serializer.endTag(null, TAG_SETTING);
return true;
}
static void setValueAttribute(String attr, String attrBase64, int version,
TypedXmlSerializer serializer, String value) throws IOException {
if (version >= SETTINGS_VERSION_NEW_ENCODING) {
if (value == null) {
// Null value -> No ATTR_VALUE nor ATTR_VALUE_BASE64.
} else if (isBinary(value)) {
serializer.attribute(null, attrBase64, base64Encode(value));
} else {
serializer.attribute(null, attr, value);
}
} else {
// Old encoding.
if (value == null) {
serializer.attribute(null, attr, NULL_VALUE_OLD_STYLE);
} else {
serializer.attribute(null, attr, value);
}
}
}
private static boolean writeSingleNamespaceHash(TypedXmlSerializer serializer, String namespace,
String bannedHashCode) throws IOException {
if (namespace == null || bannedHashCode == null) {
if (DEBUG_PERSISTENCE) {
Slog.w(LOG_TAG, "Invalid arguments for writeSingleNamespaceHash: namespace="
+ namespace + ", bannedHashCode=" + bannedHashCode);
}
return false;
}
serializer.startTag(null, TAG_NAMESPACE_HASH);
serializer.attribute(null, ATTR_NAMESPACE, namespace);
serializer.attribute(null, ATTR_BANNED_HASH, bannedHashCode);
serializer.endTag(null, TAG_NAMESPACE_HASH);
return true;
}
private static String hashCode(Map keyValues) {
return Integer.toString(keyValues.hashCode());
}
private String getValueAttribute(TypedXmlPullParser parser, String attr, String base64Attr) {
if (mVersion >= SETTINGS_VERSION_NEW_ENCODING) {
final String value = parser.getAttributeValue(null, attr);
if (value != null) {
return value;
}
final String base64 = parser.getAttributeValue(null, base64Attr);
if (base64 != null) {
return base64Decode(base64);
}
// null has neither ATTR_VALUE nor ATTR_VALUE_BASE64.
return null;
} else {
// Old encoding.
final String stored = parser.getAttributeValue(null, attr);
if (NULL_VALUE_OLD_STYLE.equals(stored)) {
return null;
} else {
return stored;
}
}
}
@GuardedBy("mLock")
private void readStateSyncLocked() throws IllegalStateException {
FileInputStream in;
AtomicFile file = new AtomicFile(mStatePersistFile);
try {
in = file.openRead();
} catch (FileNotFoundException fnfe) {
Slog.w(LOG_TAG, "No settings state " + mStatePersistFile);
if (DEBUG) {
logSettingsDirectoryInformation(mStatePersistFile);
}
addHistoricalOperationLocked(HISTORICAL_OPERATION_INITIALIZE, null);
return;
}
if (parseStateFromXmlStreamLocked(in)) {
return;
}
// Settings file exists but is corrupted. Retry with the fallback file
final File statePersistFallbackFile = new File(
mStatePersistFile.getAbsolutePath() + FALLBACK_FILE_SUFFIX);
Slog.w(LOG_TAG, "Failed parsing settings file: " + mStatePersistFile
+ ", retrying with fallback file: " + statePersistFallbackFile);
try {
in = new AtomicFile(statePersistFallbackFile).openRead();
} catch (FileNotFoundException fnfe) {
final String message = "No fallback file found for: " + mStatePersistFile;
Slog.wtf(LOG_TAG, message);
if (!isConfigSettingsKey(mKey)) {
// Allow partially deserialized config settings because they can be updated later
throw new IllegalStateException(message);
}
}
if (parseStateFromXmlStreamLocked(in)) {
// Parsed state from fallback file. Restore original file with fallback file
try {
FileUtils.copy(statePersistFallbackFile, mStatePersistFile);
} catch (IOException ignored) {
// Failed to copy, but it's okay because we already parsed states from fallback file
}
} else {
final String message = "Failed parsing settings file: " + mStatePersistFile;
Slog.wtf(LOG_TAG, message);
if (!isConfigSettingsKey(mKey)) {
// Allow partially deserialized config settings because they can be updated later
throw new IllegalStateException(message);
}
}
}
@GuardedBy("mLock")
private boolean parseStateFromXmlStreamLocked(FileInputStream in) {
try {
TypedXmlPullParser parser = Xml.resolvePullParser(in);
parseStateLocked(parser);
return true;
} catch (XmlPullParserException | IOException e) {
Slog.e(LOG_TAG, "parse settings xml failed", e);
return false;
} finally {
IoUtils.closeQuietly(in);
}
}
/**
* Uses AtomicFile to check if the file or its backup exists.
*
* @param file The file to check for existence
* @return whether the original or backup exist
*/
public static boolean stateFileExists(File file) {
AtomicFile stateFile = new AtomicFile(file);
return stateFile.exists();
}
private void parseStateLocked(TypedXmlPullParser parser)
throws IOException, XmlPullParserException {
final int outerDepth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if (tagName.equals(TAG_SETTINGS)) {
parseSettingsLocked(parser);
} else if (tagName.equals(TAG_NAMESPACE_HASHES)) {
parseNamespaceHash(parser);
}
}
}
/**
* Transforms a staged flag name to its real flag name.
*
* Staged flags take the form {@code staged/namespace*flagName}. If
* {@code stagedFlagName} takes the proper form, returns
* {@code namespace/flagName}. Otherwise, returns {@code stagedFlagName}
* unmodified, and logs an error message.
*
*/
@VisibleForTesting
public static String createRealFlagName(String stagedFlagName) {
int slashIndex = stagedFlagName.indexOf("/");
if (slashIndex == -1 || slashIndex == stagedFlagName.length() - 1
|| slashIndex == 0) {
Slog.w(LOG_TAG, "invalid staged flag, not applying: " + stagedFlagName);
return stagedFlagName;
}
String namespaceAndFlag =
stagedFlagName.substring(slashIndex + 1);
int starIndex = namespaceAndFlag.indexOf("*");
if (starIndex == -1 || starIndex == namespaceAndFlag.length() - 1
|| starIndex == 0) {
Slog.w(LOG_TAG, "invalid staged flag, not applying: " + stagedFlagName);
return stagedFlagName;
}
String namespace =
namespaceAndFlag.substring(0, starIndex);
String flagName =
namespaceAndFlag.substring(starIndex + 1);
return namespace + "/" + flagName;
}
@GuardedBy("mLock")
private void parseSettingsLocked(TypedXmlPullParser parser)
throws IOException, XmlPullParserException {
mVersion = parser.getAttributeInt(null, ATTR_VERSION);
final int outerDepth = parser.getDepth();
int type;
HashSet flagsWithStagedValueApplied = new HashSet();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if (tagName.equals(TAG_SETTING)) {
String id = parser.getAttributeValue(null, ATTR_ID);
String name = parser.getAttributeValue(null, ATTR_NAME);
String value = getValueAttribute(parser, ATTR_VALUE, ATTR_VALUE_BASE64);
String packageName = parser.getAttributeValue(null, ATTR_PACKAGE);
String defaultValue = getValueAttribute(parser, ATTR_DEFAULT_VALUE,
ATTR_DEFAULT_VALUE_BASE64);
boolean isPreservedInRestore = parser.getAttributeBoolean(null,
ATTR_PRESERVE_IN_RESTORE, false);
String tag = null;
boolean fromSystem = false;
if (defaultValue != null) {
fromSystem = parser.getAttributeBoolean(null, ATTR_DEFAULT_SYS_SET, false);
tag = getValueAttribute(parser, ATTR_TAG, ATTR_TAG_BASE64);
}
if (isConfigSettingsKey(mKey)) {
if (flagsWithStagedValueApplied.contains(name)) {
continue;
}
if (name.startsWith(CONFIG_STAGED_PREFIX)) {
name = createRealFlagName(name);
flagsWithStagedValueApplied.add(name);
}
}
if (isConfigSettingsKey(mKey) && name != null
&& name.equals(STORAGE_MIGRATION_FLAG)) {
if (value.equals("true")) {
Path path = Paths.get(STORAGE_MIGRATION_MARKER_FILE);
if (!Files.exists(path)) {
Files.createFile(path);
}
Set perms =
Files.readAttributes(path, PosixFileAttributes.class).permissions();
perms.add(PosixFilePermission.OWNER_WRITE);
perms.add(PosixFilePermission.OWNER_READ);
perms.add(PosixFilePermission.GROUP_READ);
perms.add(PosixFilePermission.OTHERS_READ);
try {
Files.setPosixFilePermissions(path, perms);
} catch (Exception e) {
Slog.e(LOG_TAG, "failed to set permissions on migration marker", e);
}
} else {
java.nio.file.Path path = Paths.get(STORAGE_MIGRATION_MARKER_FILE);
if (Files.exists(path)) {
Files.delete(path);
}
}
}
mSettings.put(name, new Setting(name, value, defaultValue, packageName, tag,
fromSystem, id, isPreservedInRestore));
if (DEBUG_PERSISTENCE) {
Slog.i(LOG_TAG, "[RESTORED] " + name + "=" + value);
}
}
}
if (isConfigSettingsKey(mKey) && !flagsWithStagedValueApplied.isEmpty()) {
// On boot, the config table XML file includes special staged flags. On the initial
// boot XML -> HashMap parse, these staged flags get transformed into real flags.
// After this, the HashMap contains no special staged flags (only the transformed
// real flags), but the XML still does. We then have no need for the special staged
// flags in the XML, so we overwrite the XML with the latest contents of the
// HashMap.
writeStateAsyncLocked();
}
}
@GuardedBy("mLock")
private void parseNamespaceHash(TypedXmlPullParser parser)
throws IOException, XmlPullParserException {
final int outerDepth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
if (parser.getName().equals(TAG_NAMESPACE_HASH)) {
String namespace = parser.getAttributeValue(null, ATTR_NAMESPACE);
String bannedHashCode = parser.getAttributeValue(null, ATTR_BANNED_HASH);
mNamespaceBannedHashes.put(namespace, bannedHashCode);
}
}
}
private static Map removeNullValueOldStyle(Map keyValues) {
Iterator> it = keyValues.entrySet().iterator();
while (it.hasNext()) {
Map.Entry keyValueEntry = it.next();
if (NULL_VALUE_OLD_STYLE.equals(keyValueEntry.getValue())) {
keyValueEntry.setValue(null);
}
}
return keyValues;
}
private final class MyHandler extends Handler {
public static final int MSG_PERSIST_SETTINGS = 1;
public MyHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MSG_PERSIST_SETTINGS: {
Runnable callback = (Runnable) message.obj;
doWriteState();
if (callback != null) {
callback.run();
}
}
break;
}
}
}
private class HistoricalOperation {
final long mTimestamp;
final String mOperation;
final Setting mSetting;
public HistoricalOperation(long timestamp,
String operation, Setting setting) {
mTimestamp = timestamp;
mOperation = operation;
mSetting = setting;
}
}
class Setting {
private String name;
private String value;
private String defaultValue;
private String packageName;
private String id;
private String tag;
// Whether the default is set by the system
private boolean defaultFromSystem;
// Whether the value of this setting will be preserved when restore happens.
private boolean isValuePreservedInRestore;
public Setting(Setting other) {
name = other.name;
value = other.value;
defaultValue = other.defaultValue;
packageName = other.packageName;
id = other.id;
defaultFromSystem = other.defaultFromSystem;
tag = other.tag;
isValuePreservedInRestore = other.isValuePreservedInRestore;
}
public Setting(String name, String value, boolean makeDefault, String packageName,
String tag) {
this(name, value, makeDefault, packageName, tag, false);
}
Setting(String name, String value, boolean makeDefault, String packageName,
String tag, boolean forceNonSystemPackage) {
this.name = name;
// overrideableByRestore = true as the first initialization isn't considered a
// modification.
update(value, makeDefault, packageName, tag, forceNonSystemPackage, true);
}
public Setting(String name, String value, String defaultValue,
String packageName, String tag, boolean fromSystem, String id) {
this(name, value, defaultValue, packageName, tag, fromSystem, id,
/* isOverrideableByRestore */ false);
}
Setting(String name, String value, String defaultValue,
String packageName, String tag, boolean fromSystem, String id,
boolean isValuePreservedInRestore) {
mNextId = Math.max(mNextId, Long.parseLong(id) + 1);
if (NULL_VALUE.equals(value)) {
value = null;
}
init(name, value, tag, defaultValue, packageName, fromSystem, id,
isValuePreservedInRestore);
}
private void init(String name, String value, String tag, String defaultValue,
String packageName, boolean fromSystem, String id,
boolean isValuePreservedInRestore) {
this.name = name;
this.value = value;
this.tag = tag;
this.defaultValue = defaultValue;
this.packageName = packageName;
this.id = id;
this.defaultFromSystem = fromSystem;
this.isValuePreservedInRestore = isValuePreservedInRestore;
}
public String getName() {
return name;
}
public int getKey() {
return mKey;
}
public String getValue() {
return value;
}
public String getTag() {
return tag;
}
public String getDefaultValue() {
return defaultValue;
}
public String getPackageName() {
return packageName;
}
public boolean isDefaultFromSystem() {
return defaultFromSystem;
}
public boolean isValuePreservedInRestore() {
return isValuePreservedInRestore;
}
public String getId() {
return id;
}
public boolean isNull() {
return false;
}
/** @return whether the value changed */
public boolean reset() {
// overrideableByRestore = true as resetting to default value isn't considered a
// modification.
return update(this.defaultValue, false, packageName, null, true, true,
/* resetToDefault */ true);
}
public boolean isTransient() {
switch (getTypeFromKey(getKey())) {
case SETTINGS_TYPE_GLOBAL:
return ArrayUtils.contains(Global.TRANSIENT_SETTINGS, getName());
}
return false;
}
public boolean update(String value, boolean setDefault, String packageName, String tag,
boolean forceNonSystemPackage, boolean overrideableByRestore) {
return update(value, setDefault, packageName, tag, forceNonSystemPackage,
overrideableByRestore, /* resetToDefault */ false);
}
private boolean update(String value, boolean setDefault, String packageName, String tag,
boolean forceNonSystemPackage, boolean overrideableByRestore,
boolean resetToDefault) {
if (NULL_VALUE.equals(value)) {
value = null;
}
final boolean callerSystem = !forceNonSystemPackage &&
!isNull() && (isCalledFromSystem(packageName)
|| isSystemPackage(mContext, packageName));
// Settings set by the system are always defaults.
if (callerSystem) {
setDefault = true;
}
String defaultValue = this.defaultValue;
boolean defaultFromSystem = this.defaultFromSystem;
if (setDefault) {
if (!Objects.equals(value, this.defaultValue)
&& (!defaultFromSystem || callerSystem)) {
defaultValue = value;
// Default null means no default, so the tag is irrelevant
// since it is used to reset a settings subset their defaults.
// Also it is irrelevant if the system set the canonical default.
if (defaultValue == null) {
tag = null;
defaultFromSystem = false;
}
}
if (!defaultFromSystem && value != null) {
if (callerSystem) {
defaultFromSystem = true;
}
}
}
// isValuePreservedInRestore shouldn't change back to false if it has been set to true.
boolean isPreserved = shouldPreserveSetting(overrideableByRestore, resetToDefault,
packageName, value);
// Is something gonna change?
if (Objects.equals(value, this.value)
&& Objects.equals(defaultValue, this.defaultValue)
&& Objects.equals(packageName, this.packageName)
&& Objects.equals(tag, this.tag)
&& defaultFromSystem == this.defaultFromSystem
&& isPreserved == this.isValuePreservedInRestore) {
return false;
}
init(name, value, tag, defaultValue, packageName, defaultFromSystem,
String.valueOf(mNextId++), isPreserved);
return true;
}
public String toString() {
return "Setting{name=" + name + " value=" + value
+ (defaultValue != null ? " default=" + defaultValue : "")
+ " packageName=" + packageName + " tag=" + tag
+ " defaultFromSystem=" + defaultFromSystem + "}";
}
private boolean shouldPreserveSetting(boolean overrideableByRestore,
boolean resetToDefault, String packageName, String value) {
if (resetToDefault) {
// By default settings are not marked as preserved.
return false;
}
if (value != null && value.equals(this.value)
&& SYSTEM_PACKAGE_NAME.equals(packageName)) {
// Do not mark preserved if it's the system reinitializing to the same value.
return false;
}
// isValuePreservedInRestore shouldn't change back to false if it has been set to true.
return this.isValuePreservedInRestore || !overrideableByRestore;
}
}
/**
* @return TRUE if a string is considered "binary" from KXML's point of view. NOTE DO NOT
* pass null.
*/
public static boolean isBinary(String s) {
if (s == null) {
throw new NullPointerException();
}
// See KXmlSerializer.writeEscaped
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
boolean allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
if (!allowedInXml) {
return true;
}
}
return false;
}
private static String base64Encode(String s) {
return Base64.encodeToString(toBytes(s), Base64.NO_WRAP);
}
private static String base64Decode(String s) {
return fromBytes(Base64.decode(s, Base64.DEFAULT));
}
// Note the followings are basically just UTF-16 encode/decode. But we want to preserve
// contents as-is, even if it contains broken surrogate pairs, we do it by ourselves,
// since I don't know how Charset would treat them.
private static byte[] toBytes(String s) {
final byte[] result = new byte[s.length() * 2];
int resultIndex = 0;
for (int i = 0; i < s.length(); ++i) {
char ch = s.charAt(i);
result[resultIndex++] = (byte) (ch >> 8);
result[resultIndex++] = (byte) ch;
}
return result;
}
private static String fromBytes(byte[] bytes) {
final StringBuilder sb = new StringBuilder(bytes.length / 2);
final int last = bytes.length - 1;
for (int i = 0; i < last; i += 2) {
final char ch = (char) ((bytes[i] & 0xff) << 8 | (bytes[i + 1] & 0xff));
sb.append(ch);
}
return sb.toString();
}
// Cache the list of names of system packages. This is only called once on system boot.
public static void cacheSystemPackageNamesAndSystemSignature(@NonNull Context context) {
final PackageManager packageManager = context.getPackageManager();
final long identity = Binder.clearCallingIdentity();
try {
sSystemPackages.add(SYSTEM_PACKAGE_NAME);
// Cache SetupWizard package name.
final String setupWizPackageName = packageManager.getSetupWizardPackageName();
if (setupWizPackageName != null) {
sSystemPackages.add(setupWizPackageName);
}
final List packageInfos = packageManager.getInstalledPackages(0);
final int installedPackagesCount = packageInfos.size();
for (int i = 0; i < installedPackagesCount; i++) {
if (shouldAddToSystemPackages(packageInfos.get(i))) {
sSystemPackages.add(packageInfos.get(i).packageName);
}
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private static boolean shouldAddToSystemPackages(@NonNull PackageInfo packageInfo) {
// Shell and Root are not considered a part of the system
if (isShellOrRoot(packageInfo.packageName)) {
return false;
}
// Already added
if (sSystemPackages.contains(packageInfo.packageName)) {
return false;
}
return isSystemPackage(packageInfo.applicationInfo);
}
private static boolean isShellOrRoot(@NonNull String packageName) {
return (SHELL_PACKAGE_NAME.equals(packageName)
|| ROOT_PACKAGE_NAME.equals(packageName));
}
private static boolean isCalledFromSystem(@NonNull String packageName) {
// Shell and Root are not considered a part of the system
if (isShellOrRoot(packageName)) {
return false;
}
final int callingUid = Binder.getCallingUid();
// Native services running as a special UID get a pass
final int callingAppId = UserHandle.getAppId(callingUid);
return (callingAppId < FIRST_APPLICATION_UID);
}
public static boolean isSystemPackage(@NonNull Context context, @NonNull String packageName) {
// Check shell or root before trying to retrieve ApplicationInfo to fail fast
if (isShellOrRoot(packageName)) {
return false;
}
// If it's a known system package or known to be platform signed
if (sSystemPackages.contains(packageName)) {
return true;
}
ApplicationInfo aInfo = null;
final long identity = Binder.clearCallingIdentity();
try {
try {
// Notice that this makes a call to package manager inside the lock
aInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException ignored) {
}
} finally {
Binder.restoreCallingIdentity(identity);
}
return isSystemPackage(aInfo);
}
private static boolean isSystemPackage(@Nullable ApplicationInfo aInfo) {
if (aInfo == null) {
return false;
}
// If the system or a special system UID (like telephony), done.
if (aInfo.uid < FIRST_APPLICATION_UID) {
return true;
}
// If a persistent system app, done.
if ((aInfo.flags & ApplicationInfo.FLAG_PERSISTENT) != 0
&& (aInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
return true;
}
// Platform signed packages are considered to be from the system
if (aInfo.isSignedWithPlatformKey()) {
return true;
}
return false;
}
@VisibleForTesting
public int getMemoryUsage(String packageName) {
synchronized (mLock) {
return mPackageToMemoryUsage.getOrDefault(packageName, 0);
}
}
/**
* Allow tests to wait for the handler to finish handling all the remaining messages
*/
@VisibleForTesting
public void waitForHandler() {
final CountDownLatch latch = new CountDownLatch(1);
synchronized (mLock) {
mHandler.post(latch::countDown);
}
try {
latch.await();
} catch (InterruptedException e) {
// ignored
}
}
}