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();