RK3576-Android15-整合PackageInstaller达成静默安装功能

提示:Android15-整合PackageInstaller实现静默安装功能


前言

在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
新增文件:

src/com/android/packageinstaller/SilenceInstallManager.java
src/com/android/packageinstaller/SilenceInstallReceiver.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 方法中,拦截,是否进行静默安装,如果是,那么就进行静默安装逻辑,拷贝文件。拦截正常的安装流程。

// 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

拷贝 文件资源代码如下:

@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 中 判断当前进行静默后,广播开始通知进行静默安装

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回调而已
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 初始化静默安装工具

SilenceInstallManager.getInstance(this);

在这里插入图片描述

5、AndroidManifest 配置 广播组件

<!-- 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,后直接调用当前业务代码即可实现。

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层,每个知识点都很重要,建议多研究代码,会收获很多。
posted @ 2026-01-17 16:20  gccbuaa  阅读(0)  评论(0)    收藏  举报