Merge "Revert "Add mmd_native namespace to system properties"" into main
diff --git a/aflags/Android.bp b/aflags/Android.bp
new file mode 100644
index 0000000..ff74350
--- /dev/null
+++ b/aflags/Android.bp
@@ -0,0 +1,40 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+ name: "aflags_updatable.defaults",
+ edition: "2021",
+ clippy_lints: "android",
+ lints: "android",
+ srcs: ["src/main.rs"],
+ rustlibs: [
+ "libaconfig_device_paths",
+ "libaconfig_flags",
+ "libaconfig_protos",
+ "libaconfigd_protos_rust",
+ "libaconfig_storage_read_api",
+ "libaconfig_storage_file",
+ "libanyhow",
+ "libclap",
+ "libnix",
+ "libprotobuf",
+ "libregex",
+ ],
+}
+
+rust_binary {
+ name: "aflags_updatable",
+ host_supported: true,
+ defaults: ["aflags.defaults"],
+ apex_available: [
+ "com.android.configinfrastructure",
+ ],
+ min_sdk_version: "34",
+}
+
+rust_test_host {
+ name: "aflags_updatable.test",
+ defaults: ["aflags_updatable.defaults"],
+ test_suites: ["general-tests"],
+}
diff --git a/aflags/Cargo.toml b/aflags/Cargo.toml
new file mode 100644
index 0000000..d31e232
--- /dev/null
+++ b/aflags/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "aflags"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0.69"
+paste = "1.0.11"
+protobuf = "3.2.0"
+regex = "1.10.3"
+aconfig_protos = { path = "../aconfig_protos" }
+aconfigd_protos = { version = "0.1.0", path = "../../../../../packages/modules/ConfigInfrastructure/aconfigd/proto"}
+nix = { version = "0.28.0", features = ["user"] }
+aconfig_storage_file = { version = "0.1.0", path = "../aconfig_storage_file" }
+aconfig_storage_read_api = { version = "0.1.0", path = "../aconfig_storage_read_api" }
+clap = {version = "4.5.2" }
+aconfig_device_paths = { version = "0.1.0", path = "../aconfig_device_paths" }
+aconfig_flags = { version = "0.1.0", path = "../aconfig_flags" }
diff --git a/aflags/src/aconfig_storage_source.rs b/aflags/src/aconfig_storage_source.rs
new file mode 100644
index 0000000..766807a
--- /dev/null
+++ b/aflags/src/aconfig_storage_source.rs
@@ -0,0 +1,167 @@
+use crate::load_protos;
+use crate::{Flag, FlagSource};
+use crate::{FlagPermission, FlagValue, ValuePickedFrom};
+use aconfigd_protos::{
+ ProtoFlagQueryReturnMessage, ProtoListStorageMessage, ProtoListStorageMessageMsg,
+ ProtoStorageRequestMessage, ProtoStorageRequestMessageMsg, ProtoStorageRequestMessages,
+ ProtoStorageReturnMessage, ProtoStorageReturnMessageMsg, ProtoStorageReturnMessages,
+};
+use anyhow::anyhow;
+use anyhow::Result;
+use protobuf::Message;
+use protobuf::SpecialFields;
+use std::collections::HashMap;
+use std::io::{Read, Write};
+use std::net::Shutdown;
+use std::os::unix::net::UnixStream;
+
+pub struct AconfigStorageSource {}
+
+static ACONFIGD_SYSTEM_SOCKET_NAME: &str = "/dev/socket/aconfigd_system";
+static ACONFIGD_MAINLINE_SOCKET_NAME: &str = "/dev/socket/aconfigd_mainline";
+
+enum AconfigdSocket {
+ System,
+ Mainline,
+}
+
+impl AconfigdSocket {
+ pub fn name(&self) -> &str {
+ match self {
+ AconfigdSocket::System => ACONFIGD_SYSTEM_SOCKET_NAME,
+ AconfigdSocket::Mainline => ACONFIGD_MAINLINE_SOCKET_NAME,
+ }
+ }
+}
+
+fn load_flag_to_container() -> Result<HashMap<String, String>> {
+ Ok(load_protos::load()?.into_iter().map(|p| (p.qualified_name(), p.container)).collect())
+}
+
+fn convert(msg: ProtoFlagQueryReturnMessage, containers: &HashMap<String, String>) -> Result<Flag> {
+ let (value, value_picked_from) = match (
+ &msg.boot_flag_value,
+ msg.default_flag_value,
+ msg.local_flag_value,
+ msg.has_local_override,
+ ) {
+ (_, _, Some(local), Some(has_local)) if has_local => {
+ (FlagValue::try_from(local.as_str())?, ValuePickedFrom::Local)
+ }
+ (Some(boot), Some(default), _, _) => {
+ let value = FlagValue::try_from(boot.as_str())?;
+ if *boot == default {
+ (value, ValuePickedFrom::Default)
+ } else {
+ (value, ValuePickedFrom::Server)
+ }
+ }
+ _ => return Err(anyhow!("missing override")),
+ };
+
+ let staged_value = match (msg.boot_flag_value, msg.server_flag_value, msg.has_server_override) {
+ (Some(boot), Some(server), _) if boot == server => None,
+ (Some(boot), Some(server), Some(has_server)) if boot != server && has_server => {
+ Some(FlagValue::try_from(server.as_str())?)
+ }
+ _ => None,
+ };
+
+ let permission = match msg.is_readwrite {
+ Some(is_readwrite) => {
+ if is_readwrite {
+ FlagPermission::ReadWrite
+ } else {
+ FlagPermission::ReadOnly
+ }
+ }
+ None => return Err(anyhow!("missing permission")),
+ };
+
+ let name = msg.flag_name.ok_or(anyhow!("missing flag name"))?;
+ let package = msg.package_name.ok_or(anyhow!("missing package name"))?;
+ let qualified_name = format!("{package}.{name}");
+ Ok(Flag {
+ name,
+ package,
+ value,
+ permission,
+ value_picked_from,
+ staged_value,
+ container: containers
+ .get(&qualified_name)
+ .cloned()
+ .unwrap_or_else(|| "<no container>".to_string())
+ .to_string(),
+ // TODO: remove once DeviceConfig is not in the CLI.
+ namespace: "-".to_string(),
+ })
+}
+
+fn read_from_socket(socket: AconfigdSocket) -> Result<Vec<ProtoFlagQueryReturnMessage>> {
+ let messages = ProtoStorageRequestMessages {
+ msgs: vec![ProtoStorageRequestMessage {
+ msg: Some(ProtoStorageRequestMessageMsg::ListStorageMessage(ProtoListStorageMessage {
+ msg: Some(ProtoListStorageMessageMsg::All(true)),
+ special_fields: SpecialFields::new(),
+ })),
+ special_fields: SpecialFields::new(),
+ }],
+ special_fields: SpecialFields::new(),
+ };
+
+ let mut socket = UnixStream::connect(socket.name())?;
+
+ let message_buffer = messages.write_to_bytes()?;
+ let mut message_length_buffer: [u8; 4] = [0; 4];
+ let message_size = &message_buffer.len();
+ message_length_buffer[0] = (message_size >> 24) as u8;
+ message_length_buffer[1] = (message_size >> 16) as u8;
+ message_length_buffer[2] = (message_size >> 8) as u8;
+ message_length_buffer[3] = *message_size as u8;
+ socket.write_all(&message_length_buffer)?;
+ socket.write_all(&message_buffer)?;
+ socket.shutdown(Shutdown::Write)?;
+
+ let mut response_length_buffer: [u8; 4] = [0; 4];
+ socket.read_exact(&mut response_length_buffer)?;
+ let response_length = u32::from_be_bytes(response_length_buffer) as usize;
+ let mut response_buffer = vec![0; response_length];
+ socket.read_exact(&mut response_buffer)?;
+
+ let response: ProtoStorageReturnMessages =
+ protobuf::Message::parse_from_bytes(&response_buffer)?;
+
+ match response.msgs.as_slice() {
+ [ProtoStorageReturnMessage {
+ msg: Some(ProtoStorageReturnMessageMsg::ListStorageMessage(list_storage_message)),
+ ..
+ }] => Ok(list_storage_message.flags.clone()),
+ _ => Err(anyhow!("unexpected response from aconfigd")),
+ }
+}
+
+impl FlagSource for AconfigStorageSource {
+ fn list_flags() -> Result<Vec<Flag>> {
+ let containers = load_flag_to_container()?;
+ let system_messages = read_from_socket(AconfigdSocket::System);
+ let mainline_messages = read_from_socket(AconfigdSocket::Mainline);
+
+ let mut all_messages = vec![];
+ if let Ok(system_messages) = system_messages {
+ all_messages.extend_from_slice(&system_messages);
+ }
+ if let Ok(mainline_messages) = mainline_messages {
+ all_messages.extend_from_slice(&mainline_messages);
+ }
+
+ all_messages
+ .into_iter()
+ .map(|query_message| convert(query_message.clone(), &containers))
+ .collect()
+ }
+
+ fn override_flag(_namespace: &str, _qualified_name: &str, _value: &str) -> Result<()> {
+ todo!()
+ }
+}
diff --git a/aflags/src/device_config_source.rs b/aflags/src/device_config_source.rs
new file mode 100644
index 0000000..cf6ab28
--- /dev/null
+++ b/aflags/src/device_config_source.rs
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+use crate::load_protos;
+use crate::{Flag, FlagSource, FlagValue, ValuePickedFrom};
+
+use anyhow::{anyhow, bail, Result};
+use regex::Regex;
+use std::collections::HashMap;
+use std::process::Command;
+use std::str;
+
+pub struct DeviceConfigSource {}
+
+fn parse_device_config(raw: &str) -> Result<HashMap<String, FlagValue>> {
+ let mut flags = HashMap::new();
+ let regex = Regex::new(r"(?m)^([[[:alnum:]]_]+/[[[:alnum:]]_\.]+)=(true|false)$")?;
+ for capture in regex.captures_iter(raw) {
+ let key =
+ capture.get(1).ok_or(anyhow!("invalid device_config output"))?.as_str().to_string();
+ let value = FlagValue::try_from(
+ capture.get(2).ok_or(anyhow!("invalid device_config output"))?.as_str(),
+ )?;
+ flags.insert(key, value);
+ }
+ Ok(flags)
+}
+
+fn read_device_config_output(command: &[&str]) -> Result<String> {
+ let output = Command::new("/system/bin/device_config").args(command).output()?;
+ if !output.status.success() {
+ let reason = match output.status.code() {
+ Some(code) => {
+ let output = str::from_utf8(&output.stdout)?;
+ if !output.is_empty() {
+ format!("exit code {code}, output was {output}")
+ } else {
+ format!("exit code {code}")
+ }
+ }
+ None => "terminated by signal".to_string(),
+ };
+ bail!("failed to access flag storage: {}", reason);
+ }
+ Ok(str::from_utf8(&output.stdout)?.to_string())
+}
+
+fn read_device_config_flags() -> Result<HashMap<String, FlagValue>> {
+ let list_output = read_device_config_output(&["list"])?;
+ parse_device_config(&list_output)
+}
+
+/// Parse the list of newline-separated staged flags.
+///
+/// The output is a newline-sepaarated list of entries which follow this format:
+/// `namespace*flagname=value`
+///
+/// The resulting map maps from `namespace/flagname` to `value`, if a staged flag exists for
+/// `namespace/flagname`.
+fn parse_staged_flags(raw: &str) -> Result<HashMap<String, FlagValue>> {
+ let mut flags = HashMap::new();
+ for line in raw.split('\n') {
+ match (line.find('*'), line.find('=')) {
+ (Some(star_index), Some(equal_index)) => {
+ let namespace = &line[..star_index];
+ let flag = &line[star_index + 1..equal_index];
+ if let Ok(value) = FlagValue::try_from(&line[equal_index + 1..]) {
+ flags.insert(namespace.to_owned() + "/" + flag, value);
+ }
+ }
+ _ => continue,
+ };
+ }
+ Ok(flags)
+}
+
+fn read_staged_flags() -> Result<HashMap<String, FlagValue>> {
+ let staged_flags_output = read_device_config_output(&["list", "staged"])?;
+ parse_staged_flags(&staged_flags_output)
+}
+
+fn reconcile(
+ pb_flags: &[Flag],
+ dc_flags: HashMap<String, FlagValue>,
+ staged_flags: HashMap<String, FlagValue>,
+) -> Vec<Flag> {
+ pb_flags
+ .iter()
+ .map(|f| {
+ let server_override = dc_flags.get(&format!("{}/{}", f.namespace, f.qualified_name()));
+ let (value_picked_from, selected_value) = match server_override {
+ Some(value) if *value != f.value => (ValuePickedFrom::Server, *value),
+ _ => (ValuePickedFrom::Default, f.value),
+ };
+ Flag { value_picked_from, value: selected_value, ..f.clone() }
+ })
+ .map(|f| {
+ let staged_value = staged_flags
+ .get(&format!("{}/{}", f.namespace, f.qualified_name()))
+ .map(|value| if *value != f.value { Some(*value) } else { None })
+ .unwrap_or(None);
+ Flag { staged_value, ..f }
+ })
+ .collect()
+}
+
+impl FlagSource for DeviceConfigSource {
+ fn list_flags() -> Result<Vec<Flag>> {
+ let pb_flags = load_protos::load()?;
+ let dc_flags = read_device_config_flags()?;
+ let staged_flags = read_staged_flags()?;
+
+ let flags = reconcile(&pb_flags, dc_flags, staged_flags);
+ Ok(flags)
+ }
+
+ fn override_flag(namespace: &str, qualified_name: &str, value: &str) -> Result<()> {
+ read_device_config_output(&["put", namespace, qualified_name, value]).map(|_| ())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_parse_device_config() {
+ let input = r#"
+namespace_one/com.foo.bar.flag_one=true
+namespace_one/com.foo.bar.flag_two=false
+random_noise;
+namespace_two/android.flag_one=true
+namespace_two/android.flag_two=nonsense
+"#;
+ let expected = HashMap::from([
+ ("namespace_one/com.foo.bar.flag_one".to_string(), FlagValue::Enabled),
+ ("namespace_one/com.foo.bar.flag_two".to_string(), FlagValue::Disabled),
+ ("namespace_two/android.flag_one".to_string(), FlagValue::Enabled),
+ ]);
+ let actual = parse_device_config(input).unwrap();
+ assert_eq!(expected, actual);
+ }
+}
diff --git a/aflags/src/load_protos.rs b/aflags/src/load_protos.rs
new file mode 100644
index 0000000..c5ac8ff
--- /dev/null
+++ b/aflags/src/load_protos.rs
@@ -0,0 +1,72 @@
+use crate::{Flag, FlagPermission, FlagValue, ValuePickedFrom};
+use aconfig_protos::ProtoFlagPermission as ProtoPermission;
+use aconfig_protos::ProtoFlagState as ProtoState;
+use aconfig_protos::ProtoParsedFlag;
+use aconfig_protos::ProtoParsedFlags;
+use anyhow::Result;
+use std::fs;
+use std::path::Path;
+
+// TODO(b/329875578): use container field directly instead of inferring.
+fn infer_container(path: &Path) -> String {
+ let path_str = path.to_string_lossy();
+ path_str
+ .strip_prefix("/apex/")
+ .or_else(|| path_str.strip_prefix('/'))
+ .unwrap_or(&path_str)
+ .strip_suffix("/etc/aconfig_flags.pb")
+ .unwrap_or(&path_str)
+ .to_string()
+}
+
+fn convert_parsed_flag(path: &Path, flag: &ProtoParsedFlag) -> Flag {
+ let namespace = flag.namespace().to_string();
+ let package = flag.package().to_string();
+ let name = flag.name().to_string();
+
+ let value = match flag.state() {
+ ProtoState::ENABLED => FlagValue::Enabled,
+ ProtoState::DISABLED => FlagValue::Disabled,
+ };
+
+ let permission = match flag.permission() {
+ ProtoPermission::READ_ONLY => FlagPermission::ReadOnly,
+ ProtoPermission::READ_WRITE => FlagPermission::ReadWrite,
+ };
+
+ Flag {
+ namespace,
+ package,
+ name,
+ container: infer_container(path),
+ value,
+ staged_value: None,
+ permission,
+ value_picked_from: ValuePickedFrom::Default,
+ }
+}
+
+pub(crate) fn load() -> Result<Vec<Flag>> {
+ let mut result = Vec::new();
+
+ let paths = aconfig_device_paths::parsed_flags_proto_paths()?;
+ for path in paths {
+ let Ok(bytes) = fs::read(&path) else {
+ eprintln!("warning: failed to read {:?}", path);
+ continue;
+ };
+ let parsed_flags: ProtoParsedFlags = protobuf::Message::parse_from_bytes(&bytes)?;
+ for flag in parsed_flags.parsed_flag {
+ // TODO(b/334954748): enforce one-container-per-flag invariant.
+ result.push(convert_parsed_flag(&path, &flag));
+ }
+ }
+ Ok(result)
+}
+
+pub(crate) fn list_containers() -> Result<Vec<String>> {
+ Ok(aconfig_device_paths::parsed_flags_proto_paths()?
+ .into_iter()
+ .map(|p| infer_container(&p))
+ .collect())
+}
diff --git a/aflags/src/main.rs b/aflags/src/main.rs
new file mode 100644
index 0000000..8173bc2
--- /dev/null
+++ b/aflags/src/main.rs
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+//! `aflags` is a device binary to read and write aconfig flags.
+
+use anyhow::{anyhow, ensure, Result};
+use clap::Parser;
+
+mod device_config_source;
+use device_config_source::DeviceConfigSource;
+
+mod aconfig_storage_source;
+use aconfig_storage_source::AconfigStorageSource;
+
+mod load_protos;
+
+#[derive(Clone, PartialEq, Debug)]
+enum FlagPermission {
+ ReadOnly,
+ ReadWrite,
+}
+
+impl std::fmt::Display for FlagPermission {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match &self {
+ Self::ReadOnly => "read-only",
+ Self::ReadWrite => "read-write",
+ }
+ )
+ }
+}
+
+#[derive(Clone, Debug)]
+enum ValuePickedFrom {
+ Default,
+ Server,
+ Local,
+}
+
+impl std::fmt::Display for ValuePickedFrom {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match &self {
+ Self::Default => "default",
+ Self::Server => "server",
+ Self::Local => "local",
+ }
+ )
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+enum FlagValue {
+ Enabled,
+ Disabled,
+}
+
+impl TryFrom<&str> for FlagValue {
+ type Error = anyhow::Error;
+
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
+ match value {
+ "true" | "enabled" => Ok(Self::Enabled),
+ "false" | "disabled" => Ok(Self::Disabled),
+ _ => Err(anyhow!("cannot convert string '{}' to FlagValue", value)),
+ }
+ }
+}
+
+impl std::fmt::Display for FlagValue {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match &self {
+ Self::Enabled => "enabled",
+ Self::Disabled => "disabled",
+ }
+ )
+ }
+}
+
+#[derive(Clone, Debug)]
+struct Flag {
+ namespace: String,
+ name: String,
+ package: String,
+ container: String,
+ value: FlagValue,
+ staged_value: Option<FlagValue>,
+ permission: FlagPermission,
+ value_picked_from: ValuePickedFrom,
+}
+
+impl Flag {
+ fn qualified_name(&self) -> String {
+ format!("{}.{}", self.package, self.name)
+ }
+
+ fn display_staged_value(&self) -> String {
+ match (&self.permission, self.staged_value) {
+ (FlagPermission::ReadOnly, _) => "-".to_string(),
+ (FlagPermission::ReadWrite, None) => "-".to_string(),
+ (FlagPermission::ReadWrite, Some(v)) => format!("(->{})", v),
+ }
+ }
+}
+
+trait FlagSource {
+ fn list_flags() -> Result<Vec<Flag>>;
+ fn override_flag(namespace: &str, qualified_name: &str, value: &str) -> Result<()>;
+}
+
+enum FlagSourceType {
+ DeviceConfig,
+ AconfigStorage,
+}
+
+const ABOUT_TEXT: &str = "Tool for reading and writing flags.
+
+Rows in the table from the `list` command follow this format:
+
+ package flag_name value provenance permission container
+
+ * `package`: package set for this flag in its .aconfig definition.
+ * `flag_name`: flag name, also set in definition.
+ * `value`: the value read from the flag.
+ * `staged_value`: the value on next boot:
+ + `-`: same as current value
+ + `(->enabled) flipped to enabled on boot.
+ + `(->disabled) flipped to disabled on boot.
+ * `provenance`: one of:
+ + `default`: the flag value comes from its build-time default.
+ + `server`: the flag value comes from a server override.
+ * `permission`: read-write or read-only.
+ * `container`: the container for the flag, configured in its definition.
+";
+
+#[derive(Parser, Debug)]
+#[clap(long_about=ABOUT_TEXT)]
+struct Cli {
+ #[clap(subcommand)]
+ command: Command,
+}
+
+#[derive(Parser, Debug)]
+enum Command {
+ /// List all aconfig flags on this device.
+ List {
+ /// Optionally filter by container name.
+ #[clap(short = 'c', long = "container")]
+ container: Option<String>,
+ },
+
+ /// Enable an aconfig flag on this device, on the next boot.
+ Enable {
+ /// <package>.<flag_name>
+ qualified_name: String,
+ },
+
+ /// Disable an aconfig flag on this device, on the next boot.
+ Disable {
+ /// <package>.<flag_name>
+ qualified_name: String,
+ },
+
+ /// Display which flag storage backs aconfig flags.
+ WhichBacking,
+}
+
+struct PaddingInfo {
+ longest_flag_col: usize,
+ longest_val_col: usize,
+ longest_staged_val_col: usize,
+ longest_value_picked_from_col: usize,
+ longest_permission_col: usize,
+}
+
+struct Filter {
+ container: Option<String>,
+}
+
+impl Filter {
+ fn apply(&self, flags: &[Flag]) -> Vec<Flag> {
+ flags
+ .iter()
+ .filter(|flag| match &self.container {
+ Some(c) => flag.container == *c,
+ None => true,
+ })
+ .cloned()
+ .collect()
+ }
+}
+
+fn format_flag_row(flag: &Flag, info: &PaddingInfo) -> String {
+ let full_name = flag.qualified_name();
+ let p0 = info.longest_flag_col + 1;
+
+ let val = flag.value.to_string();
+ let p1 = info.longest_val_col + 1;
+
+ let staged_val = flag.display_staged_value();
+ let p2 = info.longest_staged_val_col + 1;
+
+ let value_picked_from = flag.value_picked_from.to_string();
+ let p3 = info.longest_value_picked_from_col + 1;
+
+ let perm = flag.permission.to_string();
+ let p4 = info.longest_permission_col + 1;
+
+ let container = &flag.container;
+
+ format!(
+ "{full_name:p0$}{val:p1$}{staged_val:p2$}{value_picked_from:p3$}{perm:p4$}{container}\n"
+ )
+}
+
+fn set_flag(qualified_name: &str, value: &str) -> Result<()> {
+ let flags_binding = DeviceConfigSource::list_flags()?;
+ let flag = flags_binding.iter().find(|f| f.qualified_name() == qualified_name).ok_or(
+ anyhow!("no aconfig flag '{qualified_name}'. Does the flag have an .aconfig definition?"),
+ )?;
+
+ ensure!(flag.permission == FlagPermission::ReadWrite,
+ format!("could not write flag '{qualified_name}', it is read-only for the current release configuration."));
+
+ DeviceConfigSource::override_flag(&flag.namespace, qualified_name, value)?;
+
+ Ok(())
+}
+
+fn list(source_type: FlagSourceType, container: Option<String>) -> Result<String> {
+ let flags_unfiltered = match source_type {
+ FlagSourceType::DeviceConfig => DeviceConfigSource::list_flags()?,
+ FlagSourceType::AconfigStorage => AconfigStorageSource::list_flags()?,
+ };
+
+ if let Some(ref c) = container {
+ ensure!(
+ load_protos::list_containers()?.contains(c),
+ format!("container '{}' not found", &c)
+ );
+ }
+
+ let flags = (Filter { container }).apply(&flags_unfiltered);
+ let padding_info = PaddingInfo {
+ longest_flag_col: flags.iter().map(|f| f.qualified_name().len()).max().unwrap_or(0),
+ longest_val_col: flags.iter().map(|f| f.value.to_string().len()).max().unwrap_or(0),
+ longest_staged_val_col: flags
+ .iter()
+ .map(|f| f.display_staged_value().len())
+ .max()
+ .unwrap_or(0),
+ longest_value_picked_from_col: flags
+ .iter()
+ .map(|f| f.value_picked_from.to_string().len())
+ .max()
+ .unwrap_or(0),
+ longest_permission_col: flags
+ .iter()
+ .map(|f| f.permission.to_string().len())
+ .max()
+ .unwrap_or(0),
+ };
+
+ let mut result = String::from("");
+ for flag in flags {
+ let row = format_flag_row(&flag, &padding_info);
+ result.push_str(&row);
+ }
+ Ok(result)
+}
+
+fn display_which_backing() -> String {
+ if aconfig_flags::auto_generated::enable_only_new_storage() {
+ "aconfig_storage".to_string()
+ } else {
+ "device_config".to_string()
+ }
+}
+
+fn main() -> Result<()> {
+ ensure!(nix::unistd::Uid::current().is_root(), "must be root");
+
+ let cli = Cli::parse();
+ let output = match cli.command {
+ Command::List { container } => {
+ if aconfig_flags::auto_generated::enable_only_new_storage() {
+ list(FlagSourceType::AconfigStorage, container)
+ .map_err(|err| anyhow!("could not list flags: {err}"))
+ .map(Some)
+ } else {
+ list(FlagSourceType::DeviceConfig, container).map(Some)
+ }
+ }
+ Command::Enable { qualified_name } => set_flag(&qualified_name, "true").map(|_| None),
+ Command::Disable { qualified_name } => set_flag(&qualified_name, "false").map(|_| None),
+ Command::WhichBacking => Ok(Some(display_which_backing())),
+ };
+ match output {
+ Ok(Some(text)) => println!("{text}"),
+ Ok(None) => (),
+ Err(message) => println!("Error: {message}"),
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_filter_container() {
+ let flags = vec![
+ Flag {
+ namespace: "namespace".to_string(),
+ name: "test1".to_string(),
+ package: "package".to_string(),
+ value: FlagValue::Disabled,
+ staged_value: None,
+ permission: FlagPermission::ReadWrite,
+ value_picked_from: ValuePickedFrom::Default,
+ container: "system".to_string(),
+ },
+ Flag {
+ namespace: "namespace".to_string(),
+ name: "test2".to_string(),
+ package: "package".to_string(),
+ value: FlagValue::Disabled,
+ staged_value: None,
+ permission: FlagPermission::ReadWrite,
+ value_picked_from: ValuePickedFrom::Default,
+ container: "not_system".to_string(),
+ },
+ Flag {
+ namespace: "namespace".to_string(),
+ name: "test3".to_string(),
+ package: "package".to_string(),
+ value: FlagValue::Disabled,
+ staged_value: None,
+ permission: FlagPermission::ReadWrite,
+ value_picked_from: ValuePickedFrom::Default,
+ container: "system".to_string(),
+ },
+ ];
+
+ assert_eq!((Filter { container: Some("system".to_string()) }).apply(&flags).len(), 2);
+ }
+
+ #[test]
+ fn test_filter_no_container() {
+ let flags = vec![
+ Flag {
+ namespace: "namespace".to_string(),
+ name: "test1".to_string(),
+ package: "package".to_string(),
+ value: FlagValue::Disabled,
+ staged_value: None,
+ permission: FlagPermission::ReadWrite,
+ value_picked_from: ValuePickedFrom::Default,
+ container: "system".to_string(),
+ },
+ Flag {
+ namespace: "namespace".to_string(),
+ name: "test2".to_string(),
+ package: "package".to_string(),
+ value: FlagValue::Disabled,
+ staged_value: None,
+ permission: FlagPermission::ReadWrite,
+ value_picked_from: ValuePickedFrom::Default,
+ container: "not_system".to_string(),
+ },
+ Flag {
+ namespace: "namespace".to_string(),
+ name: "test3".to_string(),
+ package: "package".to_string(),
+ value: FlagValue::Disabled,
+ staged_value: None,
+ permission: FlagPermission::ReadWrite,
+ value_picked_from: ValuePickedFrom::Default,
+ container: "system".to_string(),
+ },
+ ];
+
+ assert_eq!((Filter { container: None }).apply(&flags).len(), 3);
+ }
+}
diff --git a/apex/Android.bp b/apex/Android.bp
index 8b564da..88971f3 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -100,6 +100,7 @@
file_contexts: ":com.android.configinfrastructure-file_contexts",
binaries: [
"aconfigd-mainline",
+ "aflags_updatable",
],
prebuilts: [
"com.android.configinfrastrcture.init.rc",
diff --git a/framework/java/android/os/flagging/AconfigPackage.java b/framework/java/android/os/flagging/AconfigPackage.java
index a730bb7..824d28d 100644
--- a/framework/java/android/os/flagging/AconfigPackage.java
+++ b/framework/java/android/os/flagging/AconfigPackage.java
@@ -16,6 +16,7 @@
package android.os.flagging;
+import static android.aconfig.storage.TableUtils.StorageFilesBundle;
import static android.provider.flags.Flags.FLAG_NEW_STORAGE_PUBLIC_API;
import static android.provider.flags.Flags.readPlatformFromPlatformApi;
@@ -26,7 +27,6 @@
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.os.Build;
-import android.os.StrictMode;
import android.util.Log;
import java.io.Closeable;
@@ -55,7 +55,6 @@
private static final String BOOT_PATH = "/metadata/aconfig/boot/";
private static final String PMAP_FILE_EXT = ".package.map";
- private static final Map<String, PackageTable> sPackageTableCache = new HashMap<>();
private static final boolean READ_PLATFORM_FROM_PLATFORM_API =
readPlatformFromPlatformApi() && Build.VERSION.SDK_INT > 35;
@@ -67,44 +66,36 @@
private PlatformAconfigPackage mPlatformAconfigPackage = null;
+ /** @hide */
+ static final Map<String, StorageFilesBundle> sStorageFilesCache = new HashMap<>();
+
private AconfigPackage() {}
static {
File mapDir = new File(MAP_PATH);
String[] mapFiles = mapDir.list();
if (mapFiles != null) {
- if (!READ_PLATFORM_FROM_PLATFORM_API) {
- for (String file : mapFiles) {
- if (!file.endsWith(PMAP_FILE_EXT)) {
- continue;
- }
- try {
- PackageTable pTable =
- PackageTable.fromBytes(mapStorageFile(MAP_PATH + file));
- for (String packageName : pTable.getPackageList()) {
- sPackageTableCache.put(packageName, pTable);
- }
- } catch (Exception e) {
- // pass
- Log.w(TAG, e.toString());
- }
+ for (String file : mapFiles) {
+ if (!file.endsWith(PMAP_FILE_EXT)
+ || (READ_PLATFORM_FROM_PLATFORM_API
+ && PlatformAconfigPackage.PLATFORM_PACKAGE_MAP_FILES.contains(
+ file))) {
+ continue;
}
- } else {
- for (String file : mapFiles) {
- if (!file.endsWith(PMAP_FILE_EXT)
- || PlatformAconfigPackage.PLATFORM_PACKAGE_MAP_FILES.contains(file)) {
- continue;
+ try {
+ PackageTable pTable = PackageTable.fromBytes(mapStorageFile(MAP_PATH + file));
+ String container = pTable.getHeader().getContainer();
+ FlagTable fTable =
+ FlagTable.fromBytes(mapStorageFile(MAP_PATH + container + ".flag.map"));
+ FlagValueList fValueList =
+ FlagValueList.fromBytes(mapStorageFile(BOOT_PATH + container + ".val"));
+ StorageFilesBundle files = new StorageFilesBundle(pTable, fTable, fValueList);
+ for (String packageName : pTable.getPackageList()) {
+ sStorageFilesCache.put(packageName, files);
}
- try {
- PackageTable pTable =
- PackageTable.fromBytes(mapStorageFile(MAP_PATH + file));
- for (String packageName : pTable.getPackageList()) {
- sPackageTableCache.put(packageName, pTable);
- }
- } catch (Exception e) {
- // pass
- Log.w(TAG, e.toString());
- }
+ } catch (Exception e) {
+ // pass
+ Log.w(TAG, e.toString());
}
}
}
@@ -125,7 +116,6 @@
*/
@FlaggedApi(FLAG_NEW_STORAGE_PUBLIC_API)
public static @NonNull AconfigPackage load(@NonNull String packageName) {
- StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
AconfigPackage aconfigPackage = new AconfigPackage();
@@ -136,18 +126,16 @@
}
}
- PackageTable pTable = sPackageTableCache.get(packageName);
- if (pTable == null) {
+ StorageFilesBundle files = sStorageFilesCache.get(packageName);
+ if (files == null) {
throw new AconfigStorageReadException(
AconfigStorageReadException.ERROR_PACKAGE_NOT_FOUND,
"package " + packageName + " cannot be found on the device");
}
- PackageTable.Node pNode = pTable.get(packageName);
- String container = pTable.getHeader().getContainer();
- aconfigPackage.mFlagTable =
- FlagTable.fromBytes(mapStorageFile(MAP_PATH + container + ".flag.map"));
- aconfigPackage.mFlagValueList =
- FlagValueList.fromBytes(mapStorageFile(BOOT_PATH + container + ".val"));
+
+ PackageTable.Node pNode = files.packageTable.get(packageName);
+ aconfigPackage.mFlagTable = files.flagTable;
+ aconfigPackage.mFlagValueList = files.flagValueList;
aconfigPackage.mPackageBooleanStartOffset = pNode.getBooleanStartIndex();
aconfigPackage.mPackageId = pNode.getPackageId();
return aconfigPackage;
@@ -159,8 +147,6 @@
} catch (Exception e) {
throw new AconfigStorageReadException(
AconfigStorageReadException.ERROR_GENERIC, "Fail to create AconfigPackage", e);
- } finally {
- StrictMode.setThreadPolicy(oldPolicy);
}
}
diff --git a/framework/java/android/os/flagging/AconfigPackageInternal.java b/framework/java/android/os/flagging/AconfigPackageInternal.java
index f460423..e531199 100644
--- a/framework/java/android/os/flagging/AconfigPackageInternal.java
+++ b/framework/java/android/os/flagging/AconfigPackageInternal.java
@@ -16,13 +16,13 @@
package android.os.flagging;
+import static android.aconfig.storage.TableUtils.StorageFilesBundle;
+
import android.aconfig.storage.AconfigStorageException;
import android.aconfig.storage.FlagValueList;
import android.aconfig.storage.PackageTable;
-import android.aconfig.storage.StorageFileProvider;
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
-import android.os.StrictMode;
/**
* An {@code aconfig} package containing the enabled state of its flags.
@@ -56,7 +56,6 @@
* <p>This method is intended for internal use only and may be changed or removed without
* notice.
*
- * @param container The name of the container.
* @param packageName The name of the Aconfig package.
* @param packageFingerprint The expected fingerprint of the package.
* @return An instance of {@link AconfigPackageInternal} representing the loaded package.
@@ -64,50 +63,20 @@
*/
@UnsupportedAppUsage
public static @NonNull AconfigPackageInternal load(
- @NonNull String container, @NonNull String packageName, long packageFingerprint) {
- return load(
- container,
- packageName,
- packageFingerprint,
- StorageFileProvider.getDefaultProvider());
- }
-
- /** @hide */
- public static @NonNull AconfigPackageInternal load(
- @NonNull String container,
- @NonNull String packageName,
- long packageFingerprint,
- @NonNull StorageFileProvider fileProvider) {
- StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
- PackageTable.Node pNode = null;
- FlagValueList vList = null;
- try {
- pNode = fileProvider.getPackageTable(container).get(packageName);
- vList = fileProvider.getFlagValueList(container);
- } finally {
- StrictMode.setThreadPolicy(oldPolicy);
- }
-
- if (pNode == null || vList == null) {
+ @NonNull String packageName, long packageFingerprint) {
+ StorageFilesBundle files = AconfigPackage.sStorageFilesCache.get(packageName);
+ if (files == null) {
throw new AconfigStorageException(
AconfigStorageException.ERROR_PACKAGE_NOT_FOUND,
- String.format(
- "package "
- + packageName
- + " in container "
- + container
- + " cannot be found on the device"));
+ "package " + packageName + " cannot be found on the device");
}
+ PackageTable.Node pNode = files.packageTable.get(packageName);
+ FlagValueList vList = files.flagValueList;
if (pNode.hasPackageFingerprint() && packageFingerprint != pNode.getPackageFingerprint()) {
throw new AconfigStorageException(
AconfigStorageException.ERROR_FILE_FINGERPRINT_MISMATCH,
- String.format(
- "package "
- + packageName
- + " in container "
- + container
- + " cannot be found on the device"));
+ "package " + packageName + "fingerprint doesn't match the one on device");
}
return new AconfigPackageInternal(vList, pNode.getBooleanStartIndex());
diff --git a/framework/tests/Android.bp b/framework/tests/Android.bp
index 0a64d69..bd15197 100644
--- a/framework/tests/Android.bp
+++ b/framework/tests/Android.bp
@@ -20,7 +20,7 @@
name: "AconfigPackageTests",
srcs: ["src/**/*.java"],
static_libs: [
- "aconfig_device_paths_java",
+ "aconfig_device_paths_java_util",
"androidx.test.rules",
"aconfig_storage_file_java",
"junit",
diff --git a/framework/tests/src/AconfigPackageInternalTests.java b/framework/tests/src/AconfigPackageInternalTests.java
index 47b74d2..5d5d362 100644
--- a/framework/tests/src/AconfigPackageInternalTests.java
+++ b/framework/tests/src/AconfigPackageInternalTests.java
@@ -19,7 +19,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
-import android.aconfig.DeviceProtos;
+import android.aconfig.DeviceProtosTestUtil;
import android.aconfig.nano.Aconfig;
import android.aconfig.nano.Aconfig.parsed_flag;
import android.aconfig.storage.AconfigStorageException;
@@ -42,7 +42,7 @@
public class AconfigPackageInternalTests {
@Test
public void testAconfigPackageInternal_load() throws IOException {
- List<parsed_flag> flags = DeviceProtos.loadAndParseFlagProtos();
+ List<parsed_flag> flags = DeviceProtosTestUtil.loadAndParseFlagProtos();
Map<String, AconfigPackageInternal> readerMap = new HashMap<>();
StorageFileProvider fp = StorageFileProvider.getDefaultProvider();
@@ -67,7 +67,7 @@
AconfigPackageInternal reader = readerMap.get(packageName);
if (reader == null) {
- reader = AconfigPackageInternal.load(container, packageName, fingerprint);
+ reader = AconfigPackageInternal.load(packageName, fingerprint);
readerMap.put(packageName, reader);
}
boolean jVal = reader.getBooleanFlagValue(fNode.getFlagIndex());
@@ -78,22 +78,15 @@
@Test
public void testAconfigPackageInternal_load_withError() throws IOException {
- // container not found fake_container
+ // package not found
AconfigStorageException e =
assertThrows(
AconfigStorageException.class,
- () -> AconfigPackageInternal.load("fake_container", "fake_package", 0));
- assertEquals(AconfigStorageException.ERROR_CANNOT_READ_STORAGE_FILE, e.getErrorCode());
-
- // package not found
- e =
- assertThrows(
- AconfigStorageException.class,
- () -> AconfigPackageInternal.load("system", "fake_container", 0));
+ () -> AconfigPackageInternal.load("fake_package", 0));
assertEquals(AconfigStorageException.ERROR_PACKAGE_NOT_FOUND, e.getErrorCode());
// fingerprint doesn't match
- List<parsed_flag> flags = DeviceProtos.loadAndParseFlagProtos();
+ List<parsed_flag> flags = DeviceProtosTestUtil.loadAndParseFlagProtos();
StorageFileProvider fp = StorageFileProvider.getDefaultProvider();
parsed_flag flag = flags.get(0);
@@ -108,9 +101,7 @@
e =
assertThrows(
AconfigStorageException.class,
- () ->
- AconfigPackageInternal.load(
- container, packageName, fingerprint + 1));
+ () -> AconfigPackageInternal.load(packageName, fingerprint + 1));
assertEquals(AconfigStorageException.ERROR_FILE_FINGERPRINT_MISMATCH, e.getErrorCode());
}
}
diff --git a/framework/tests/src/AconfigPackageTests.java b/framework/tests/src/AconfigPackageTests.java
index 99243d2..45c0fdb 100644
--- a/framework/tests/src/AconfigPackageTests.java
+++ b/framework/tests/src/AconfigPackageTests.java
@@ -17,9 +17,10 @@
package android.os.flagging.test;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
-import android.aconfig.DeviceProtos;
+import android.aconfig.DeviceProtosTestUtil;
import android.aconfig.nano.Aconfig;
import android.aconfig.nano.Aconfig.parsed_flag;
import android.aconfig.storage.FlagTable;
@@ -40,9 +41,23 @@
@RunWith(JUnit4.class)
public class AconfigPackageTests {
+
+ @Test
+ public void testAconfigPackage_StorageFilesCache() throws IOException {
+ List<parsed_flag> flags = DeviceProtosTestUtil.loadAndParseFlagProtos();
+ for (parsed_flag flag : flags) {
+ if (flag.permission == Aconfig.READ_ONLY && flag.state == Aconfig.DISABLED) {
+ continue;
+ }
+ String container = flag.container;
+ String packageName = flag.package_;
+ assertNotNull(AconfigPackage.load(packageName));
+ }
+ }
+
@Test
public void testExternalAconfigPackageInstance() throws IOException {
- List<parsed_flag> flags = DeviceProtos.loadAndParseFlagProtos();
+ List<parsed_flag> flags = DeviceProtosTestUtil.loadAndParseFlagProtos();
Map<String, AconfigPackage> readerMap = new HashMap<>();
StorageFileProvider fp = StorageFileProvider.getDefaultProvider();