Android预置可卸载app,恢复出厂不能恢复

本文探讨了在Android系统中实现预置APP可卸载且恢复出厂设置后不恢复的机制。通过将APP放置在特定目录并在PackageManagerService中进行扫描,结合缓存目录下记录删除状态的文件,实现了APP的预置与可卸载特性。
问题背景

在某些时候我们希望对于预置的app可卸载,但是恢复出厂不能恢复。比如设备上的一些生产工具之类的软件,生产验证结束之后人工卸载,而在用户手里不能恢复出来。
预置能够减少生产流程中的安装环节。

实现方法
原生实现

有一种原生的实现方式是将app打到data分区,这样相当于在编译的时候就把app装上,最终打入的img在userdata.img里。下面是Android.mk的写法:

LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)

但是有些工程组织是默认不打userdata分区的,还有没有其他的实现方式呢?

另一种实现

其实想做到恢复出厂不能恢复,无非是要能记录到该app曾经被卸载过的状态,与app本身放在系统的哪个目录下无关。下面我讲一下实现的关键路径。

  • 在cache目录下新建文件deleteApkFile.dat(文件名随意啦,但是在cache下面就行)
  • 将app打入vendor/preinstall_gone 目录(这个取名也随意吧)
  • 在PackageManagerService的构造函数中添加对于该目录的扫描,就是上一步你放该app的目录
            // Collect ordinary system packages.
            final File systemAppDir = new File(Environment.getRootDirectory(), "app");
            scanDirTracedLI(systemAppDir, mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM
                    | PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);

            // Collect all vendor packages.
            File vendorAppDir = new File("/vendor/app");
            try {
                vendorAppDir = vendorAppDir.getCanonicalFile();
            } catch (IOException e) {
                // failed to look up canonical path, continue with original one
            }
            scanDirTracedLI(vendorAppDir, mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM
                    | PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);

            // Collect all OEM packages.
            final File oemAppDir = new File(Environment.getOemDirectory(), "app");
            scanDirTracedLI(oemAppDir, mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM
                    | PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);

            // Collect bundled app packages which can be uninstalled
            scanDirTracedLI(mAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0);
            ...
            //假设该app放在getPrebundledUninstallGoneDirectory这个目录下
            scanDirTracedLI(Environment.getPrebundledUninstallGoneDirectory(),
                    mDefParseFlags | PackageParser.PARSE_IS_PREBUNDLED_DIR,
                    scanFlags,0);


  • scanDirTracedLI最终使用的是scanDirLI方法,去里面改一下增加对于deleteApkFile.dat的判断即可
if (dir.getAbsolutePath().contains(VENDOR_BUNDLED_UNINSTALL_GONE_DIR)) {
            if (!readDeleteFile(list)) {
                Log.e(TAG,"read data failed");
                return;
            }
        }

        for (File file : files) {
            final boolean isPackage = (isApkFile(file) || file.isDirectory())
                    && !PackageInstallerService.isStageName(file.getName());
            if (!isPackage) {
                // Ignore entries which are not packages
                continue;
            }
            if (file.getAbsolutePath().contains(VENDOR_BUNDLED_UNINSTALL_GONE_DIR)) {
                if (list != null && list.size() > 0) {
                    final boolean isdeleteApk = isDeleteApk(file,parseFlags,list);
                    if (isdeleteApk) {
                        // Ignore deleted bundled apps
                        continue;
                    }
                }
            }
            try {
                scanPackageTracedLI(file, parseFlags | PackageParser.PARSE_MUST_BE_APK,
                        scanFlags, currentTime, null);
                if (isPrebundled) {
                    final PackageParser.Package pkg;
                    try {
                        pkg = new PackageParser().parsePackage(file, parseFlags);
                    } catch (PackageParserException e) {
                        throw PackageManagerException.from(e);
                    }
                    synchronized (mPackages) {
                        mSettings.markPrebundledPackageInstalledLPr(pkg.packageName);
                    }
                }
            } catch (PackageManagerException e) {
                Slog.w(TAG, "Failed to parse " + file + ": " + e.getMessage());

                // Delete invalid userdata apps
                if ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 &&
                        e.error == PackageManager.INSTALL_FAILED_INVALID_APK) {
                    logCriticalInfo(Log.WARN, "Deleting invalid package at " + file);
                    removeCodePathLI(file);
                }
            }
        }

下面给出读取记录删除app文件的方法示例

    public static boolean readDeleteFile(ArrayList<String> list) {
        File deleteApkFile = new File(DELETE_APK_FILE);
        if (!deleteApkFile.exists()) {
            Slog.w(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 (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    return false;
                }
                br = null;
            }
        }
    }
  • 最后就是用户删除app的时候去记录一下包名到该文件内就ok啦,这个就自己去实现吧
### Android 恢复出厂设置后第三方内置 APK 是否可以恢复Android 设备中,通过特定方式预置的第三方 APK 可以在卸载后通过恢复出厂设置进行恢复。这种方式主要依赖于将 APK 文件预置到系统的特定目录,并在设备首次启动时将其从受保护的目录(如 `/system`)复制到可写目录(如 `/data/app`)。以下是对这一机制的详细说明: #### 1. 预置 APK 的实现方式 为了使第三方 APK恢复出厂设置后能够恢复,通常采用以下方法[^3]: - 将 APK 文件放置在受保护的系统目录(如 `/system/pre-install/`)中。 - 在设备首次启动时,通过自动运行脚本(如 `init.rc` 或类似的启动脚本)将这些 APK 文件从 `/system` 目录复制到 `/data/app` 目录。 - 创建一个标志文件(如 `/data/app/did`),用于确保该操作仅在设备首次启动时执行。 脚本示例如下: ```bash #!/system/bin/sh sleep 10 if [ ! -f /data/app/did ]; then chmod 777 /data/app/ cp /system/pre-install/*.apk /data/app/ echo 1 > /data/app/did chmod 777 /data/app/*.apk fi ``` #### 2. 恢复出厂设置的工作原理 恢复出厂设置会清除 `/data` 分区中的数据,包括用户安装的应用程序和相关配置文件。然而,由于预置APK 文件存储在 `/system` 分区(或类似的只读分区),这些文件会被清除。因此,在设备重新启动时,上述脚本会再次执行,将 APK 文件从 `/system` 复制到 `/data/app`,从而实现 APK恢复[^5]。 #### 3. 权限要求 为了实现上述功能,需要满足以下条件: - 预置APK 必须具有系统权限,这通常通过在 `AndroidManifest.xml` 中设置 `android:sharedUserId="android.uid.system"` 实现[^2]。 - 执行恢复出厂设置的操作需要系统级权限,否则可能会因权限足导致失败[^4]。 #### 4. 兼容性与版本差异 同版本的 Android 系统对恢复出厂设置的实现方式可能有所同。例如: - 在 Android 9.0 之前,可以通过发送 `Intent.ACTION_MASTER_CLEAR` 广播触发恢复出厂设置。 - 在 Android 9.0 及更高版本中,需使用 `android.intent.action.FACTORY_RESET` 广播[^2]。 代码示例如下: ```java // Android 9.0 之前 Intent intent = new Intent(Intent.ACTION_MASTER_CLEAR); sendBroadcast(intent); // Android 9.0 及以上 Intent resetIntent = new Intent("android.intent.action.FACTORY_RESET"); resetIntent.setPackage("android"); sendBroadcast(resetIntent); ``` #### 结论 通过上述方法,第三方 APK 可以在 Android 设备中实现卸载后通过恢复出厂设置进行恢复的功能。关键在于将 APK 文件预置到受保护的系统目录,并在设备首次启动时将其复制到可写目录。此外,还需要确保应用具有系统权限以及兼容同版本的恢复出厂设置实现方式。 ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值