MTK 平台,客户自定义了很多ro.product 开头属性,要求系统实现可读可写。
文章目录
- 前言-需求
- [一、 属性相关基本知识点-参考资料](#一、 属性相关基本知识点-参考资料)
- 二、思路
- [三、 实现方案](#三、 实现方案)
-
- 涉及到修改文件
- 实战-实现客需-保证读写
-
- 1、定义属性
- [2、拷贝授权文件property_data.json 到 system/etc 目录](#2、拷贝授权文件property_data.json 到 system/etc 目录)
- [3、拷贝/system/etc 目录授权文件 到/data/system 分区目录](#3、拷贝/system/etc 目录授权文件 到/data/system 分区目录)
- [4、解析 /data/system 分区目录 的授权文件并设置属性](#4、解析 /data/system 分区目录 的授权文件并设置属性)
- [5、在服务Service 里面拷贝授权文件并加载](#5、在服务Service 里面拷贝授权文件并加载)
- 6、适配属性获取
- [四、mtk 相关目录介绍-mnt/vendor 分区目录](#四、mtk 相关目录介绍-mnt/vendor 分区目录)
- [五、实现属性读写-adb 命令](#五、实现属性读写-adb 命令)
- 总结
前言-需求
系统适配产品应用,产品自定义了很多ro 开头的属性,部分属性需要能够更改,且在恢复出厂设置和OTA升级时候也不会改变。

三码一秘机制-ro属性要求
对于产品,要求 ro.product.cmcc_cmei 、ro.product.cmcc_andlinkauthkey、ro.product.sn、mac 四个属性不可变,作为授权机制
属性值修改难点
- 本身ro.开头定义的属性,在Android 体系里面是不允许修改的属性 是可读属性,是特殊类别的属性。
- 自己做的商显产品是MTK平台,没有
remount一说。adb 命令也无法修改。 - MTK 是没有提供工具实现修改属性值的、再加上ro 在Framework、守护进程中 都是可读,不可修改。
一、 属性相关基本知识点-参考资料
我们还是先补一补之前属性相关的知识点,基本知识点,属性在系统中用到非常广泛的。
RK-Android11-系统增加一个属性值
Framework 层属性机制Settings.System, Settings.Secure和Settings.Global存储及应用
系统拷贝文件到data分区-/data/system目录-实战拷贝资源到vendor分区
Framework-自定义服务 + AIDL 与应用通信(二)
二、思路
核心思路如下:特别特别特别重要
- 其它属性代替:既然ro. 开头属性无法设置,那么不要死磕ro.属性如何设置成功,直接用其它属性代替。
- Framework层适配:如上不是真的用其它属性替换ro 属性,是在Framework层适配,获取和设置ro 开头属性的时候,适配到其它属性上面去,来满足要求
- 如何实现刷机、恢复出厂设置、OTA升级 属性数据不丢失: 不要死磕在哪里存放哪个分区存放然后读取,mtk 确实有相应的分区,很可惜 你没有任何权限,SeLinux 限制的死死的;也不要想着服务器拉取,因为App 在 开机时候就已经获取属性进行数据同步操作。 所以,这里直接进行 内置授权文件,然后进行数据拷贝,机器通过自身SN 来匹配自己的授权数据。 就是把授权文件对应的 ro 开头属性的属性值放到授权文件里面去,每次开机拷贝一次。
三、 实现方案
涉及到修改文件
java
/device/mediatek/system/common/fise/property_data.json 授权文件,里面都是授权key 和 值
/device/mediatek/system/common/device.mk 配置属性、配置拷贝脚本:比如拷贝如上 property_data.json 到指定分区
/frameworks/base/services/core/java/com/android/device/utils/ParseYLPropertyFileService.java 负责拷贝资源的工具类 第一次开机只是把镜像中打包的property_data.json 拷贝到 对应目录,最终应用或者Framework层访问是需要访问 /data/ 分区目录,所以这里有一个拷贝动作
/frameworks/base/services/core/java/com/android/device/utils/CmccAuthKeyManager.java 解析授权文件,根据授权文件内容设置属性值到系统中保存。
frameworks/base/services/core/java/com/android/device/DeviceCtrlService.java 一个服务Service 文件,在文件里面执行如上拷贝和解析授权文件。也在服务中获取mac 地址相关操作。
/frameworks/base/core/java/android/os/SystemProperties.java 适配属性地方
实战-实现客需-保证读写
1、定义属性
路径:/device/mediatek/system/common/device.mk
java
PRODUCT_SYSTEM_DEFAULT_PROPERTIES += \
ro.product.cmcc_cmei= \
persist.sys.cmcc_cmei= \ 实际去设置和获取的属性
ro.product.cmcc_andlinkauthkey= \
persist.sys.cmcc_andlinkauthkey= \ 实际去设置和获取的属性
ro.product.sn= \
persist.sys.sn= \ 实际去设置和获取的属性
2、拷贝授权文件property_data.json 到 system/etc 目录
路径:
/device/mediatek/system/common/fise/property_data.json
/device/mediatek/system/common/device.mk
拷贝授权文件:
java
PRODUCT_COPY_FILES += $(LOCAL_PATH)/fise/property_data.json:$(TARGET_COPY_OUT_SYSTEM)/etc/property_data.json:mtk
3、拷贝/system/etc 目录授权文件 到/data/system 分区目录
路径: /frameworks/base/services/core/java/com/android/device/utils/ParseYLPropertyFileService.java
java
package com.android.device.utils;
import android.util.Slog;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class ParseYLPropertyFileService {
private static final String TAG = "ParseYLPropertyFileService";
private static final String SOURCE_PATH = "/system/etc/property_data.json";
private static final String DEST_PATH = "/data/system/property_data.json";
public static boolean ensurePermissionFileExists() {
File sourceFile = new File(SOURCE_PATH);
File destFile = new File(DEST_PATH);
if (!sourceFile.exists()) {
Slog.w(TAG, "Source file not found: " + SOURCE_PATH);
return false;
}
if (destFile.exists()) {
Slog.d(TAG, "Destination file already exists: " + DEST_PATH);
return true;
}
return copyFile(sourceFile, destFile);
}
public static boolean forceCopyPermissionFile() {
File sourceFile = new File(SOURCE_PATH);
File destFile = new File(DEST_PATH);
if (!sourceFile.exists()) {
Slog.w(TAG, "Source file not found: " + SOURCE_PATH);
return false;
}
return copyFile(sourceFile, destFile);
}
private static boolean copyFile(File source, File dest) {
File destDir = dest.getParentFile();
if (!destDir.exists()) {
if (!destDir.mkdirs()) {
Slog.e(TAG, "Failed to create directory: " + destDir.getPath());
return false;
}
}
try (InputStream in = new FileInputStream(source);
OutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 8KB buffer
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
setFilePermissions(dest);
Slog.i(TAG, "File copied successfully: " + source.getPath() + " -> " + dest.getPath());
return true;
} catch (IOException e) {
Slog.e(TAG, "Failed to copy file: " + e.getMessage());
return false;
}
}
private static void setFilePermissions(File file) {
file.setReadable(true, false);
file.setWritable(true, true);
file.setExecutable(false, false);
}
public static boolean isPermissionFileExists() {
return new File(DEST_PATH).exists();
}
public static String getDestinationPath() {
return DEST_PATH;
}
}
4、解析 /data/system 分区目录 的授权文件并设置属性
在Framework层读取并解析自己SN 管理的授权信息,然后设置属性
路径:/frameworks/base/services/core/java/com/android/device/utils/CmccAuthKeyManager.java
java
package com.android.device.utils;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import android.os.SystemProperties;
import android.util.Log;
public class CmccAuthKeyManager {
private static final String TAG = "CmccAuthKeyManager";
private static final String JSON_FILE_PATH = "/data/system/property_data.json";
private static final String PROP_SERIALNO = "ro.serialno";
private static final String PROP_TARGET_SN = "ro.product.sn";
private static final String PROP_AUTH_KEY = "ro.product.cmcc_andlinkauthkey";
private static final String SET_PROP_AUTH_KEY = "persist.sys.cmcc_andlinkauthkey";
private static final String SET_PROP_CMEI_KEY = "persist.sys.cmcc_cmei";
public static void loadAndSetAuthKey() {
try {
String localSn = SystemProperties.get(PROP_SERIALNO, "");
Log.d(TAG, "Local serialno: " + localSn);
if (localSn.isEmpty()) {
Log.e(TAG, "local sn is empty");
return;
}
InputStreamReader reader = new InputStreamReader(new FileInputStream(JSON_FILE_PATH));
StringBuilder sb = new StringBuilder();
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer)) != -1) {
sb.append(buffer, 0, len);
}
reader.close();
String jsonContent = sb.toString();
JSONArray jsonArray = new JSONArray(jsonContent);
boolean found = false;
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject obj = jsonArray.getJSONObject(i);
String itemSn = obj.optString(PROP_TARGET_SN, "");
String authKey = obj.optString(PROP_AUTH_KEY, "");
if (localSn.equals(itemSn)) {
SystemProperties.set(SET_PROP_AUTH_KEY, authKey);
Log.d(TAG, "MATCH SUCCESS! Set auth key: " + authKey);
if (localSn.length() >= 15) {
String last15 = localSn.substring(localSn.length() - 15);
Log.d(TAG,"set persist.product.cmcc_cmei:"+last15);
SystemProperties.set(SET_PROP_CMEI_KEY, last15);
}
found = true;
break;
}
}
if (!found) {
Log.e(TAG, "NO MATCH SN found!");
}
} catch (Exception e) {
Log.e(TAG, "loadAndSetAuthKey error", e);
}
}
}
5、在服务Service 里面拷贝授权文件并加载
路径:frameworks/base/services/core/java/com/android/device/DeviceCtrlService.java
这里是自己用自己自定义的服务实现,当然也可以加载其它系统服务里面都可以的。
在 onBootPhase 方法的 PHASE_BOOT_COMPLETED 状态,代表已经开机、服务已经准备完毕:
拷贝后就去解析 授权文件。
java
try {
if (!ParseYLPropertyFileService.ensurePermissionFileExists()) {
Log.d(TAG,"Failed to ensure parseylproperty file exists");
}else{
Log.d(TAG,"====to CmccAuthKeyManager loadAndSetAuthKey ====");
CmccAuthKeyManager.loadAndSetAuthKey();
}
} catch (Exception e) {
Slog.e(TAG, "Error copying parseylproperty file", e);
Log.d(TAG,"Failed to Exception ");
}
6、适配属性获取
路径:/frameworks/base/core/java/android/os/SystemProperties.java
java
/**
* Get the String value for the given {@code key}.
*
* @param key the key to lookup
* @return an empty string if the {@code key} isn't found
* @hide
*/
@NonNull
@SystemApi
public static String get(@NonNull String key) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
Log.d(TAG,"========get key:==:"+key);
if("ro.product.sn".equals(key)){
String serialnoValue=native_get("ro.serialno");
Log.d(TAG,"========get keyValue:==:"+serialnoValue);
return serialnoValue;
}else if("ro.product.cmcc_cmei".equals(key)){
String meiValue=native_get("persist.sys.cmcc_cmei");
Log.d(TAG,"========get keyValue:==:"+meiValue);
return meiValue;
}else if("ro.product.cmcc_andlinkauthkey".equals(key)){
String andlinkauthkeyValue=native_get("persist.sys.cmcc_andlinkauthkey");
Log.d(TAG,"========get keyValue:==:"+andlinkauthkeyValue);
return andlinkauthkeyValue;
}
if(key.contains("ro.product.cmcc")){
String cmcckeyValue=native_get(key);
Log.d(TAG,"========get cmcckeyValue keyValue:==:"+cmcckeyValue);
}
return native_get(key);
}
四、mtk 相关目录介绍-mnt/vendor 分区目录
为什么讲这个分区目录,如果不用系统内置授权文件,只需要客户生产的时候自己push 授权文件到系统里面去。而且这个授权文件无论 OTA升级、恢复出厂设置、哪怕重新刷机这个push 的授权文件都不会丢, 这就是最完美的解决方案。
恰好:MTK平台中,有这样的目录 完美契合,如下:

坑点:可惜的是 这个分区目录下的文件,任何进程都无法访问。 system server init void vendor-init 进程都无法访问,无法进行拷贝到其它分区。 这就导致这个方案完全不可行。
当然,可以在其它平台上面试一试,如果其它平台有相关的需求。
五、实现属性读写-adb 命令
假使 adb 命令都实现了ro开头属性修改,那么是不是可以实现。 我自己尝试过,可以进行adb 读写成功,但是 重启之后就丢失了。 adb 的操作其实就是 守护进程或者到Framework层的传递。 这里针对的是mtk 平台 重启丢失,其它平台可以自行再验证。最核心的问题是 mtk 平台无法挂载,导致写入的属性重启会丢失, RK 平台应该没问题。
修改文件
java
/bionic/libc/bionic/system_property_set.cpp
/system/core/init/property_service.cpp
具体修改实现
system_property_set.cpp 屏蔽拦截

property_service.cpp 屏蔽拦截


总结
- 遇到问题不要死磕:死磕.ro 必须可写问题,转变思路;
- 遇到问题不要死磕:死磕 Android体系存在恢复出厂设置、OTA后授权文件不丢失问题,不要死磕 其它分区进程一定要能访问到,然后尝试解决各种SELinux 权限。
- 不要死磕:adb 能够写ro 开头属性了已经。然后死磕 系统层也进行写操作,自己写守护进程模块。 太麻烦了。
- 学会尝试不同的方案,快速验证。