android 实现静默安装与卸载
Android实现静默安装和卸载
前言
采用系统应用安装管理器方式,需要系统签名才可使用,可用于应用商店,负一屏,launcher等应用
安装流程
这是我们平时使用的发起显式安装请求,会有弹窗提示安装确认
public static void install(Context context, File file) {
Intent installApkIntent = new Intent();
installApkIntent.setAction(Intent.ACTION_VIEW);
installApkIntent.addCategory(Intent.CATEGORY_DEFAULT);
installApkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//适配8.0需要有权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = context.getPackageManager().canRequestPackageInstalls();
if (hasInstallPermission) {
//安装应用
installApkIntent.setDataAndType(FileProvider.getUriForFile(context.getApplicationContext(), context.getPackageName() + ".file_provider", file), "application/vnd.android.package-archive");
installApkIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (context.getPackageManager().queryIntentActivities(installApkIntent, 0).size() > 0) {
context.startActivity(installApkIntent);
}
}
}
}
系统收到用户安装请求会调用系统内部预置的packageinstaller.apk,首先是启动InstallStart(Activity),在onCreate中构建引用的基本信息,然后传递给PackageInstallerActivity(AlertActivity),代码如下:
Intent nextActivity = new Intent(intent);
nextActivity.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
| Intent.FLAG_GRANT_READ_URI_PERMISSION);
// The the installation source as the nextActivity thinks this activity is the source, hence
// set the originating UID and sourceInfo explicitly
nextActivity.putExtra(PackageInstallerActivity.EXTRA_CALLING_PACKAGE, callingPackage);
nextActivity.putExtra(PackageInstallerActivity.EXTRA_CALLING_ATTRIBUTION_TAG,
callingAttributionTag);
nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo);
nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid);
if (isSessionInstall) {
nextActivity.setClass(this, PackageInstallerActivity.class);
} else {
Uri packageUri = intent.getData();
if (packageUri != null && packageUri.getScheme().equals(
ContentResolver.SCHEME_CONTENT)) {
// [IMPORTANT] This path is deprecated, but should still work. Only necessary
// features should be added.
// Copy file to prevent it from being changed underneath this process
nextActivity.setClass(this, InstallStaging.class);
} else if (packageUri != null && packageUri.getScheme().equals(
PackageInstallerActivity.SCHEME_PACKAGE)) {
nextActivity.setClass(this, PackageInstallerActivity.class);
} else {
Intent result = new Intent();
result.putExtra(Intent.EXTRA_INSTALL_RESULT,
PackageManager.INSTALL_FAILED_INVALID_URI);
setResult(RESULT_FIRST_USER, result);
nextActivity = null;
}
}
if (nextActivity != null) {
startActivity(nextActivity);
}
PackageInstallerActivity启动后在onCreate中进行一系列解析apk信息的操作,然后在onResume中创建弹窗,再将解析的信息显示在窗口上面,当用户点击确认安装时,再启动InstallInstalling(AlertActivity)
final Intent intent = getIntent();
mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG);
mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
PackageInstaller.SessionParams.UID_UNKNOWN);
mOriginatingPackage = (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN)
? getPackageNameForUid(mOriginatingUid) : null;
final Uri packageUri;
if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction())) {
final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(sessionId);
if (info == null || !info.sealed || info.resolvedBaseCodePath == null) {
Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
finish();
return;
}
mSessionId = sessionId;
packageUri = Uri.fromFile(new File(info.resolvedBaseCodePath));
mOriginatingURI = null;
mReferrerURI = null;
} else {
mSessionId = -1;
packageUri = intent.getData();
mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
}
// if there's nothing to do, quietly slip into the ether
if (packageUri == null) {
Log.w(TAG, "Unspecified source");
setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
finish();
return;
}
if (DeviceUtils.isWear(this)) {
showDialogInner(DLG_NOT_SUPPORTED_ON_WEAR);
return;
}
boolean wasSetUp = processPackageUri(packageUri);
if (mLocalLOGV) Log.i(TAG, "wasSetUp: " + wasSetUp);
if (!wasSetUp) {
return;
}
··· ···
@Override
protected void onResume() {
super.onResume();
if (mLocalLOGV) Log.i(TAG, "onResume(): mAppSnippet=" + mAppSnippet);
if (mAppSnippet != null) {
// load dummy layout with OK button disabled until we override this layout in
// startInstallConfirm
bindUi();
checkIfAllowedAndInitiateInstall();
}
if (mOk != null) {
mOk.setEnabled(mEnableOk);
}
}
··· ···
private void bindUi() {
mAlert.setIcon(mAppSnippet.icon);
mAlert.setTitle(mAppSnippet.label);
mAlert.setView(R.layout.install_content_view);
mAlert.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.install),
(ignored, ignored2) -> {
if (mOk.isEnabled()) {
if (mSessionId != -1) {
mInstaller.setPermissionsResult(mSessionId, true);
finish();
} else {
startInstall();
}
}
}, null);
mAlert.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.cancel),
(ignored, ignored2) -> {
// Cancel and finish
setResult(RESULT_CANCELED);
if (mSessionId != -1) {
mInstaller.setPermissionsResult(mSessionId, false);
}
finish();
}, null);
setupAlert();
mOk = mAlert.getButton(DialogInterface.BUTTON_POSITIVE);
mOk.setEnabled(false);
if (!mOk.isInTouchMode()) {
mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).requestFocus();
}
}
··· ···
private void startInstall() {
// Start subactivity to actually install the application
Intent newIntent = new Intent();
newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
mPkgInfo.applicationInfo);
newIntent.setData(mPackageURI);
newIntent.setClass(this, InstallInstalling.class);
String installerPackageName = getIntent().getStringExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME);
if (mOriginatingURI != null) {
newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);
}
if (mReferrerURI != null) {
newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
}
if (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
}
if (installerPackageName != null) {
newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME,
installerPackageName);
}
if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
}
newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI);
startActivity(newIntent);
finish();
}
··· ···
InstallInstalling启动后在onCreate方法中会先将安装apk时所需要的一些参数提前初始化完毕,然后在onResume中调用安装异步任务,在
@Override
protected void onResume() {
super.onResume();
// This is the first onResume in a single life of the activity
if (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);
}
}
}
/**
* 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;
@Override
protected 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();
}
}
}
@Override
protected 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 | PendingIntent.FLAG_MUTABLE);
session.commit(pendingIntent.getIntentSender());
mCancelButton.setEnabled(false);
setFinishOnTouchOutside(false);
} else {
getPackageManager().getPackageInstaller().abandonSession(mSessionId);
if (!isCancelled()) {
launchFailure(PackageInstaller.STATUS_FAILURE,
PackageManager.INSTALL_FAILED_INVALID_APK, null);
}
}
}
}
到这里安装就可以告一段落了,后续的操作由PackageInstaller.Session内部的IPackageInstallerSession完成,我们需要的功能到这里也够了。
卸载流程
接下来我们来看下卸载的流程,卸载相对于安装来说简单了许多,用户发起一个卸载请求
public static void uninstall(Context context, String packageName) {
//获取删除包名的URI
Uri uri = Uri.parse("package:" + packageName);
Intent intent = new Intent();
//设置我们要执行的卸载动作
intent.setAction(Intent.ACTION_DELETE);
//设置获取到的URI
intent.setData(uri);
context.startActivity(intent);
}
系统接收到请求后启动UninstallerActivity(Activity),在onCreate里面通过传入的包名加载apk信息,然后创建用户确认弹窗,当用户点击确认按钮时再来到startUninstallProgress(boolean keepData)
public void startUninstallProgress(boolean keepData) {
... ...
int uninstallId;
try {
uninstallId = UninstallEventReceiver.getNewId(this);
} catch (EventResultPersister.OutOfIdsException e) {
showGenericError();
return;
}
Intent broadcastIntent = new Intent(this, UninstallFinish.class);
broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
broadcastIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, mDialogInfo.allUsers);
broadcastIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mDialogInfo.appInfo);
broadcastIntent.putExtra(UninstallFinish.EXTRA_APP_LABEL, label);
broadcastIntent.putExtra(UninstallFinish.EXTRA_UNINSTALL_ID, uninstallId);
PendingIntent pendingIntent =
PendingIntent.getBroadcast(this, uninstallId, broadcastIntent,
PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
NotificationChannel uninstallingChannel = new NotificationChannel(UNINSTALLING_CHANNEL,
getString(R.string.uninstalling_notification_channel),
NotificationManager.IMPORTANCE_MIN);
notificationManager.createNotificationChannel(uninstallingChannel);
Notification uninstallingNotification =
(new Notification.Builder(this, UNINSTALLING_CHANNEL))
.setSmallIcon(R.drawable.ic_remove).setProgress(0, 1, true)
.setContentTitle(getString(R.string.uninstalling_app, label)).setOngoing(true)
.build();
notificationManager.notify(uninstallId, uninstallingNotification);
try {
Log.i(TAG, "Uninstalling extras=" + broadcastIntent.getExtras());
int flags = mDialogInfo.allUsers ? PackageManager.DELETE_ALL_USERS : 0;
flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
ActivityThread.getPackageManager().getPackageInstaller().uninstall(
new VersionedPackage(mDialogInfo.appInfo.packageName,
PackageManager.VERSION_CODE_HIGHEST),
getPackageName(), flags, pendingIntent.getIntentSender(),
mDialogInfo.user.getIdentifier());
} catch (Exception e) {
notificationManager.cancel(uninstallId);
Log.e(TAG, "Cannot start uninstall", e);
showGenericError();
}
}
到这里就完成我们的需求了,之后再由PackageInstaller里面的IPackageInstaller的uninstall方法完成具体的卸载操作。
简单实现
基于以上的了解,相信大家对apk的安装和卸载都有了一定的认知,陆游的《冬夜读书示子聿》说到一句:纸上得来终觉浅,绝知此事要躬行。很多时候我们看似会了,但不实际动手操作的话很难进一步的了解,不实践永远也不知道坑有多少,所以就一起来动手实现一个安装和卸载的功能吧。
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.util.Log;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Copyright 王字旁的理
* Date: 2022/8/18
* Description: 静默安装与卸载
* Author: zl
*/
public class InstallUtil {
private static final String TAG = "Install";
/**
* 安装apk
* @param context
* @param filePath
*/
public static synchronized void installApk(Context context, String filePath) {
if (!isSystemSign(context)) {
Log.e(TAG, "apk不具备系统签名,无法使用静默安装功能!");
install(context, filePath);
return;
}
File apkFile = new File(filePath);
Log.e(TAG, "apkPath " + apkFile.getAbsolutePath());
if (!apkFile.exists()) {
Log.e(TAG, "apk 不存在!");
return;
}
//1. 获取包安装程序
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
//2. 安装参数
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
//设置大小
sessionParams.setSize(apkFile.length());
//3. 会话id
int sessionId = createSession(packageInstaller, sessionParams);
Log.e(TAG, "sessionId " + sessionId);
if (sessionId != -1) {
//4. 将数据拷贝进session
boolean copySuccess = copyInstallFile(packageInstaller, sessionId, filePath);
Log.e(TAG, "copySuccess " + copySuccess);
if (copySuccess) {
//5. 执行安装
execInstallCommand(context, packageInstaller, sessionId);
}
}
}
/**
* 显示安装
*
* @param context
* @param filePath
*/
public static synchronized void install(Context context, String filePath) {
File apkFile = new File(filePath);
Log.e(TAG, "apkPath " + apkFile.getAbsolutePath());
if (!apkFile.exists()) {
Log.e(TAG, "apk 不存在!");
return;
}
Intent installApkIntent = new Intent();
installApkIntent.setAction(Intent.ACTION_VIEW);
installApkIntent.addCategory(Intent.CATEGORY_DEFAULT);
installApkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//这里只适配了8.0需要有权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = context.getPackageManager().canRequestPackageInstalls();
if (hasInstallPermission) {
//通过FileProvider赋予apk访问权限
Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
installApkIntent.setDataAndType(uri, "application/vnd.android.package-archive");
installApkIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (context.getPackageManager().queryIntentActivities(installApkIntent, 0).size() > 0) {
context.startActivity(installApkIntent);
}
}
}
}
/**
* 卸载apk
*
* @param context
* @param packageName
*/
public static synchronized void uninstallPackage(Context context, String packageName) {
if (!isSystemSign(context)) {
Log.e(TAG, "apk不具备系统签名,无法使用静默安装功能!");
uninstall(context, packageName);
return;
}
Intent intent = new Intent(context, UninstallResultReceiver.class);
intent.setAction(PackageInstaller.EXTRA_STATUS);
//创建卸载广播意图
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
//获取安装程序
PackageInstaller installer = context.getPackageManager().getPackageInstaller();
//执行卸载操作
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//卸载最高版本apk
installer.uninstall(new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), pendingIntent.getIntentSender());
} else {
//卸载apk
installer.uninstall(packageName, pendingIntent.getIntentSender());
}
}
/**
* 显式卸载
*
* @param context
* @param packageName
*/
public static synchronized void uninstall(Context context, String packageName) {
//获取删除包名的URI
Uri uri = Uri.parse("package:" + packageName);
Intent intent = new Intent();
//设置我们要执行的卸载动作
intent.setAction(Intent.ACTION_DELETE);
//设置获取到的URI
intent.setData(uri);
context.startActivity(intent);
}
/**
* 创建sessionId
*
* @param packageInstaller
* @param sessionParams
* @return
*/
private static int createSession(PackageInstaller packageInstaller, PackageInstaller.SessionParams sessionParams) {
int sessionId = -1;
try {
//根据sessionParams创建sessionId
sessionId = packageInstaller.createSession(sessionParams);
} catch (IOException e) {
e.printStackTrace();
}
return sessionId;
}
/**
* 拷贝apk文件,写入PackageInstaller.Session
*
* @param packageInstaller
* @param sessionId
* @param apkFilePath
* @return
*/
private static boolean copyInstallFile(PackageInstaller packageInstaller, int sessionId, String apkFilePath) {
InputStream in = null;
OutputStream out = null;
PackageInstaller.Session session = null;
boolean success = false;
try {
File apkFile = new File(apkFilePath);
//通过sessionId获取PackageInstaller.Session
session = packageInstaller.openSession(sessionId);
//打开输入流
out = session.openWrite("base.apk", 0, apkFile.length());
//创建文件流
in = new FileInputStream(apkFile);
int total = 0, c;
byte[] buffer = new byte[1024 * 1024];
//读取文件流
while ((c = in.read(buffer)) != -1) {
total += c;
out.write(buffer, 0, c);
}
//同步数据
session.fsync(out);
Log.i(TAG, "streamed " + total + " bytes");
success = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
if (session != null) {
session.close();
}
}
return success;
}
/**
* 执行安装
*
* @param context
* @param packageInstaller
* @param sessionId
* @return
*/
private static void execInstallCommand(Context context, PackageInstaller packageInstaller, int sessionId) {
PackageInstaller.Session session = null;
try {
//通过sessionId获取PackageInstaller.Session
session = packageInstaller.openSession(sessionId);
//创建一个广播意图
Intent intent = new Intent(context, InstallResultReceiver.class);
intent.setAction(PackageInstaller.EXTRA_STATUS);
//设置广播接受者
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
//执行安装命令,安装完成将发送广播通知
session.commit(pendingIntent.getIntentSender());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (session != null) {
session.close();
}
}
}
/**
* 根据包名判断app是否具有系统签名
*/
private static boolean isSystemSign(Context context) {
return context.getPackageManager().checkSignatures(Binder.getCallingUid(), android.os.Process.SYSTEM_UID) == PackageManager.SIGNATURE_MATCH;
}
}
总结代码里面都有注解,写的也还是比较详细,希望对大家有用,代码没有做Android版本适配,我这边测试是基于Android12虚拟机测试的,虚拟机用的是原生系统,所以也可去下载Google源码仓库里下载对应的platform.pk8和platform.x509.pem这两个文件,源码仓库链接直达: google源码仓库 ,下载完之后制作签名文件放入apk,打包时使用系统签名,生成的包就具有系统权限了。要是各位不想浪费时间和C币购的话大家也可以用我打包好的,直达链接: Android12系统签名 备注有使用方式,一目了然。
结语
如果你觉得文章还不错就点个免费的小爱心吧,后续有时间还会继续分享各个系列的知识点,感兴趣的话点个关注吧!
如果你有更好的提议欢迎评论区留言或者私信!
更多推荐
所有评论(0)