RK-Android11-PackageInstaller安装器自动安装功能实现
RK-Android11-PackageInstaller安装器自动安装功能实现,知识点规整总结
文章目录
- 前言-需求
- 参考资料
- 一、 知识点回顾
- 二、包安装器PackageInstaller 流程分析-知识点分析
- 核心类在安装流程中的职责与协作顺序
- 核心类详解
- 核心类InstallStart
- 核心类InstallStaging
- 核心类PackageInstallerActivity
- 核心类InstallInstalling
- 核心类DeleteStagedFileOnResult
- 总结与完整协作流程
- 核心类思考
- 三、需求实现
- 需求分析
- 需求实现
- 四、知识点扩展
- 快速编译项目
- 解决方案
- 修改编译脚本
- 替换现有的包安装器
- 如何查找替换 包安装器PackageInstaller 模块配置
- 方案实现
- PackageInstaller.Session 知识点
- 核心概念与作用
- 主要用途和场景
- 静默安装(需特权权限):
- 安装拆分 APK(APK Splits):
- 流式安装:
- 获取详细的安装结果:
- 创建安装会话:
- 基本工作流程(代码示例概览)
- 获取 PackageInstaller
- 创建会话参数
- 创建会话
- 打开会话并传输 APK 数据:
- 提交会话(开始真正安装)
- 处理安装结果
- 安装小结
- 总结
前言-需求
通过具体需求,再次熟悉、理解 包安装器流程和业务,简单规整一些知识点,方便后续进一步深度定制。
客户需求:实现下载流程和安装流程UI主题一致,安装过程去掉一些列确认弹框提示,直接进入安装流程、安装失败时候直接显示安装失败原因。 如下两图:
参考资料
PMS安装apk之界面跳转
MTK-Android13-包安装器PackageInstaller 静默安装实现
这是之前总结、规整的包安装器PackageInstaller
知识点,平台是MTK,我们这里是在RK平台上面定制需求的,源码流程少许不一样,但思想一样,具有极高的参考价值。特别是静默安装篇,对于安装过程有一定的参考价值。
一、 知识点回顾
包安装器PackageInstaller
里面流程暂不回顾,这里直接看之前做的静默安装功能:思路如下
- 在
InstallStart
路由里面拦截,如果带了静默安装参数,那么拦截一次,执行自己静默安装逻辑 - 在
InstallStart
拦截执行自己把文件写到本地一次,开启异步线程StagingAsyncAppTask
把指定路径的文件写到包的缓存文件路径下或者包指定路径下【方便统一管理:删除、导入】,发送信息,广播形式告诉SilenceInstallReceiver
,我要静默安装了 - 自定义广播接收到数据后就执行
SilenceInstallManager
来执行静默安装 - 静默安装类
SilenceInstallManager
里面核心调用了PackageInstaller.Session
进行静默安装:把文件通过session
发送到系统framework
进行安装;注册监听SessionCallback
回调安装成功或失败。
这里我们简单回顾一下流程,下面会进一步分析的。 我把之前静默安装管理类贴出来 SilenceInstallManager
,方便理解:
package com.android.packageinstaller;import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PackageParser;
import android.content.pm.ResolveInfo;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.util.ArrayMap;
import android.util.Log;import androidx.annotation.NonNull;import com.android.internal.content.InstallLocationUtils;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;import android.content.pm.IPackageDeleteObserver;final class SilenceInstallManager {private static final String TAG = "SilenceInstallManager";private static final int MSG_WHAT_INSTALL_FINISH_SUCCESS = 0;private static final int MSG_WHAT_INSTALL_FINISH_FAIL = 1;private static final int MSG_WHAT_UNINSTALL_COMPLETE = 2;private Context mContext;@SuppressLint("NewApi")private ArrayMap<Integer, InstallAppInfo> InstallAppInfoMap = new ArrayMap<>();private static volatile SilenceInstallManager INSTANCE;private SilenceInstallManager(Context context) {mContext = context;}public static SilenceInstallManager getInstance(Context context) {if (null == INSTANCE) {synchronized (SilenceInstallManager.class) {if (null == INSTANCE) {INSTANCE = new SilenceInstallManager(context.getApplicationContext());}}}return INSTANCE;}@SuppressLint("NewApi")private PackageInstaller.SessionCallback mSessionCallback = new PackageInstaller.SessionCallback() {@Overridepublic void onCreated(int sessionId) {Log.d(TAG, "onCreated---->" + sessionId);}@Overridepublic void onBadgingChanged(int sessionId) {Log.w(TAG, "mSessionCallback onBadgingChanged---->" + sessionId);}@Overridepublic void onActiveChanged(int sessionId, boolean active) {Log.w(TAG, "mSessionCallback onActiveChanged---->" + sessionId + " active--->" + active);}@Overridepublic void onProgressChanged(int sessionId, float progress) {Log.w(TAG, "mSessionCallback onProgressChanged---->" + sessionId + " progress--->" + progress);}@Overridepublic void onFinished(int sessionId, boolean success) {Log.d(TAG, "onFinished---->" + sessionId + " success--->" + success);Message msg = Message.obtain();msg.what = MSG_WHAT_INSTALL_FINISH_SUCCESS;msg.arg1 = sessionId;msg.obj = success;mHandler.sendMessage(msg);}};@SuppressLint("HandlerLeak")private Handler mHandler = new Handler() {@Overridepublic void dispatchMessage(@NonNull Message msg) {mContext.getPackageManager().getPackageInstaller().unregisterSessionCallback(mSessionCallback);if (msg.what == MSG_WHAT_INSTALL_FINISH_SUCCESS) {boolean result = (boolean) msg.obj;int sessionId = msg.arg1;InstallAppInfo info = InstallAppInfoMap.remove(sessionId);if (result) {Log.d(TAG, "install success");if (null != info) {if (info.isLaunch && null != info.info && null != info.info.packageName && !"".equals(info.info.packageName)) {launchApp(info.info.packageName);}File f = new File(info.filePath);if (f.exists()) {f.delete();}}} else {Log.d(TAG, "install fail");}} else if (msg.what == MSG_WHAT_INSTALL_FINISH_FAIL) {int sessionId = msg.arg1;if (sessionId != -1) {InstallAppInfoMap.remove(sessionId);}Log.d(TAG, "install fail");} else if (msg.what == MSG_WHAT_UNINSTALL_COMPLETE) {Log.d(TAG, "uninstall complete--->" + msg.arg1);if (msg.arg1 == PackageManager.DELETE_SUCCEEDED) {Log.d(TAG, "delete succeeded");} else {Log.d(TAG, "delete fail");}}}};public void silenceInstall(String appFilePath, boolean launch) {if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {mContext.getPackageManager().getPackageInstaller().registerSessionCallback(mSessionCallback);PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);params.setInstallAsInstantApp(false);params.setInstallReason(PackageManager.INSTALL_REASON_USER);File file = new File(appFilePath);if (!file.exists()) {sendFailMsg(-1);return;}try {PackageParser.PackageLite pkg = PackageParser.parsePackageLite(file, 0);params.setAppPackageName(pkg.packageName);params.setInstallLocation(pkg.installLocation);Log.e(TAG, "params.abiOverride:"+params.abiOverride);// params.setSize(// InstallLocationUtils.calculateInstalledSize(pkg, false, params.abiOverride));params.setSize(file.length()); /*} catch (IOException e) {e.printStackTrace();}*/ }catch (PackageParser.PackageParserException e) {Log.e(TAG, "Cannot parse package " + file + ". Assuming defaults.");Log.e(TAG,"Cannot calculate installed size " + file + ". Try only apk size.");params.setSize(file.length());}/* catch (IOException e) {Log.e(TAG,"Cannot calculate installed size " + file + ". Try only apk size.");params.setSize(file.length());}*/try {PackageInfo mPkgInfo = PackageUtil.getPackageInfo(mContext, file, PackageManager.GET_PERMISSIONS);int mSessionId = mContext.getPackageManager().getPackageInstaller().createSession(params);InstallAppInfo installAppInfo = new InstallAppInfo(mSessionId, appFilePath, mPkgInfo, launch);InstallAppInfoMap.put(mSessionId, installAppInfo);InstallingAsyncTask mInstallingTask = new InstallingAsyncTask(mContext, appFilePath, mSessionId);mInstallingTask.execute();} catch (IOException e) {e.printStackTrace();sendFailMsg(-1);}}}private void sendFailMsg(int sessionId) {Message msg = Message.obtain();msg.what = MSG_WHAT_INSTALL_FINISH_FAIL;msg.arg1 = sessionId;mHandler.sendMessage(msg);}@SuppressLint("NewApi")private final class InstallingAsyncTask extends AsyncTask<Void, Void,PackageInstaller.Session> {private Context mContext;private String mAppPath;private int mSessionId;public InstallingAsyncTask(Context context, String appPath, int sessionId) {mContext = context;mAppPath = appPath;mSessionId = sessionId;}@Overrideprotected PackageInstaller.Session doInBackground(Void... params) {PackageInstaller.Session session;try {session = mContext.getPackageManager().getPackageInstaller().openSession(mSessionId);} catch (IOException e) {return null;}session.setStagingProgress(0);try {File file = new File(mAppPath);try (InputStream in = new FileInputStream(file)) {long sizeBytes = file.length();try (OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes)) {byte[] buffer = new byte[1024 * 1024];while (true) {int numRead = in.read(buffer);if (numRead == -1) {session.fsync(out);break;}if (isCancelled()) {session.close();break;}out.write(buffer, 0, numRead);if (sizeBytes > 0) {float fraction = ((float) numRead / (float) sizeBytes);session.addProgress(fraction);}}}}return session;} catch (IOException | SecurityException e) {Log.e(TAG, "Could not write package", e);session.close();return null;}}@Overrideprotected void onPostExecute(PackageInstaller.Session session) {if (session != null) {Intent broadcastIntent = new Intent();PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,1,broadcastIntent,// PendingIntent.FLAG_UPDATE_CURRENT);PendingIntent.FLAG_IMMUTABLE);session.commit(pendingIntent.getIntentSender());session.close();Log.d(TAG, "send install PendingIntent----->");} else {mContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);sendFailMsg(mSessionId);File f = new File(mAppPath);if (f.exists()) {f.delete();}Log.e(TAG, "copy fail delete file----->");}mContext = null;mAppPath = "";mSessionId = -1;}}private class InstallAppInfo {private int sessionId;private String filePath;private PackageInfo info;private boolean isLaunch;public InstallAppInfo(int sessionId, String filePath, PackageInfo info, boolean isLaunch) {this.sessionId = sessionId;this.filePath = filePath;this.info = info;this.isLaunch = isLaunch;}public int getSessionId() {return sessionId;}public String getFilePath() {return filePath;}public PackageInfo getInfo() {return info;}public boolean isLaunch() {return isLaunch;}}private void launchApp(String appPackageName) {Intent mLaunchIntent = mContext.getPackageManager().getLaunchIntentForPackage(appPackageName);if (mLaunchIntent != null) {List<ResolveInfo> list = mContext.getPackageManager().queryIntentActivities(mLaunchIntent, 0);if (list != null && list.size() > 0) {Log.d(TAG, "launch app--->");mContext.startActivity(mLaunchIntent);}}}public void silenceUninstall(String packageName) {Log.i(TAG, "silenceUninstall--->" + packageName);PackageDeleteObserver observer = new PackageDeleteObserver();mContext.getPackageManager().deletePackage(packageName, observer, PackageManager.DELETE_ALL_USERS);}private class PackageDeleteObserver extends IPackageDeleteObserver.Stub {public void packageDeleted(String packageName, int returnCode) {Message msg = mHandler.obtainMessage(MSG_WHAT_UNINSTALL_COMPLETE);msg.arg1 = returnCode;msg.obj = packageName;mHandler.sendMessage(msg);}}
}
二、包安装器PackageInstaller 流程分析-知识点分析
这里我们还是分析一下PackageInstaller
应用基本架构,涉及到需求的基本源码,源码内容源码功能等。 后面再继续说明实现方式。
首先,我需要确认这些类在最新AOSP版本中的职责。InstallStart
作为入口点,处理权限和路由;InstallStaging
负责APK的暂存和验证;PackageInstallerActivity
显示确认界面;InstallInstalling
执行安装操作;DeleteStagedFileOnResult
处理清理。需要确保每个类的说明准确,特别是InstallStaging
和DeleteStagedFileOnResult
的区别,避免用户混淆。
用户可能没有明确说出来的需求是:如何在自己的应用中集成或修改安装流程,或者解决安装过程中的特定错误。因此,解释每个类的关键方法和协作流程尤为重要,比如InstallStaging
使用FileProvider
处理URI权限,这对安全性和Android
版本适配很关键。
还需要注意用户提到的类名差异,比如之前提到的InstallInstalling
可能对应旧版的InstallAppProgress
,确保名称和版本对应,避免误导。最后,总结协作流程图能帮助用户直观理解整个流程,满足他们梳理逻辑的需求。
核心类在安装流程中的职责与协作顺序
核心类详解
核心类InstallStart
作用
安装流程的入口点和路由器(Dispatcher
)。它是 PackageInstaller
应用的主Activity
,在 AndroidManifest.xml
中注册了 android.intent.action.INSTALL_PACKAGE
和 android.intent.action.VIEW (针对 application/vnd.android.package-archive MIME type) 的 Intent Filter
。
详细说明
-
权限检查:在
onCreate
中,它首先进行基础检查,例如调用者是否有权限发起安装请求(如REQUEST_INSTALL_PACKAGES
)。 -
Intent 解析与路由:这是其核心职责。它会分析传入的
Intent(ACTION_VIEW 或 ACTION_INSTALL_PACKAGE
),并检查各种 Extra 标志(如EXTRA_NOT_UNKNOWN_SOURCE
)。 -
决定下一步:根据分析结果,它决定启动哪个
Activity
来继续流程: -
对于绝大多数新的安装请求,它的目的是启动
InstallStaging
。 -
如果请求来自其他特殊源头(如系统应用、adb),它可能会直接启动
PackageInstallerActivity
甚至跳过用户确认。 -
总结:
InstallStart
本身通常不显示界面或只显示一个短暂的启动图(Splash Screen
)。它是一个纯粹的逻辑控制器,负责验证请求的合法性并将正确的 Intent 转发给下一个处理阶段(InstallStaging
或PackageInstallerActivity
)。
核心类InstallStaging
作用
文件准备(Staging
)和验证(Verification
)阶段。负责将传入的 APK
内容复制到一个可由系统安全访问的临时位置(暂存文件,Staged File
)。这是处理各种文件来源(如 content:// Uris
)和进行安装前验证的关键环节。
详细说明
- 处理文件来源:在
onCreate
中,它从Intent
中获取APK
文件的Uri
。这个Uri
可能是:
1) file://:
直接文件路径。
2)content://:
由 ContentProvider
提供的内容(例如从文件管理器、浏览器下载目录中选择)。这是最常见且需要特殊处理的情况。
- 复制文件
1)为了安全地访问 content:// Uri
,它会使用 ParcelFileDescriptor
来打开输入流。
2)它会在应用的私有缓存目录(如 getCacheDir()
)下创建一个临时文件(输出文件)。
3)将输入流中的数据异步复制到这个临时文件中。这个过程会显示一个进度条。
-
可选验证(Obsolete):在非常旧的版本中,这个阶段会进行安装前验证(
Pre-reboot Verification
),但此功能已被废弃。现在主要的验证在 PMS 中完成。 -
启动下一步:当文件成功复制后,它会创建一个新的
Intent
,将原始Intent
的所有Extras
和这个新暂存文件的 Uri 一起设置好,然后启动PackageInstallerActivity
。 -
总结:
InstallStaging
是安装流程的“预处理”阶段。它将不可预测的输入源标准化为一个可靠的、可重复读取的本地文件,为后续的解析和安装操作打下基础。如果文件复制失败,它会直接显示错误并结束流程。
核心类PackageInstallerActivity
作用
安装确认和用户决策界面。这是用户看到的、询问是否要安装应用的对话框界面。
详细说明
-
解析 APK:从 InstallStaging 接收到暂存文件的 Uri 后,它使用
PackageParser
等工具解析这个 APK,提取应用图标、名称、版本号、所需权限等元数据(mPkgInfo, mAppSnippet
)。 -
初始化视图:将解析到的应用信息(图标、名称)设置到界面上的 View 中。
-
权限展示:核心功能之一是列出该应用申请的所有权限,让用户在安装前知悉。
-
未知来源检查:检查设备是否允许安装来自“未知来源”的应用。如果未开启,会引导用户去设置中开启。
-
用户交互:用户点击“安装”后,调用
startInstall()
方法。该方法会创建一个新的 Intent,设置目标 Class 为InstallInstalling
,并将所有必要的信息(如暂存文件的Uri, mPkgInfo
等)作为Extra
传递过去,然后启动InstallInstalling
。 -
总结:
PackageInstallerActivity
是安装流程中与用户交互的核心环节,负责信息展示、风险告知和用户意愿收集。
核心类InstallInstalling
作用
安装执行与进度展示界面。它负责实际的安装操作,并向用户展示安装进度。
详细说明
-
获取参数:从
Intent
中获取PackageInstallerActivity
传过来的暂存文件的Uri
和包信息。 -
创建安装会话 (Session):
1)获取 PackageInstaller
系统服务。
2)创建一个PackageInstaller.SessionParams
对象(模式:MODE_FULL_INSTALL
)。
3)调用 mInstaller.createSession(params)
创建一个安装会话,得到 sessionId
。
- 写入数据
通过 sessionId
打开一个 Session
。
打开暂存文件的输入流。
通过 session.openWrite("package", 0, -1)
打开输出流。
将文件数据从输入流拷贝到 Session 的输出流中。在这个过程中,它会更新进度条。
-
提交安装:数据写入完成后,调用
session.commit(IntentSender)
提交安装请求。这里会传递一个 IntentSender 用于接收安装结果的回调。 -
处理回调:在
onCreate
中注册一个BroadcastReceiver
来等待安装结果(成功或失败)的Intent
,并据此更新界面。 -
总结:
InstallInstalling
是安装流程的执行者,负责与PackageInstaller
服务交互,完成文件传输和安装提交,并将状态反馈给用户。
核心类DeleteStagedFileOnResult
作用
一个简单的、用于清理临时文件的工具类。它通常继承自 BroadcastReceiver
。
详细说明
-
触发时机:它被注册为一个接收者,监听安装结果的广播。
-
核心逻辑:在
onReceive(Context, Intent)
方法中,无论安装成功还是失败,它都会执行一个操作:删除由InstallStaging
创建的暂存文件。 -
文件来源:要删除的文件路径是通过
Intent
的Extra
(例如PackageInstallerActivity.EXTRA_STAGED_FILE
)传递过来的。 -
必要性:这是一个关键的清理机制,确保安装过程结束后,删除临时文件,避免浪费存储空间。即使安装过程崩溃或意外结束,这个机制也应能触发。
-
总结:
DeleteStagedFileOnResult
是一个后勤保障组件,职责单一:在安装流程结束时,清理战场(删除暂存文件)。
总结与完整协作流程
-
路由 (
InstallStart
):用户点击APK
-> 系统启动InstallStart
-> 进行初步检查 -> 启动InstallStaging
。 -
准备 (
InstallStaging
):接收原始Uri
-> 复制内容到临时文件(暂存)-> 启动PackageInstallerActivity
并传递暂存文件Uri
。 -
确认 (
PackageInstallerActivity
):解析暂存文件 -> 展示应用信息和权限 -> 用户点击“安装” -> 启动InstallInstalling
并传递暂存文件Uri
。 -
执行 (
InstallInstalling
):创建Session
-> 将暂存文件数据写入Session
-> 提交给系统PMS
-> 等待并显示结果。 -
清理 (
DeleteStagedFileOnResult
):在安装结果广播中被触发 -> 删除InstallStaging
创建的暂存文件。
这个流程清晰地将文件准备、用户交互和安装执行分离,保证了代码的模块化和健壮性。InstallStaging
和 DeleteStagedFileOnResult
的组合尤其重要,它们优雅地处理了文件生命周期管理。
核心类思考
上面的核心类已经说明了基本流程和包安装器PackageInstaller
的基本流程了,实际在调试代码过程中更能深刻理解每个核心类的作用。
可能光看核心类和其说明,只能初识,但是当实际做项目过程中会慢慢理解,如果先研究代码再看这些核心类的总结会更加豁然开朗。
三、需求实现
需求分析
既然需要实现客户自定义的效果,点击应用或者调用后自动安装,那么上面分析的流程,就需要知己在一个类里面处理了。不然会造成Activity 界面跳转、闪屏的现象了。
思路:所以实现需求的基本思路就是把安装流程中的准备InstallStaging
、安装流程中的确认PackageInstallerActivity
两个过程中所有的操作需要满足如下:
- 所有的验证相关全部写在安装流程
InstallInstalling
中 - 所有的确认选项直接通过
- 所有的错误提示全部放在安装流程
InstallInstalling
中 - 当然,这里需要结合使用使用清理功能
需求实现
这里只提供相关操作的安装流程InstallInstalling
的伪代码,供参考,实际研发过程中会遇到各种问题,但思路清晰了,逐个击破,遇到各种问题解决即可。
/** Copyright (C) 2016 The Android Open Source Project** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package com.android.packageinstaller;import static android.content.pm.PackageInstaller.SessionParams.UID_UNKNOWN;import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.PackageParser;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;import com.android.internal.app.AlertActivity;
import com.android.internal.content.PackageHelper;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
/*** Send package to the package manager and handle results from package manager. Once the* installation succeeds, start {@link InstallSuccess} or {@link InstallFailed}.* <p>This has two phases: First send the data to the package manager, then wait until the package* manager processed the result.</p>*/
public class InstallInstalling extends AlertActivity {private static final String LOG_TAG = InstallInstalling.class.getSimpleName();private static final String SESSION_ID = "com.android.packageinstaller.SESSION_ID";private static final String INSTALL_ID = "com.android.packageinstaller.INSTALL_ID";private static final String BROADCAST_ACTION ="com.android.packageinstaller.ACTION_INSTALL_COMMIT";/** Listens to changed to the session and updates progress bar */private PackageInstaller.SessionCallback mSessionCallback;/** Task that sends the package to the package installer */private InstallingAsyncTask mInstallingTask;/** Id of the session to install the package */private int mSessionId;/** Id of the install event we wait for */private int mInstallId;/** URI of package to install */private Uri mPackageURI;/** The button that can cancel this dialog */// private Button mCancelButton;private CircleProgressBar circleProgressBar;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY| View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));super.onCreate(savedInstanceState);Log.e(LOG_TAG, "onCreate =================");setContentView(R.layout.custom_installing_content_view); ApplicationInfo appInfo = getIntent().getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO);mPackageURI = getIntent().getData();if ("package".equals(mPackageURI.getScheme())) {try {getPackageManager().installExistingPackage(appInfo.packageName);Log.e(LOG_TAG, "onCreate to launchSuccess........");// finish();launchSuccess();} catch (PackageManager.NameNotFoundException e) {launchFailure(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);}} else {Log.e(LOG_TAG, "onCreate ================1111111=====installing_content_view");final File sourceFile = new File(mPackageURI.getPath());PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile);Log.e(LOG_TAG, "onCreate ================22222222===== requireViewById(R.id.installing)");if (savedInstanceState != null) {Log.e(LOG_TAG, "onCreate ================22222222===== savedInstanceState != null ");mSessionId = savedInstanceState.getInt(SESSION_ID);mInstallId = savedInstanceState.getInt(INSTALL_ID);// Reregister for result; might instantly call back if result was delivered while// activity was destroyedtry {InstallEventReceiver.addObserver(this, mInstallId,this::launchFinishBasedOnResult);} catch (EventResultPersister.OutOfIdsException e) {// Does not happen}} else {Log.e(LOG_TAG, "onCreate ================33333333===== savedInstanceState == null ");PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);params.setInstallAsInstantApp(false);params.setReferrerUri(getIntent().getParcelableExtra(Intent.EXTRA_REFERRER));params.setOriginatingUri(getIntent().getParcelableExtra(Intent.EXTRA_ORIGINATING_URI));params.setOriginatingUid(getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID,UID_UNKNOWN));params.setInstallerPackageName(getIntent().getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME));params.setInstallReason(PackageManager.INSTALL_REASON_USER);File file = new File(mPackageURI.getPath());try {PackageParser.PackageLite pkg = PackageParser.parsePackageLite(file, 0);params.setAppPackageName(pkg.packageName);params.setInstallLocation(pkg.installLocation);params.setSize(PackageHelper.calculateInstalledSize(pkg, false, params.abiOverride));} catch (PackageParser.PackageParserException e) {Log.e(LOG_TAG, "Cannot parse package " + file + ". Assuming defaults.");Log.e(LOG_TAG,"Cannot calculate installed size " + file + ". Try only apk size.");params.setSize(file.length());} catch (IOException e) {Log.e(LOG_TAG,"Cannot calculate installed size " + file + ". Try only apk size.");params.setSize(file.length());}try {Log.e(LOG_TAG, "onCreate ================55555555=====InstallEventReceiver ");mInstallId = InstallEventReceiver.addObserver(this, EventResultPersister.GENERATE_NEW_ID,this::launchFinishBasedOnResult);} catch (EventResultPersister.OutOfIdsException e) {Log.e(LOG_TAG, "onCreate ================66666=====INSTALL_FAILED_INTERNAL_ERROR ");launchFailure(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);}try {mSessionId = getPackageManager().getPackageInstaller().createSession(params);} catch (IOException e) {launchFailure(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);}}// mCancelButton = mAlert.getButton(DialogInterface.BUTTON_NEGATIVE);mSessionCallback = new InstallSessionCallback();}}/*** Launch the "success" version of the final package installer dialog*/private void launchSuccess() {Log.e(LOG_TAG, "launchSuccess........");/*Intent successIntent = new Intent(getIntent());successIntent.setClass(this, InstallSuccess.class);successIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);startActivity(successIntent);*/finish();}/*** Launch the "failure" version of the final package installer dialog** @param legacyStatus The status as used internally in the package manager.* @param statusMessage The status description.*/private void launchFailure(int legacyStatus, String statusMessage) {Intent failureIntent = new Intent(getIntent());failureIntent.setClass(this, InstallFailed.class);failureIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);failureIntent.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, legacyStatus);failureIntent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, statusMessage);startActivity(failureIntent);finish();}@Overrideprotected void onStart() {super.onStart();getPackageManager().getPackageInstaller().registerSessionCallback(mSessionCallback);}@Overrideprotected void onResume() {super.onResume();// This is the first onResume in a single life of the activityif (mInstallingTask == null) {PackageInstaller installer = getPackageManager().getPackageInstaller();PackageInstaller.SessionInfo sessionInfo = installer.getSessionInfo(mSessionId);if (sessionInfo != null && !sessionInfo.isActive()) {mInstallingTask = new InstallingAsyncTask();mInstallingTask.execute();} else {// we will receive a broadcast when the install is finished// mCancelButton.setEnabled(false);setFinishOnTouchOutside(false);}}}@Overrideprotected void onSaveInstanceState(Bundle outState) {super.onSaveInstanceState(outState);outState.putInt(SESSION_ID, mSessionId);outState.putInt(INSTALL_ID, mInstallId);}@Overridepublic void onBackPressed() {super.onBackPressed();/* if (mCancelButton.isEnabled()) {}*/}@Overrideprotected void onStop() {super.onStop();getPackageManager().getPackageInstaller().unregisterSessionCallback(mSessionCallback);}@Overrideprotected void onDestroy() {if (mInstallingTask != null) {mInstallingTask.cancel(true);synchronized (mInstallingTask) {while (!mInstallingTask.isDone) {try {mInstallingTask.wait();} catch (InterruptedException e) {Log.i(LOG_TAG, "Interrupted while waiting for installing task to cancel",e);}}}}InstallEventReceiver.removeObserver(this, mInstallId);super.onDestroy();}/*** Launch the appropriate finish activity (success or failed) for the installation result.** @param statusCode The installation result.* @param legacyStatus The installation as used internally in the package manager.* @param statusMessage The detailed installation result.*/private void launchFinishBasedOnResult(int statusCode, int legacyStatus, String statusMessage) {if (statusCode == PackageInstaller.STATUS_SUCCESS) {Log.e(LOG_TAG, "launchFinishBasedOnResult: STATUS_SUCCESS to launchSuccess finish");// finish();launchSuccess();} else {Log.e(LOG_TAG, "launchFinishBasedOnResult: launchFailure legacyStatus:"+legacyStatus+" statusMessage:"+statusMessage);launchFailure(legacyStatus, statusMessage);}}private class InstallSessionCallback extends PackageInstaller.SessionCallback {@Overridepublic void onCreated(int sessionId) {// empty}@Overridepublic void onBadgingChanged(int sessionId) {// empty}@Overridepublic void onActiveChanged(int sessionId, boolean active) {// empty}@Overridepublic void onProgressChanged(int sessionId, float progress) {if (sessionId == mSessionId) {/* ProgressBar progressBar = requireViewById(R.id.progress);progressBar.setMax(Integer.MAX_VALUE);progressBar.setProgress((int) (Integer.MAX_VALUE * progress));*/if(circleProgressBar==null){circleProgressBar = requireViewById(R.id.circleProgressBar);}// circleProgressBar = findViewById(R.id.circleProgressBar);circleProgressBar.setProgress((int)(progress*100));Log.e(LOG_TAG, "onProgressChanged:"+progress);}}@Overridepublic void onFinished(int sessionId, boolean success) {// empty, finish is handled by InstallResultReceiverLog.e(LOG_TAG, "onFinished: sessionId:"+sessionId+" success:"+success);}}/*** Send the package to the package installer and then register a event result observer that* will call {@link #launchFinishBasedOnResult(int, int, String)}*/private final class InstallingAsyncTask extends AsyncTask<Void, Void,PackageInstaller.Session> {volatile boolean isDone;@Overrideprotected PackageInstaller.Session doInBackground(Void... params) {PackageInstaller.Session session;try {session = getPackageManager().getPackageInstaller().openSession(mSessionId);} catch (IOException e) {synchronized (this) {isDone = true;notifyAll();}return null;}session.setStagingProgress(0);try {File file = new File(mPackageURI.getPath());try (InputStream in = new FileInputStream(file)) {long sizeBytes = file.length();try (OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes)) {byte[] buffer = new byte[1024 * 1024];while (true) {int numRead = in.read(buffer);if (numRead == -1) {session.fsync(out);break;}if (isCancelled()) {session.close();break;}out.write(buffer, 0, numRead);if (sizeBytes > 0) {float fraction = ((float) numRead / (float) sizeBytes);session.addProgress(fraction);}}}}return session;} catch (IOException | SecurityException e) {Log.e(LOG_TAG, "Could not write package", e);session.close();return null;} finally {synchronized (this) {isDone = true;notifyAll();}}}@Overrideprotected void onPostExecute(PackageInstaller.Session session) {if (session != null) {Intent broadcastIntent = new Intent(BROADCAST_ACTION);broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);broadcastIntent.setPackage(getPackageName());broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mInstallId);PendingIntent pendingIntent = PendingIntent.getBroadcast(InstallInstalling.this,mInstallId,broadcastIntent,PendingIntent.FLAG_UPDATE_CURRENT);session.commit(pendingIntent.getIntentSender());// mCancelButton.setEnabled(false);setFinishOnTouchOutside(false);} else {getPackageManager().getPackageInstaller().abandonSession(mSessionId);if (!isCancelled()) {Log.e(LOG_TAG, "onPostExecute: INSTALL_FAILED_INVALID_APK");launchFailure(PackageManager.INSTALL_FAILED_INVALID_APK, null);}}}}
}
四、知识点扩展
快速编译项目
遇到的第一个问题:如何提升研发速度和效率:编译一次整包需要30-40分钟,RK平台里面编译一次单独的软件包 mm
、或者 mmm
模块名。 需要十来分钟,还生成单独的包 如 supper.img
,需要单独烧录。
解决方案
只编译当前framework
层代码,生成整包,然后直接烧录,验证问题。 实测发现,编译Frameowrk层代码和打包生成镜像只需要3分钟,瓶颈成了下载固件和烧录,再也不是编译固件耗时了。
修改编译脚本
- 在源码根目录的
build.sh
脚本, 找到make installclean
命令, 将make installclean
注释掉,如下:
- 打包配置时候直接配置打包
Android
文件系统, 去掉uboot 、kernel 、OTA
包 编译 如下:
./build.sh -A其它案例:
./build.sh -Aup
实战如下:
#!/bin/bash#生成软件路径
FS_SW_BIN_PATH=rockdev# SM666_MMMMMM_20250311_V1.0.0.0
#项目名
export FS_PRODUCT_NAME=MMMM_NNNNNN
#日期
FS_SW_DATE=20250320
#版本号
export FS_PRODUCT_VERSION=1.0.0.0
#完整项目软件版本名
export FS_SW_VERSION=${FS_PRODUCT_NAME}_${FS_SW_DATE}_V${FS_PRODUCT_VERSION}function binPac()
{source build/envsetup.shlunch rk3566_r-userdebug# ./build.sh -UKAoup # 带OTA./build.sh -Aup # 带OTA
# ./build.sh -UKAup # 无OTA mkdir ./${FS_SW_BIN_PATH}/${FS_SW_VERSION}cp ./${FS_SW_BIN_PATH}/Image-rk3566_r/update.img ./${FS_SW_BIN_PATH}/${FS_SW_VERSION}/${FS_SW_VERSION}.imgcp ./${FS_SW_BIN_PATH}/Image-rk3566_r/rk3566_r-ota-*.zip ./${FS_SW_BIN_PATH}/${FS_SW_VERSION}/${FS_SW_VERSION}_ota.ziprm -rf ./${FS_SW_BIN_PATH}/Image-rk3566_rreturn 0;
}binPac;
实际效果就是:自动打包成功生成整包固件,下载固件,直接烧录,整个编译过程只需要3分钟不到
替换现有的包安装器
遇到另外一个问题:因为直接在包安装器PackageInstaller
上面修改的UI内容,实际修改量蛮大的。 现在需求就是包安装其对于其它项目是需要使用的,但是指定项目单独用自定义安装器,如何实现?
思路:把以前的包安装器模块不编译到系统,修改名称为另外的模块名称,包安装器作为另外一个模块编译到系统里面去。模块还是用原有的模块名。
遇到问题:
- 找不到包安装前在哪里配置编译的,不知道哪一个.mk 文件里面修改
如何查找替换 包安装器PackageInstaller 模块配置
这个命令很重要,在指定类型文件名中搜索对应的字符串关键字:
grep -r "PackageInstaller" --include="*.mk"
方案实现
- 把修改后的包安装器名称改一下,比如:
YunDianNaoPackageInstaller
项目文件夹放到原有的包安装器PackageInstaller
同级目录; - 修改原有包安装器模块名配置
实现方案:
- 把YunDianNaoPackageInstaller 放到 frameworks\base\packages 目录
- build\make\target\product\base_system.mk 文件中去掉PackageInstaller 改成 YunDianNaoPackageInstaller
- frameworks\base\packages\PackageInstaller\Android.bp 文件中 "PackageInstaller" 模块名称改成其它模块名,比如:PackageInstaller_Replaced
PackageInstaller.Session 知识点
简单来说,PackageInstaller.Session
是一个用于执行 APK
安装或卸载事务的、低级别的系统 API。它提供了对应用安装过程的精细控制,允许你将多个 APK
文件(例如一个基础 APK
和多个拆分 APK
)作为一个原子事务(要么全部成功,要么全部失败)进行流式传输和安装。
核心概念与作用
你可以把PackageInstaller.Session
想象成一个“安装工作台”或一个“事务”:
-
创建会话(
Session
):你告诉系统:“我准备开始安装一个应用了”。系统会为你创建一个“工作区”(Session
),并给你一个Session ID
。 -
传输数据:你将要安装的
APK
文件(一个或多个)一块一块地(流式地)写入这个“工作台”。这个过程允许安装非常大的应用而不会占用双倍存储空间(先下载完再安装)。 -
提交会话:当所有
APK
文件都传输完毕后,你“提交”这个工作。系统会开始正式的安装流程:校验签名、解析包信息、优化Dex
文件等。 -
原子性操作:在整个过程中,如果任何一步失败(如传输中断、签名不一致、空间不足),整个安装事务都会中止,系统会回滚,就像什么都没发生过一样,避免了安装一个不完整或损坏的应用。
主要用途和场景
PackageInstaller.Session
并不是为普通应用商店设计的简单安装场景(它们通常使用 Intent.ACTION_VIEW
来调用系统安装器)。它的强大之处在于以下场景:
静默安装(需特权权限):
这是最主要的使用场景。设备管理(MDM
)应用、系统应用商店(如华为应用市场、三星 Galaxy Store)或拥有 android.permission.INSTALL_PACKAGES
权限的系统应用,可以使用它来在不与用户交互的情况下安装应用。这对于企业设备批量部署应用至关重要。
安装拆分 APK(APK Splits):
现代 Android
应用通常由一个基础 APK 和多个配置拆分 APK(如针对不同 CPU 架构、不同屏幕密度、不同语言)组成。Session API
是唯一能可靠地、原子性地安装整套拆分 APK 的方式。
流式安装:
你可以在下载 APK 文件的同时就开始安装过程,无需等待整个文件下载完成。这可以显著减少从“下载完成”到“可以打开应用”的等待时间,提升用户体验。
获取详细的安装结果:
使用 Intent 安装时,你只能知道用户是点了“安装”还是“取消”。而使用 Session
,你可以通过 PackageInstaller.SessionCallback 获取精确的安装状态码(如STATUS_SUCCESS, STATUS_FAILURE_STORAGE, STATUS_FAILURE_INCOMPATIBLE
等),便于进行错误处理和日志记录。
创建安装会话:
你甚至可以创建一个会话,然后将 Session ID
交给另一个进程(甚至是另一台设备),由它来继续完成文件的传输和提交。这为实现复杂的安装逻辑提供了可能性。
基本工作流程(代码示例概览)
以下是一个简化的使用流程,展示了核心步骤:
获取 PackageInstaller
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
创建会话参数
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
params.setAppPackageName("com.example.myapp");
// 可以设置其他参数,如安装位置等
创建会话
int sessionId = packageInstaller.createSession(params);
打开会话并传输 APK 数据:
PackageInstaller.Session session = packageInstaller.openSession(sessionId);try (OutputStream out = session.openWrite("base.apk", 0, -1);InputStream in = new FileInputStream(apkFile)) {byte[] buffer = new byte[65536];int length;while ((length = in.read(buffer)) != -1) {out.write(buffer, 0, length);}session.fsync(out); // 确保数据写入磁盘
}
提交会话(开始真正安装)
// 创建一个用于接收安装结果的 PendingIntent
Intent intent = new Intent(context, InstallResultReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);session.commit(pendingIntent.getIntentSender());
session.close();
处理安装结果
系统会通过你提供的 PendingIntent
广播一个带有 PackageInstaller.EXTRA_STATUS
的 Intent
,你在广播接收器里可以获取结果
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
if (status == PackageInstaller.STATUS_SUCCESS) {// 安装成功
} else {String errorMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);// 处理错误
}
安装小结
总而言之,PackageInstaller.Session
是 Android
系统底层一个强大而专业的包管理工具,它为需要深度集成和设备管理能力的特定类型的应用程序提供了必要的接口。
特性 | PackageInstaller.Session | 普通 Intent 安装 |
---|---|---|
控制力 | 高,可编程控制整个过程 | 低,交给系统安装器 |
交互 | 可无交互(静默安装,需权限) | 必须用户确认 |
适用对象 | 系统应用、设备管理应用、特权应用 | 所有应用 |
功能 | 支持拆分 APK、流式安装、原子操作 | 仅支持单个 APK 安装 |
复杂度 | 高 | 低(一行代码) |
总结
- 包安装器流程架构很清晰的,特别是在理解相关代码后
- 核心逻辑就是
PackageInstaller.Session
- 初识需求慢复杂的,特别是研究代码的时候,整个流程走通了发现真清晰
- 我觉得理解包安装器架构、业务流程和
PackageInstaller.Session
知识点,来实现静默安装或者通过PackageInstaller.Session
来实现安装的一个思路,特别重要,并在实践中运用。 - 这里包安装器和之前的静默安装,其实就是
在这里插入代码片
实现安装的两个场景,一个有界面一个没有界面而已。