Android Framework 层实现-自定义ro.开头属性-实现属性可读可写

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 命令)
    • 修改文件
    • 具体修改实现
      • [system_property_set.cpp 屏蔽拦截](#system_property_set.cpp 屏蔽拦截)
      • [property_service.cpp 屏蔽拦截](#property_service.cpp 屏蔽拦截)
  • 总结

前言-需求

系统适配产品应用,产品自定义了很多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-系统增加一个属性值

Android 系统属性添加篇

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 开头属性了已经。然后死磕 系统层也进行写操作,自己写守护进程模块。 太麻烦了。
  • 学会尝试不同的方案,快速验证。