提示:Android15-整合PackageInstaller实现静默安装功能
文章目录
- 前言
- [一、 需求](#一、 需求)
- 二、参考资料
- 三、文件修改
- 四、实现方案
-
- [1、InstallStart - onCreate 方法中-判断是否拦截进行静默安装-拷贝文件](#1、InstallStart - onCreate 方法中-判断是否拦截进行静默安装-拷贝文件)
- [2、SilenceInstallReceiver 接收静默安装广播请求-进行静默安装业务](#2、SilenceInstallReceiver 接收静默安装广播请求-进行静默安装业务)
- 3、静默安装核心工具类-SilenceInstallManager
- [4、PackageInstallerApplication 初始化静默安装工具](#4、PackageInstallerApplication 初始化静默安装工具)
- [5、AndroidManifest 配置 广播组件](#5、AndroidManifest 配置 广播组件)
- 五、实验验证-进行静默安装测试
- 六、拓展思路-讨论
- 总结
前言
在RK 平台 Android15 上面做静默安装功能
一、 需求
在Android15 上面实现静默安装功能,不要用脚本实现,借助包安装器PackageInstaller 来实现。
二、参考资料
MTK-Android13-包安装器PackageInstaller 静默安装实现
本文笔记完全参考之前知识点 MTK平台Android13 版本思想,在安装器App-PackageInstaller 基础上更改部分流程实现静默安装功能。
- 可以参考、copy 全部代码:思想完全一样的,借助安装器,添加自己的业务代码,客户是否静默安装看客户自己选择。
- 以前是Android13 且 MTK平台,在Android15 上面部分API、机制、权限有区别,适当几个点适配下即可,本章会后面分析会做简单介绍。
Android15-解决后台无法启动应用App问题 静默安装成功后 有 是否直接启动的参数,这个业务在Android15 中无法后台启动App,这里有修改方案
针对PM 安装流程、PackageInstaller 安装流程,如果有需要强烈建议如下知识篇 相关知识点。 对 安装流程、PackageInstaller 安装等知识面和业务流程有非常全面的介绍。
RK-Android11-PackageInstaller安装器自动安装功能实现 魔改PackageInstaller 去掉各种状态跳转,自定义一个界面一个带进度界面安装实现。
Rk3576-Android15-Apk 安装基本知识点和Apk安装拦截功能实现 Framework层 安装过程流程分析
拦截adb install/uninstall安装 - 安装流程分析 命令脚本安装流程分析
PMS安装apk之界面跳转 这个是对 PackageInstaller 安装器安装流程,界面跳转流程分析
三、文件修改
包安装前 PackageInstaller 目录:/frameworks/base/packages/PackageInstaller
新增文件:
java
src/com/android/packageinstaller/SilenceInstallManager.java
src/com/android/packageinstaller/SilenceInstallReceiver.java
修改文件:
java
AndroidManifest.xml
src/com/android/packageinstaller/InstallStart.java
src/com/android/packageinstaller/PackageInstallerApplication.java
src/com/android/packageinstaller/UninstallerActivity.java

四、实现方案
1、InstallStart - onCreate 方法中-判断是否拦截进行静默安装-拷贝文件
在 InstallStart 入口onCreate 方法中,拦截,是否进行静默安装,如果是,那么就进行静默安装逻辑,拷贝文件。拦截正常的安装流程。
java
// modify by fangchen start
// if silence install ->to silence install
if (intent.getBooleanExtra(SilenceInstallReceiver.SILENCE_INSTALL_KEY, false)) {
Log.d(TAG," StagingAsyncAppTask to execute ");
StagingAsyncAppTask mStagingTask = new StagingAsyncAppTask(intent.getBooleanExtra(SilenceInstallReceiver.IS_LAUNCH_KEY, false));
mStagingTask.execute(getIntent().getData());
return;
}
// modify by fangchen end
拷贝 文件资源代码如下:
java
@SuppressLint("NewApi")
private final class StagingAsyncAppTask extends AsyncTask<Uri, Void, File> {
private boolean mIsLaunch;
public StagingAsyncAppTask(boolean isLaunch){
mIsLaunch = isLaunch;
}
@Override
protected File doInBackground(Uri... params) {
Log.d(TAG, "===========copy file from user app start");
if (params == null || params.length <= 0) {
return null;
}
Uri packageUri = params[0];
try (InputStream in = getContentResolver().openInputStream(packageUri)) {
// Despite the comments in ContentResolver#openInputStream the returned stream can
// be null.
if (in == null) {
return null;
}
File mStagedFile = TemporaryFileManager.getStagedFile(InstallStart.this);
try (OutputStream out = new FileOutputStream(mStagedFile)) {
byte[] buffer = new byte[1024 * 1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) >= 0) {
// Be nice and respond to a cancellation
out.write(buffer, 0, bytesRead);
}
}
return mStagedFile;
} catch (IOException | SecurityException | IllegalStateException e) {
Log.w(TAG, "Error staging apk from content URI", e);
}
return null;
}
@Override
protected void onPostExecute(File installFile) {
if (null != installFile) {
// Now start the installation again from a file
Log.d(TAG, "copy file from user app finish");
Intent installIntent = new Intent(SilenceInstallReceiver.SILENCE_INSTALL_APP);
installIntent.putExtra(SilenceInstallReceiver.APP_URI_KEY, Uri.fromFile(installFile));
installIntent.putExtra(SilenceInstallReceiver.IS_LAUNCH_KEY, mIsLaunch);
installIntent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
installIntent.setPackage("com.android.packageinstaller");
sendBroadcast(installIntent);
Log.d(TAG, "send to install");
} else {
Log.d(TAG, "copy file from user app fail");
}
finish();
}
}
2、SilenceInstallReceiver 接收静默安装广播请求-进行静默安装业务
如上,在进行安装初期InstallStart 中 判断当前进行静默后,广播开始通知进行静默安装
java
package com.android.packageinstaller;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
public class SilenceInstallReceiver extends BroadcastReceiver {
public static final String SILENCE_INSTALL_APP = "com.android.packageinstaller.ACTION_SILENCE_INSTALL";
public static final String SILENCE_INSTALL_KEY = "silence_install";
public static final String IS_LAUNCH_KEY = "is_launch";
public static final String APP_URI_KEY = "app_uri";
private static final String TAG = "SilenceInstallReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.w(TAG, "SilenceInstallReceiver getAction---->" + intent.getAction());
if (SILENCE_INSTALL_APP.equals(intent.getAction())) {
Uri uri = intent.getParcelableExtra(APP_URI_KEY);
boolean isLaunch = intent.getBooleanExtra(IS_LAUNCH_KEY, false);
SilenceInstallManager.getInstance(context).silenceInstall(uri, isLaunch);
}
}
}
3、静默安装核心工具类-SilenceInstallManager
假使对安装流程有相关的了解和掌握,我个人经验就是两点:
- 文件的拷贝,
uri的传递,按照规范传递参数拼凑数据 - 进行
Session处理,提交 到Framework层 进行安装。 - 对于安装回调进度,注册
Session回调而已
java
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.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 android.content.ActivityNotFoundException;
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.os.ParcelFileDescriptor;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.content.ActivityNotFoundException;
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 @Nullable PackageInstaller mInstaller;
private static volatile SilenceInstallManager INSTANCE;
private SilenceInstallManager(Context context) {
mContext = context;
mInstaller = mContext.getPackageManager().getPackageInstaller();
}
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() {
@Override
public void onCreated(int sessionId) {
Log.d(TAG, "onCreated---->" + sessionId);
}
@Override
public void onBadgingChanged(int sessionId) {
Log.w(TAG, "mSessionCallback onBadgingChanged---->" + sessionId);
}
@Override
public void onActiveChanged(int sessionId, boolean active) {
Log.w(TAG, "mSessionCallback onActiveChanged---->" + sessionId + " active--->" + active);
}
@Override
public void onProgressChanged(int sessionId, float progress) {
Log.w(TAG, "mSessionCallback onProgressChanged---->" + sessionId + " progress--->" + progress);
}
@Override
public 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() {
@Override
public void dispatchMessage(@NonNull Message msg) {
mInstaller.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===========111111111111111===");
}
} else if (msg.what == MSG_WHAT_INSTALL_FINISH_FAIL) {
int sessionId = msg.arg1;
if (sessionId != -1) {
InstallAppInfoMap.remove(sessionId);
}
Log.d(TAG, "install fail===========00000000000000000===");
} 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) {
public void silenceInstall(Uri uri, boolean launch) {
String appFilePath=uri.getPath();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
mInstaller.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()) {
Log.d(TAG,"=====silenceInstall wenjian not exists======appFilePath:"+appFilePath);
sendFailMsg(-1);
return;
}
final AssetFileDescriptor afd = openAssetFileDescriptor(uri);
final Uri packageUri =uri;// getIntent().getData();
ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null;
if (pfd != null) {
Log.d(TAG,"===========pfd != null=================");
try {
String debugPathName= packageUri.toString();
final PackageInstaller.InstallInfo result = mInstaller.readInstallInfo(pfd,
debugPathName, 0);
params.setAppPackageName(result.getPackageName());
params.setInstallLocation(result.getInstallLocation());
params.setSize(result.calculateInstalledSize(params, pfd));
params.setSize(file.length());
}catch (Exception 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());
}
}else{
Log.d(TAG,"===========pfd ====== null=================");
}
try {
PackageInfo mPkgInfo = PackageUtil.getPackageInfo(mContext, file, PackageManager.GET_PERMISSIONS);
int mSessionId =mInstaller.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();
Log.d(TAG,"============InstallingAsyncTask execute failed ==============");
sendFailMsg(-1);
}
}
}
public AssetFileDescriptor openAssetFileDescriptor(Uri uri) {
try {
return mContext.getContentResolver().openAssetFileDescriptor(uri, "r");
} catch (Exception e) {
Log.d(TAG, "========Failed to open asset file descriptor", e);
return null;
}
}
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;
}
@Override
protected PackageInstaller.Session doInBackground(Void... params) {
PackageInstaller.Session session;
try {
session = mInstaller.openSession(mSessionId);
} catch (IOException e) {
return null;
}
session.setStagingProgress(0);
try {
File file = new File(mAppPath);
long totalRead = 0;
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);*/
totalRead += numRead;
float fraction = ((float) totalRead / (float) sizeBytes);
session.setStagingProgress(fraction);
// publishProgress((int) (fraction * 100.0));
}
}
}
}
return session;
} catch (IOException | SecurityException e) {
Log.e(TAG, "Could not write package", e);
session.close();
return null;
}
}
@Override
protected 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_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
// PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE |PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
);
session.commit(pendingIntent.getIntentSender());
session.close();
Log.d(TAG, "send install PendingIntent----->");
} else {
mInstaller.abandonSession(mSessionId);
Log.d(TAG,"============session error =======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) {
Log.d(TAG,"=======launchApp========appPackageName:"+appPackageName);
Intent mLaunchIntent = mContext.getPackageManager().getLaunchIntentForPackage(appPackageName);
if (mLaunchIntent != null) {
Log.d(TAG, "launch app---> appPackageName:"+appPackageName);
if (mLaunchIntent.resolveActivity(mContext.getPackageManager()) != null) {
Log.d(TAG, "launch app---> startActivity ");
try {
mLaunchIntent.addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
mContext.startActivity(mLaunchIntent);
} catch (ActivityNotFoundException | SecurityException e) {
Log.e(TAG, "Could not start activity", e);
}
} else {
Log.e(TAG, "No activity found to handle the intent for: " + appPackageName);
}
}else{
Log.d(TAG,"===============qidong failed========");
}
}
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);
}
}*/
}
4、PackageInstallerApplication 初始化静默安装工具
java
SilenceInstallManager.getInstance(this);

5、AndroidManifest 配置 广播组件
java
<!-- modify by fangchen start -->
<receiver android:name=".SilenceInstallReceiver"
android:exported="true">
<intent-filter android:priority="1">
<action android:name="com.android.packageinstaller.ACTION_SILENCE_INSTALL" />
</intent-filter>
</receiver>
<!-- modify by fangchen end -->
五、实验验证-进行静默安装测试
这里可以传递两个参数:
- 是否静默安装
silence_install,如果否 则走默认的安装器安装方式 - 安装成功后是否自启动
is_launch
备注说明:在实际业务中,如果下载成功 软件包 文件 apk,后直接调用当前业务代码即可实现。
java
val aPath ="/data/data/com.fish.togetsn/files" + File.separator + "bilibili.apk"
Log.d(TAG," aPath path:"+aPath);
val appFile = File(aPath)
if (!appFile.exists()) {
//showToast("请在" + getExternalFilesDir(null)!!.absolutePath + File.separator + "目录中放置升级文件")
ToastUtils.showShort("请 配置文件")
return@setOnClickListener
}
//大于7.0使用此方法
val apkUri = FileProvider.getUriForFile(
ContextProvider.get().context,
"com.fish.togetsn.fileProvider",
appFile
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val installApkIntent = Intent()
installApkIntent.setAction(Intent.ACTION_VIEW)
installApkIntent.addCategory(Intent.CATEGORY_DEFAULT)
installApkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
installApkIntent.setDataAndType(apkUri, "application/vnd.android.package-archive")
installApkIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
//设置静默安装标识
installApkIntent.putExtra("silence_install", true)
installApkIntent.setPackage("com.android.packageinstaller")
//设置安装完成是否启动标识
installApkIntent.putExtra("is_launch", true)
//设置后台中启动activity标识
// installApkIntent.putExtra("allowed_Background", true)
if (packageManager.queryIntentActivities(installApkIntent, 0).size > 0) {
startActivity(installApkIntent)
}
}
六、拓展思路-讨论
- 这里模仿的是下载软件成功后,借用 系统自带安装前
PackageInstaller实现。 其中拷贝文件是在入口InstallStart这个Activity中实现的,那么实际上 我自己把这部分代码实现,也就是拷贝文件实现,直接发送广播给安装器进行安装业务不行吗? - 如上 思考,我们使用了
PackageInstaller的入口InstallStart这个Activity,屏幕会闪一下,实际过程中这个如果避免达到无感? - 这里都是在借助
PackageInstaller这个包实现的,比如我们设置com.android.packageinstaller指定的包名进行application/vnd.android.package-archive业务,实际上 静默安装和非静默安装混在一起的, 那么就不能分离开来直接在Framework层的Service服务中实现吗? 这里先给出答案,绝对可行。 - 其实静默安装 不管哪一个版本都有自己的一套,比如开机第一次 会内置apk, 把这个业务流程单独提出来不也是一个思路吗。
总结
- 这里进行了Android15 ,RK平台 静默安装功能,针对以前的知识点再次实践而已,这里面涉及到一些API变化、规则变化,参考
PackageInstaller业务代码和流程实现。 - 静默安装核心要点还是
session的传递、提交、校验 等。 分为两部分:前端和Framework层,每个知识点都很重要,建议多研究代码,会收获很多。