常规的预置可卸载 apk 并且恢复出厂不恢复,都是放到 data 目录下,也就是打包到 userdata.img 中。
这里列举几个缺点,
1、从 Q 开始谷歌默认不建议这样做了,所以在不修改源码情况下,你只要 data 中预置了东西,烧写后开机直接进入
recovery了提示必须清除数据才能正常开机。当然通过修改源码可以编译这个问题androidQ(10.0) 预装集成apk到data分区
即便是解决这个问题以后还有潜在的其它问题,比如第一次成功开机后,你并没有主动卸载apk,再一次重启后apk没了。
2、打包 ota 的时候,需要手动将上一次编译的 userdata.img 拷贝留存,不然再次编译会被覆盖,烧写后并没有预装 apk
3、预装 apk 大小过大时,也容易出现预置失败的情况。
参考 RK 解决方案,通过文件 last_deleteApkFile.dat 记录卸载过的 apk 包名,在 PMS 扫描安装时读取此文件进行过滤。
RK 这个解决方案还存在一个 bug,当你把 apk 卸载后包名被记录到 last_deleteApkFile.dat,你再次刷机后开机 apk 也不存在,
这显然是不符合正常需求的。
现在在 MTK 的平台上来实现并解决这个 bug,思路如下
1、MTK 本身存在配置可卸载白名单包名,文件路径 vendor\mediatek\proprietary\frameworks\base\data\etc\pms_sysapp_removable_system_list.txt
在里面添加预制可卸载 apk 包名,并将 apk 预装在 system 分区下,mk 中不用指定路径就行
2、MTK 自带节点 /mnt/vendor/protect_f/ 可存储恢复出厂+刷机不丢失数据,正好用来存放 last_deleteApkFile.dat
3、在 PMS 中增加读写 /mnt/vendor/protect_f/last_deleteApkFile.dat 逻辑,需要解决 selinux 权限(我这里偷懒了直接关闭源码里的 selinux)
4、区分是刷机后第一次启动还是恢复出厂后第一次启动,正规来讲需要通过 nvram 方式去写标志区分(我发现一种偷懒取巧方式,判断 /cache/recovery/last_install 文件是否存在)
刚刷机完启动系统不存在这个文件,当手动点击恢复出厂后会多出来这个文件。RK 平台烧写后也会有这个文件,因为烧写后会执行一次恢复出厂,RK 平台可以判断
/cache/recovery/last_kmsg.2 每执行一次恢复出厂操作,/cache/recovery/ 中就会多出文件
/cache/recovery # ls
last_install last_kmsg last_kmsg.1 last_locale last_log last_log.1
/cache/recovery # ls
last_install last_kmsg last_kmsg.1 last_kmsg.2 last_locale last_log last_log.1 last_log.2
/cache/recovery # ls
last_install last_kmsg.1 last_kmsg.3 last_log last_log.2
last_kmsg last_kmsg.2 last_locale last_log.1 last_log.3
接下来是实现代码
frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
卸载 apk 时判断包名是否在 pms_sysapp_removable_system_list 中,区分于用户后来手动安装的。
在白名单中,先检查 last_deleteApkFile.dat 文件是否存在,不存在第一次卸载先创建文件并把包名写进去
文件存在,读取 dat 中是否已经写入过包名,未写入则 write
import java.util.function.Predicate;+import java.io.BufferedWriter;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.FileReader;@@ -605,6 +614,9 @@ public class PackageManagerService extends IPackageManager.Stubprivate static final String ODM_OVERLAY_DIR = "/odm/overlay";private static final String OEM_OVERLAY_DIR = "/oem/overlay";
+ //cczheng add
+ private static final String SYSTEM_APP_DIR = "/system/app";
+ private static final String DELETE_APK_FILE = "/mnt/vendor/protect_f/last_deleteApkFile.dat";@@ -19651,6 +19686,48 @@ public class PackageManagerService extends IPackageManager.Stubif (DEBUG_REMOVE) Slog.d(TAG, "Removing non-system package: " + ps.name);deleteInstalledPackageLIF(ps, deleteCodeAndResources, flags, allUserHandles,outInfo, writeSettings, replacingPackage);
+ Log.d(TAG, "Removing non-system package: " + ps.name);
+ //cczheng add
+ if (checkIsCanRemoveSystemApp(ps.name)) {
+ File deleteApkFile = new File(DELETE_APK_FILE);
+ if(!deleteApkFile.exists()) {
+ try {
+ deleteApkFile.createNewFile();
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(TAG,"create file failed: " + DELETE_APK_FILE);
+ return;
+ }
+ }
+ ArrayList<String> list = new ArrayList<String>();
+ readDeleteFile(list);
+ if (list.contains(ps.name)) {
+ Log.d(TAG, ps.name +" already writed");
+ return;
+ }
+
+ BufferedWriter fileWriter = null;
+ try {
+ fileWriter = new BufferedWriter(new FileWriter(deleteApkFile,true));
+ fileWriter.append(ps.name);
+ fileWriter.newLine();
+ fileWriter.flush();
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(TAG,"write file failed: " + DELETE_APK_FILE);
+ } finally {
+ if (fileWriter != null) {
+ try {
+ fileWriter.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return;
+ }
+ fileWriter = null;
+ }
+ }
+ }
+ //cczheng add }
scanDirLI 中扫描时进行过滤,先区分是否刷机第一次启动还是恢复出厂第一次启动,如果是恢复出厂第一次启动,
读取 last_deleteApkFile.dat 中包名集合,扫描 system/app 文件夹下 apk 时,包名在集合中则跳过安装
private static final Intent sBrowserIntent;
@@ -9114,6 +9126,17 @@ public class PackageManagerService extends IPackageManager.StubLog.d(TAG, "Scanning app dir " + scanDir + " scanFlags=" + scanFlags+ " flags=0x" + Integer.toHexString(parseFlags));}
+ //cczheng add
+ ArrayList<String> list = new ArrayList<String>();
+ if (isRecoveryFirstBoot()) {
+ if (scanDir.getAbsolutePath().contains(SYSTEM_APP_DIR)) {
+ if (!readDeleteFile(list)) {
+ Log.e(TAG, "read readDeleteFile data failed");
+ return;
+ }
+ }
+ }//cczheng add
+try (ParallelPackageParser parallelPackageParser = new ParallelPackageParser(mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir,mParallelPackageParserCallback)) {
@@ -9126,6 +9149,18 @@ public class PackageManagerService extends IPackageManager.Stub// Ignore entries which are not packagescontinue;}
+ //cczheng add
+ if (file.getAbsolutePath().contains(SYSTEM_APP_DIR)) {
+ if (list != null && list.size() > 0) {
+ final boolean isdeleteApk = isDeleteApk(file, parseFlags, list);
+ if (isdeleteApk) {
+ // Ignore deleted bundled apps
+ Log.d(TAG, "skip install "+file.getAbsolutePath());
+ continue;
+ }
+ }
+ }//cczheng add
+parallelPackageParser.submit(file, parseFlags);fileCount++;}+ //cczheng add
+ private boolean checkIsCanRemoveSystemApp(String pkgName){
+ Log.d(TAG, "checkIsCanRemoveSystemApp pkgName=" + pkgName);
+ FileReader fr = null;
+ BufferedReader br = null;
+ HashSet<String> resultSet = new HashSet<String>();
+ resultSet.clear();
+ File file = Environment.buildPath(Environment.getRootDirectory(),
+ "etc", "permissions","pms_sysapp_removable_system_list.txt");
+ try {
+ if (file.exists()) {
+ fr = new FileReader(file);
+ } else {
+ Log.d(TAG, "file in " + file + " does not exist!");
+ return false;
+ }
+ br = new BufferedReader(fr);
+ String line;
+ while ((line = br.readLine()) != null) {
+ line = line.trim();
+ if (!TextUtils.isEmpty(line)) {
+ resultSet.add(line);
+ }
+ }
+ Log.e(TAG,"GRANT_SYS_APP_LIST_SYSTEM size="+resultSet.size());
+ } catch (Exception io) {
+ Log.d(TAG, io.getMessage());
+ } finally {
+ try {
+ if (br != null) {
+ br.close();
+ }
+ if (fr != null) {
+ fr.close();
+ }
+ } catch (Exception io) {
+ Log.d(TAG, io.getMessage());
+ }
+ }
+ return resultSet.contains(pkgName);
+ }
+
+ private boolean readDeleteFile(ArrayList<String> list) {
+ File deleteApkFile = new File(DELETE_APK_FILE);
+ if (!deleteApkFile.exists()) {
+ Log.e(TAG,"deliteApkFile not exist");
+ return true;
+ }
+ BufferedReader br = null;
+ try {
+ br = new BufferedReader(new FileReader(deleteApkFile));
+ String name = null;
+ while(null != (name = br.readLine())) {
+ list.add(name);
+ }
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ if (br != null) {
+ try {
+ br.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return false;
+ }
+ br = null;
+ }
+ }
+ }
+
+ private boolean isDeleteApk(File scanFile, int parseFlags, ArrayList<String> list) {
+ PackageParser pp = new PackageParser();
+ final PackageParser.Package pkg;
+ try {
+ pkg = pp.parsePackage(scanFile, parseFlags);
+ } catch (PackageParserException e) {
+ e.printStackTrace();
+ return false;
+ }
+ if (list.contains(pkg.packageName)) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isRecoveryFirstBoot() {
+ boolean result = false;
+ try {
+ File recoveryFile = new File("/cache/recovery/last_install");
+ Log.d("ccz","recoveryFile "+recoveryFile.exists());
+ if (isFirstBoot()) {//only first boot need check
+ result = recoveryFile.exists();
+ }
+ }catch(Exception e){
+ e.printStackTrace();
+ }
+ Log.i("ccz","isRecoveryFirstBoot result="+result);
+ return result;
+ }
+ //cczheng add
+@GuardedBy("mPackages")private void markPackageUninstalledForUserLPw(PackageSetting ps, UserHandle user) {final int[] userIds = (user == null || user.getIdentifier() == UserHandle.USER_ALL)