Android7.x 通过Recovery保留特定文件实现恢复出厂设置后保留系统语言设置
最近有一个需求,要求在恢复出厂设置之后不还原语言设置,由于我们知道语言设置可以在Properties System中保存,所以首先想到的就是能不能将某个property保存下来。
恢复出厂设置不影响的文件持久化保存方法
经过研究发现,有几种方法可以实现,可以做一个新的分区,并且在恢复出厂时不擦除。
但后来发现Recovery在恢复出厂的时候会保留/cache/recovery目录中的部分文件,因为我们的需求只是保留个别配置,感觉这个方式更合适。
因此参照这个过程照葫芦画瓢搞一个,我们先看关键代码:
// bootable\recovery.cpp
// 清理的入口函数为:
static bool wipe_data(int should_confirm, Device* device);
{
//.....
//往里追踪可以看到关键语句,实际上是跳进了erase_volume方法
ui->Print("\n-- Wiping data...\n");
bool success =
device->PreWipeData() &&
erase_volume("/data") &&
(has_cache ? erase_volume("/cache") : true) &&
device->PostWipeData();
ui->Print("Data wipe %s.\n", success ? "complete" : "failed");
return success;
}
//再来看这个操作
bool erase_volume(const char* volume) {
bool is_cache = (strcmp(volume, CACHE_ROOT) == 0);
bool is_data = (strcmp(volume, DATA_ROOT) == 0);
ui->SetBackground(RecoveryUI::ERASING);
ui->SetProgressType(RecoveryUI::INDETERMINATE);
saved_log_file* head = NULL;
if (is_cache) {
// If we're reformatting /cache, we load any past logs
// (i.e. "/cache/recovery/last_*") and the current log
// ("/cache/recovery/log") into memory, so we can restore them after
// the reformat.
// 可以看到这里其实已经写明了,会将既往logs先存到内存里,然后再格式化完成后在进行
// 为此,要先确保待擦除的该cache分区已经挂载了
ensure_path_mounted(volume);
DIR* d;
struct dirent* de;
d = opendir(CACHE_LOG_DIR);
if (d) {
char path[PATH_MAX];
strcpy(path, CACHE_LOG_DIR);
strcat(path, "/");
int path_len = strlen(path);
while ((de = readdir(d)) != NULL) {
//然后扫描所有符合条件的文件名,并分配结构体所需的内存。
if (strncmp(de->d_name, "last_", 5) == 0 || strcmp(de->d_name, "log") == 0 || strncmp(de->d_name, "Recovery_", 9) == 0) {
saved_log_file* p = (saved_log_file*) malloc(sizeof(saved_log_file));
strcpy(path+path_len, de->d_name);
p->name = strdup(path);
if (stat(path, &(p->st)) == 0) {
// truncate files to 512kb
if (p->st.st_size > (1 << 19)) {
p->st.st_size = 1 << 19;
}
//分配文件本身内容所需的内存,并将其读出,至此完成将内容保存至内存中
p->data = (unsigned char*) malloc(p->st.st_size);
FILE* f = fopen(path, "rb");
fread(p->data, 1, p->st.st_size, f);
fclose(f);
//通过一个链表保存所有的文件
p->next = head;
head = p;
} else {
free(p);
}
}
}
closedir(d);
} else {
if (errno != ENOENT) {
printf("opendir failed: %s\n", strerror(errno));
}
}
}
ui->Print("Formatting %s...\n", volume);
ensure_path_unmounted(volume);
//....还有很多代码省略了,后面的流程就是接触挂载,调用系统api format_volume格式化分区,然后重新挂载并将内存中的文件写入到分区里。
if (is_cache) {
while (head) {
FILE* f = fopen_path(head->name, "wb");
if (f) {
fwrite(head->data, 1, head->st.st_size, f);
fclose(f);
chmod(head->name, head->st.st_mode);
chown(head->name, head->st.st_uid, head->st.st_gid);
}
free(head->name);
free(head->data);
saved_log_file* temp = head->next;
free(head);
head = temp;
//上面这块是回写到文件的步骤,同时也包括了设置文件权限的操作。
}
}
上面就是recovery里保存其自身log文件的方法,既然知道原理,而我们目的只是保存一个文件,那么就不需要搞的那么复杂,实现一个自己的操作:
//定义文件最大大小
#define LOCAL_FILE_LEN 1024
//指针,用来开辟一块内存,存放文件内容
static char *p_locale_buffer = NULL;
//记录从文件中读出的字节数
static int locale_file_copy_cnt = 0;
//路径
static const char *LOCALE_FILE_PATH = "/cache/persist.prop";
//将文件读出到内存里,可以同样放在erase_volume中,也可以放到wipe_data里,我这里是放在wipe_data的erase_volumn调用之前
{
ui->Print("\nStorage persist prop...\n");
p_locale_buffer = (char*)malloc(LOCAL_FILE_LEN);
int res;
if(NULL == p_locale_buffer){
printf("malloc locale failed!\n");
}else{
memset(p_locale_buffer, 0, LOCAL_FILE_LEN);
if( copyFileToMem(LOCALE_FILE_PATH, p_locale_buffer, &locale_file_copy_cnt, LOCAL_FILE_LEN) < 0 ){
ui->Print("copyFileToMem failed: %s\n", LOCALE_FILE_PATH);
}else{
ui->Print("Persist prop file copied, total: %d byte.\n", locale_file_copy_cnt);
}
}
}
//待erase_volumn返回后,再重新回写
{
if(p_locale_buffer != NULL){
if(copyFileFromMem(LOCALE_FILE_PATH, p_locale_buffer, locale_file_copy_cnt) < 0){
ui->Print("copyFileFromMem failed: %s\n", LOCALE_FILE_PATH);
}else{
ui->Print("Persist prop file restored.\n");
}
free(p_locale_buffer);
p_locale_buffer = NULL;
}
}
//上面所使用的copyFileToMem/FromMem如下,基本文件IO,没什么好说的:
static int copyFileToMem(const char *path,char *p,int *count,int size) {
char *tmp = p;
chmod(path,0770);
chown(path,1000,1000);
FILE* fd = fopen_path(path,"r");
if(NULL == fd) {
printf("open %s failed %d(%s)\n",path,errno,strerror(errno));
return -1;
}
int res = 0;
char buf[50] = {0};
while((res = fread(buf,1,sizeof(buf),fd)) > 0) {
*count += res;
if (*count <= size) {
memcpy(tmp,buf,res);
tmp += res;
} else {
*count -= res;
printf("size overflow");
break;
}
}
fclose(fd);
return 0;
}
static int copyFileFromMem(const char *path,char *p,int count) {
FILE* fd = fopen_path(path,"w+");
//设置下文件权限,这里的644是必要的,否则就需要改init中的util.cpp,不然init会因为权限问题认为该文件是insecure的,拒绝读取
chmod(path,0644);
chown(path,1000,1000);
if(NULL == fd) {
printf("open %s failed %d(%s)\n",path,errno,strerror(errno));
return -1;
}
int res = 0;
if ((res = fwrite(p,1,count,fd)) > 0) {
printf("write done\n");
}
fclose(fd);
return 0;
}
这样一来就实现了文件的持久化保存,但这只是第一步,下面要将其作用在保存系统语言。
将配置写入持久化保存的文件
因为android的系统语言都是可以在property中定义的,所以首先想到的就是在底层的property_set的入口里加一个处理函数,根据property key来决定是不是写入到持久化的文件里,后来查找一下发现这个函数比较深,而且目前的需求只是写一个参数,所以想最小化它对系统代码的影响,所以直接移到设置入口里写。
Android 7.x中,语言设置在Settings的入口是这样的,实现了一个List的Drag&Drop交互,因为允许多个语言按优先级来显示,所以看起来有点复杂:
但是通过一顿搜索,发现最终实际都通过系统内部的一个接口类LocalePicker.updateLocales
方法来更新系统语言,直接一顿查找,发现下面这个入口
//com.android.settings.localepicker 是的,同名
//import com.android.internal.app.LocalePicker; 上面说的内部接口类是这个
public void updateLocalesWhenAnimationStops(final LocaleList localeList) {
if (localeList.equals(mLocalesToSetNext)) {
return;
}
// This will only update the Settings application to make things feel more responsive,
// the system will be updated later, when animation stopped.
LocaleList.setDefault(localeList);
mLocalesToSetNext = localeList;
final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator();
itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
@Override
public void onAnimationsFinished() {
if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) {
// All animations finished, but the locale list did not change
return;
}
//调用内部接口,更新系统语言,这里的mLocaleToSetNext是一个LocaleList,也是由内部提供的,由于系统支持按照语言优先级显示,实际上是对Locale的一个便利化封装,并且实现了序列化接口
LocalePicker.updateLocales(mLocalesToSetNext);
mLocalesSetLast = mLocalesToSetNext;
mLocalesToSetNext = null;
mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault());
}
});
}
发现入口之后直接在这里做手脚, 写一个工具方法将某个系统语言保存到持久化文件中,调用的方法直接插在LocalePicker.updateLocales
调用的后面,只需要获取mLocalesToSetNext
的第一位:
public void saveLocaleToPersistProp(Locale locale){
try {
File output = new File("/cache/persist.prop");
if (!output.exists()) output.createNewFile();
FileOutputStream fos = new FileOutputStream(output);
//因为最终是按照prop来读的,这里直接写进去文件里
fos.write(("persist.sys.locale="+locale.toLanguageTag()).getBytes());
Log.e("GCAT DBG", "Write to persist prop.");
fos.flush();
fos.close();
} catch (Exception ex) {
Log.e("GCAT DBG", "Error when writing file.");
ex.printStackTrace();
}
}
顺带一提,实际负责property写入的其实是在ActivityManager里
//frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java
private boolean updateConfigurationLocked(Configuration values, ActivityRecord starting,
boolean initLocale, boolean persistent, int userId, boolean deferResume) {
int changes = 0;
if (mWindowManager != null) {
mWindowManager.deferSurfaceLayout();
}
if (values != null) {
Configuration newConfig = new Configuration(mConfiguration);
changes = newConfig.updateFrom(values);
if (changes != 0) {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.i(TAG_CONFIGURATION,
"Updating configuration to: " + values);
EventLog.writeEvent(EventLogTags.CONFIGURATION_CHANGED, changes);
if (!initLocale && !values.getLocales().isEmpty() && values.userSetLocale) {
final LocaleList locales = values.getLocales();
int bestLocaleIndex = 0;
if (locales.size() > 1) {
if (mSupportedSystemLocales == null) {
mSupportedSystemLocales =
Resources.getSystem().getAssets().getLocales();
}
bestLocaleIndex = Math.max(0,
locales.getFirstMatchIndex(mSupportedSystemLocales));
}
SystemProperties.set("persist.sys.locale",
locales.get(bestLocaleIndex).toLanguageTag());
LocaleList.setDefault(locales, bestLocaleIndex);
mHandler.sendMessage(mHandler.obtainMessage(SEND_LOCALE_TO_MOUNT_DAEMON_MSG,
locales.get(bestLocaleIndex)));
}
mConfigurationSeq++;
if (mConfigurationSeq <= 0) {
mConfigurationSeq = 1;
}
newConfig.seq = mConfigurationSeq;
mConfiguration = newConfig;
Slog.i(TAG, "Config changes=" + Integer.toHexString(changes) + " " + newConfig);
mUsageStatsService.reportConfigurationChange(newConfig,
mUserController.getCurrentUserIdLocked());
//mUsageStatsService.noteStartConfig(newConfig);
final Configuration configCopy = new Configuration(mConfiguration);
// TODO: If our config changes, should we auto dismiss any currently
// showing dialogs?
mShowDialogs = shouldShowDialogs(newConfig, mInVrMode);
AttributeCache ac = AttributeCache.instance();
if (ac != null) {
ac.updateConfiguration(configCopy);
}
// Make sure all resources in our process are updated
// right now, so that anyone who is going to retrieve
// resource values after we return will be sure to get
// the new ones. This is especially important during
// boot, where the first config change needs to guarantee
// all resources have that config before following boot
// code is executed.
mSystemThread.applyConfigurationToResources(configCopy);
if (persistent && Settings.System.hasInterestingConfigurationChanges(changes)) {
Message msg = mHandler.obtainMessage(UPDATE_CONFIGURATION_MSG);
msg.obj = new Configuration(configCopy);
msg.arg1 = userId;
mHandler.sendMessage(msg);
}
final boolean isDensityChange = (changes & ActivityInfo.CONFIG_DENSITY) != 0;
if (isDensityChange) {
// Reset the unsupported display size dialog.
mUiHandler.sendEmptyMessage(SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG_MSG);
killAllBackgroundProcessesExcept(Build.VERSION_CODES.N,
ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
}
for (int i=mLruProcesses.size()-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
try {
if (app.thread != null) {
if (DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION, "Sending to proc "
+ app.processName + " new config " + mConfiguration);
app.thread.scheduleConfigurationChanged(configCopy);
}
} catch (Exception e) {
}
}
Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_REPLACE_PENDING
| Intent.FLAG_RECEIVER_FOREGROUND);
broadcastIntentLocked(null, null, intent, null, null, 0, null, null,
null, AppOpsManager.OP_NONE, null, false, false,
MY_PID, Process.SYSTEM_UID, UserHandle.USER_ALL);
if ((changes&ActivityInfo.CONFIG_LOCALE) != 0) {
intent = new Intent(Intent.ACTION_LOCALE_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
if (initLocale || !mProcessesReady) {
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
}
broadcastIntentLocked(null, null, intent,
null, null, 0, null, null, null, AppOpsManager.OP_NONE,
null, false, false, MY_PID, Process.SYSTEM_UID, UserHandle.USER_ALL);
}
}
// Update the configuration with WM first and check if any of the stacks need to be
// resized due to the configuration change. If so, resize the stacks now and do any
// relaunches if necessary. This way we don't need to relaunch again below in
// ensureActivityConfigurationLocked().
if (mWindowManager != null) {
final int[] resizedStacks = mWindowManager.setNewConfiguration(mConfiguration);
if (resizedStacks != null) {
for (int stackId : resizedStacks) {
final Rect newBounds = mWindowManager.getBoundsForNewConfiguration(stackId);
mStackSupervisor.resizeStackLocked(
stackId, newBounds, null, null, false, false, deferResume);
}
}
}
}
让系统启动时读取我们自己持久化的property
上面完成了文件持久化和设置语言时的保存,最后一步就是使系统加载时读取:
这点可以直接修改init对应的property_service来实现
//system\core\init\property_service.cpp
//这个文件实际负责系统启动时的property初始化,其入口
void load_system_props(){
load_properties_from_file(PROP_PATH_SYSTEM_BUILD, NULL);
load_properties_from_file(PROP_PATH_VENDOR_BUILD, NULL);
//加入自定义配置的读取,这里已经有现成的方法直接从文件中读取并解析到property system里了
ERROR("Loading CACHE Prop....\n");
load_properties_from_file("/cache/persist.prop", NULL);
load_properties_from_file(PROP_PATH_FACTORY, "ro.*");
load_recovery_id_prop();
}
//这个方法会被->init\builtins.cpp注册到一个map中,并且由builtins.h提供一个类。
//最终在主入口init.cpp的main函数里Action::set_function_map(&function_map);中注册
//然后通过响应请求load_system_props来调用