前言

关于 DeviceConfig 这个东西,网上几乎没有什么描述它的内容,于是在这里小小的科普一下,来做一点微小的贡献。

Q & A

打算以 Q & A 的形式展现内容,因为这样比较好写。

它是什么?

说白了,它就是一张会被持久化保存的数据表。
这张数据表有三列,每行代表一个数据:

p

“命名空间”和“名称”共同构成这张数据表的“主键”,也就是说,只要确定了“命名空间”和“名称”,那么“值”就唯一确定,不能有“命名空间”和“名称”完全相同的两行。
上表的内容是随便挑的,假如不主动往里面存东西,它就是一张空表。另外,你也可以往里面塞任何字符串,而只有那些被系统监听的项目才会对系统功能产生影响。

在存储时,需要同时指定“命名空间”和“名称”,然后把值放进去。
在读取时,需要同时指定“命名空间”和“名称”,然后把值读出来。
有没有觉得和 Settings 还有 prop 很像?没错,说白了就是从字符串到字符串的映射罢了,只不过这次的 key 是两个字符串组合在了一起。

它该如何被操作?

受限于篇幅,这里只讲命令行下的调试接口,不讲 api。

Device Config (device_config) commands:
  help
      Print this help text.
  get NAMESPACE KEY
      Retrieve the current value of KEY from the given NAMESPACE.
  put NAMESPACE KEY VALUE [default]
      Change the contents of KEY to VALUE for the given NAMESPACE.
      {default} to set as the default value.
  delete NAMESPACE KEY
      Delete the entry for KEY for the given NAMESPACE.
  list [NAMESPACE]
      Print all keys and values defined, optionally for the given NAMESPACE.
  reset RESET_MODE [NAMESPACE]
      Reset all flag values, optionally for a NAMESPACE, according to RESET_MODE.
      RESET_MODE is one of {untrusted_defaults, untrusted_clear, trusted_defaults}
      NAMESPACE limits which flags are reset if provided, otherwise all flags are reset
  set_sync_disabled_for_tests SYNC_DISABLED_MODE
      Modifies bulk property setting behavior for tests. When in one of the disabled modes this ensures that config isn't overwritten.
      SYNC_DISABLED_MODE is one of:
        none: Sync is not disabled. A reboot may be required to restart syncing.
        persistent: Sync is disabled, this state will survive a reboot.
        until_reboot: Sync is disabled until the next reboot.
  is_sync_disabled_for_tests
      Prints 'true' if sync is disabled, 'false' otherwise.

device_config put NAMESPACE KEY VALUE [default]
这个命令用于存储一个数据项,至少需要携带三个参数,分别是数据项的“命名空间”、“名称”和需要存储的“值”。
(这里最后的 default 是可选的,而且是要输入真正的 “default” 而不是 true false。比如 adb shell device_config put privacy location_indicators_enabled true default。加上 default 表示这个值要被作为默认值,有默认值时 reset 会将其设置为默认值。不只是 DeviceConfig ,其实 Settings 也有这样的设计,在下面你将可以看到这俩之间的血缘关系。个人认为这其实是一种过度设计,因为我从来没有看到过它的真正用途。。。因此对 defaultreset 的介绍就到此为止,下面不再提及。)

device_config put privacy location_indicators_enabled true 为例,命令中的 privacy 是指 NAMESPACE,也就是这个数据项的命名空间,而 location_indicators_enabled 是指 KEY 也就是这个数据项的名称,而最后的 true 便是 VALUE 了,也就是将要存储的值。

再来看看别的命令:
device_config get NAMESPACE KEY
这个命令用于读取一个数据项,需要两个参数,分别是数据项的命名空间和名称。

还有别的:
device_config delete NAMESPACE KEY 用于删除一个数据项,需要指定数据项的命名空间和名称。
device_config list [NAMESPACE] 则用于展示(某个命名空间中的)全部数据项,需要注意的是 NAMESPACE 是可选参数,如果不写上则可以显示全部命名空间中的数据项。

至于 device_config set_sync_disabled_for_testsdevice_config is_sync_disabled_for_tests ,这里暂且不提,会在下面介绍 DeviceConfig 的设计初衷时再提到它们。

它工作在哪里?

它,由 SettingsProvider 驱动,以 ContentProvider 的形式工作在 java 层。事实上,它可以有另一个名称,那就是 Settings.Config

core/java/android/provider/DeviceConfig.java

    public static Properties getProperties(@NonNull String namespace, @NonNull String ... names) {
        ContentResolver contentResolver = ActivityThread.currentApplication().getContentResolver();
        return new Properties(namespace,
                Settings.Config.getStrings(contentResolver, namespace, Arrays.asList(names)));
    }

没错,Settings 除了 System、Global、Secure 三张常见的表之外,还包括 Config 这张表,而它正是 DeviceConfig 的下层接口。

哦对了,我们还可以看看它存在哪里:

packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java

    private static final String SETTINGS_FILE_GLOBAL = "settings_global.xml";
    private static final String SETTINGS_FILE_SYSTEM = "settings_system.xml";
    private static final String SETTINGS_FILE_SECURE = "settings_secure.xml";
    private static final String SETTINGS_FILE_SSAID = "settings_ssaid.xml";
    private static final String SETTINGS_FILE_CONFIG = "settings_config.xml";
    private File getSettingsFile(int key) {
        if (isConfigSettingsKey(key)) {
            final int userId = getUserIdFromKey(key);
            return new File(Environment.getUserSystemDirectory(userId),
                    SETTINGS_FILE_CONFIG);
        } else if (isGlobalSettingsKey(key)) {
            final int userId = getUserIdFromKey(key);
            return new File(Environment.getUserSystemDirectory(userId),
                    SETTINGS_FILE_GLOBAL);
        } else if (isSystemSettingsKey(key)) {
            final int userId = getUserIdFromKey(key);
            return new File(Environment.getUserSystemDirectory(userId),
                    SETTINGS_FILE_SYSTEM);
        } else if (isSecureSettingsKey(key)) {
            final int userId = getUserIdFromKey(key);
            return new File(Environment.getUserSystemDirectory(userId),
                    SETTINGS_FILE_SECURE);
        } else if (isSsaidSettingsKey(key)) {
            final int userId = getUserIdFromKey(key);
            return new File(Environment.getUserSystemDirectory(userId),
                    SETTINGS_FILE_SSAID);
        } else {
            throw new IllegalArgumentException("Invalid settings key:" + key);
        }
    }

对于主用户来说,是在 /data/system/users/0/settings_config.xml 和别的设置表完全就是在一个地方。

事实上,在 Settings.Config 的实现中,DeviceConfig 的“命名空间”和“名称”是被 / 连接之后作为真正的“设置项名称”进行保存的:

core/java/android/provider/Settings.java

    private static String createCompositeName(@NonNull String namespace, @NonNull String name) {
        Preconditions.checkNotNull(namespace);
        Preconditions.checkNotNull(name);
        return createPrefix(namespace) + name;
    }

    private static String createPrefix(@NonNull String namespace) {
        Preconditions.checkNotNull(namespace);
        return namespace + "/";
    }

也就是说,如果用命令来表示的话:
device_config put privacy location_indicators_enabled true
相当于
settings put config privacy/location_indicators_enabled true
虽然第二条命令实际上没有被实现,但是从逻辑上看,就是这样的。

为什么要有它?

好问题,没办法进入设计者的脑子一探究竟,只能推测。

目前看起来,它被设计成一个更底层的“设置”。
众所周知,“设置”是给用户调的,那 DeviceConfig 是给谁调的?答案是厂家和开发者。

有两种方式可以对它进行调整(指正常状况下),一种是使用 adb 执行上面的 device_config 命令,另一种则是持有 android.permission.WRITE_DEVICE_CONFIG 这一权限,那么则可以直接调用相关 api。

谁会持有这一权限呢?GMS。

p

这就很有意思,GMS 竟然偷偷利用这个权限去改变第三方系统启用的特性。第三方系统还要特地砍掉 GMS 的这个权限来防止它发生。
实际上,DeviceConfig 就是谷歌给自己留下的“云控”接口,一方面是可以远程控制各安装了 GMS 的设备的系统特性,另一方面则是配合 updatable apex 在系统模块更新时对系统特性做出针对性的调整。嗯,这个东西是在 Android 10,伴随着 updatable apex 的诞生而诞生的。

那为什么不直接调整 Settings 呢?
总的来说,DeviceConfig 是在 Settings 基础上进一步进行的封装,它的设计比较有功能针对性。

首先,每个“命名空间”对应一个系统功能模块,对应的功能模块只需要监听单个“命名空间”的变化即可,不再需要像原始的 Settings 那样需要监听一大堆的设置项,这在代码上就优雅了许多:

    DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
            ActivityThread.currentApplication().getMainExecutor(), mOnFlagsChangedListener);

假如模块出现了问题,也只需要重置那一整个命名空间即可,并不需要写死相关的设置项的列表,比如模块回滚机制中对命名空间的清除:

services/core/java/com/android/server/RescueParty.java

    /**
     * Called when {@code RollbackManager} performs Mainline module rollbacks,
     * to avoid rolled back modules consuming flag values only expected to work
     * on modules of newer versions.
     */
    public static void resetDeviceConfigForPackages(List<String> packageNames) {
        ......
        while (namespaceIt.hasNext()) {
            ......
                DeviceConfig.setProperties(properties);
            ......
        }
    }

Settings 往往是被限制在 java 层的,但是 DeviceConfig 中指定的命名空间却可以通过 prop 映射来对 native 层可见:

services/core/java/com/android/server/am/SettingsToPropertiesMapper.java

    // All the flags under the listed DeviceConfig scopes will be synced to native level.
    //
    // NOTE: please grant write permission system property prefix
    // with format persist.device_config.[device_config_scope]. in system_server.te and grant read
    // permission in the corresponding .te file your feature belongs to.
    @VisibleForTesting
    static final String[] sDeviceConfigScopes = new String[] {
        DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
        DeviceConfig.NAMESPACE_CONFIGURATION,
        DeviceConfig.NAMESPACE_CONNECTIVITY,
        DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT,
        DeviceConfig.NAMESPACE_INTELLIGENCE_CONTENT_SUGGESTIONS,
        DeviceConfig.NAMESPACE_LMKD_NATIVE,
        DeviceConfig.NAMESPACE_MEDIA_NATIVE,
        DeviceConfig.NAMESPACE_NETD_NATIVE,
        DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT,
        DeviceConfig.NAMESPACE_RUNTIME_NATIVE,
        DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT,
        DeviceConfig.NAMESPACE_STATSD_NATIVE,
        DeviceConfig.NAMESPACE_STATSD_NATIVE_BOOT,
        DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
        DeviceConfig.NAMESPACE_WINDOW_MANAGER_NATIVE_BOOT,
    };

这种映射会将 DeviceConfig 映射成 persist.device_config.命名空间(NAMESPACE).名称(KEY)=值(VALUE) 的 prop 形式,比如在
device_config put activity_manager_native_boot offload_queue_enabled true
之后就会多出来一条
persist.device_config.activity_manager_native_boot.offload_queue_enabled=true 的 prop 。

这种映射只针对上面这张表中指定的命名空间生效,这些命名空间往往包含 “native” 字样,表示是这个命名空间属于 native 层服务。
(哦对了,这种映射的单向的,并不能通过改变 prop 来改变 DeviceConfig。因此直接往 prop 中写入 persist.device_config.xxx 来改变系统特性的做法其实只是搭了 DeviceConfig 的便车,并不是它的原始设计)

上面这一系列的设计使得 DeviceConfig 在“控制系统特性”这一方面,相对于不管是原始的 Settings 还是 prop 都有着与生俱来的优势。相对于 Settings,它的覆盖面更广。相对于 prop ,它有着更标准 api,更容易的监听,更精准的权限要求,比如,它可以通过 adb 调试。

哦对了,还记得上面的 device_config set_sync_disabled_for_tests 吗?
还记得上面所说的“云控”吗?
这个命令所做的事情,便是阻止 DeviceConfig 的批量覆盖,从而使得“云控”失效,因此本地调试可以更好的进行。毕竟,你也不想看到自己正在测试的内容被“云控”覆盖掉吧?

用法是:
device_config set_sync_disabled_for_tests none 允许“云控”。
device_config set_sync_disabled_for_tests until_reboot 关闭“云控”直到重启。
device_config set_sync_disabled_for_tests persistent 永久关闭“云控”。
剩下的 device_config is_sync_disabled_for_tests 便是获取当前“云控”阻止状态。

回到标题,所以为什么要有它呢?只是为了更优雅的开后门罢了(笑

一点题外话

"device_config" 是什么?

device_config 乍一看应该是一个可执行文件,它的确是一个可执行文件,但是

cmds/device_config/Android.bp

sh_binary {
    name: "device_config",
    src: "device_config",
}

cmds/device_config/device_config

#!/system/bin/sh
cmd device_config "$@"

它仅仅只是一个脚本,把所有的参数原封不动的转发给了 cmd device_config(比如 device_config fuck == cmd device_config fuck)。

cmd 服务名 参数 可以创建一个 binder transaction,并将其 输入输出流对应的文件描述符 以及 运行参数 转发给位于不同进程的 binder 服务端。
(转发文件描述符本质上是使一组位于不同进程的文件描述符在内核中指向相同的 struct file,效果类似 dup()
于是通过在对应的服务中读取参数,向共享的输出流写入结果,就可以实现对命令的响应。虽然这个响应看起来就像执行普通的命令一样,但其实暗含了一次 IPC。

frameworks/native/cmds/cmd/cmd.cpp

int cmdMain(const std::vector<std::string_view>& argv, TextOutput& outputLog, TextOutput& errorLog,
            int in, int out, int err, RunMode runMode) {
    ......
    sp<IServiceManager> sm = defaultServiceManager();
    ......
    Vector<String16> args;
    ......
    const auto cmd = argv[serviceIdx];
    ......
    String16 serviceName = String16(cmd.data(), cmd.size());
    for (int i = serviceIdx + 1; i < argc; i++) {
        args.add(String16(argv[i].data(), argv[i].size()));
    }
    sp<IBinder> service;
    ......
        service = sm->checkService(serviceName);
    ......
    status_t error = IBinder::shellCommand(service, in, out, err, args, cb, result);
    ......
}

可以看到,服务是根据“服务名”找到的,这里我们通过 cmd 命令传入的服务名是 device_config
我们可以轻松的找到使用对应服务名注册的 binder 服务端:

frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java

    @Override
    public boolean onCreate() {
        ......
        ServiceManager.addService("device_config", new DeviceConfigService(this));
        ......
    }

这里与 binder 相关的东西就不深入介绍了,内容实在是太多,现在你所需要知道的只有:

我们上面调用的 IBinder::shellCommand() ,会将参数传递到 位于另一个进程的 名为 device_config 的服务的这里:

packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java

public final class DeviceConfigService extends Binder {
    ......
    @Override
    public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
            String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
        (new MyShellCommand(mProvider)).exec(this, in, out, err, args, callback, resultReceiver);
    }
    ......
}

那么,接下来要做的事情就是解析参数并做出对应的操作了:

    public int onCommand(String cmd) {
        if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) {
            onHelp();
            return -1;
        }

        final PrintWriter perr = getErrPrintWriter();
        boolean isValid = false;

        CommandVerb verb;
        if ("get".equalsIgnoreCase(cmd)) {
            verb = CommandVerb.GET;
        } else if ("put".equalsIgnoreCase(cmd)) {
            verb = CommandVerb.PUT;
        } else if ("delete".equalsIgnoreCase(cmd)) {
            verb = CommandVerb.DELETE;
        } else if ("list".equalsIgnoreCase(cmd)) {
            verb = CommandVerb.LIST;
            if (peekNextArg() == null) {
                isValid = true;
            }
        } else if ("reset".equalsIgnoreCase(cmd)) {
            verb = CommandVerb.RESET;
        } else if ("set_sync_disabled_for_tests".equalsIgnoreCase(cmd)) {
            verb = CommandVerb.SET_SYNC_DISABLED_FOR_TESTS;
        } else if ("is_sync_disabled_for_tests".equalsIgnoreCase(cmd)) {
            verb = CommandVerb.IS_SYNC_DISABLED_FOR_TESTS;
            if (peekNextArg() != null) {
                perr.println("Bad arguments");
                return -1;
            }
            isValid = true;
        } else {
            // invalid
            perr.println("Invalid command: " + cmd);
            return -1;
        }
        ......
    }

现在我们能够回答 “device_config 是什么?”这个问题了吧:
它只是一个脚本,可以将参数转发至 cmd 命令以与名为 device_config 的服务进行通信。

呐,其实凡是能够在命令行里交互的系统服务,基本都是走的这一套。

其实这个 device_config 系统服务的作用,只有调用系统 API 与 SettingsProvider 通信。它虽然叫这个名字,但却并没有负责与 DeviceConfig 存储之类相关的内容。它,仅仅只是一个衔接 shell 与 java 层的小服务罢了。
settings 服务也是这样的哦,这俩可是一对)

后记

本文是打算给“暂停执行已缓存的应用”做铺垫的,只是不知道它什么时候能写出来。。。