使用activity安装xapk

This commit is contained in:
2025-04-23 23:46:43 +08:00
parent a95958cf20
commit 6c4b3ffc8e
8 changed files with 371 additions and 93 deletions

View File

@@ -44,7 +44,7 @@
<activity
android:name=".activity.upload.UploadActivity"
android:launchMode="singleTask" />
<activity android:name=".activity.InstallActivity"
<activity android:name=".activity.install.InstallActivity"
android:enabled="true"
android:launchMode="singleTop"
android:exported="true"/>

View File

@@ -1,68 +0,0 @@
package com.example.mir4updater.activity;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import com.example.mir4updater.R;
import com.example.mir4updater.utils.XapkUtils;
public class InstallActivity extends AppCompatActivity {
private static final String TAG = "InstallActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_install);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.e(TAG, "onNewIntent: ");
Bundle extras = intent.getExtras();
if (XapkUtils.PACKAGE_INSTALLED_ACTION.equals(intent.getAction())) {
int status = -100;
String message = "";
if (extras != null) {
status = extras.getInt(PackageInstaller.EXTRA_STATUS);
message = extras.getString(PackageInstaller.EXTRA_STATUS_MESSAGE);
}
switch (status) {
case PackageInstaller.STATUS_PENDING_USER_ACTION:
// This test app isn't privileged, so the user has to confirm the install.
Intent confirmIntent = (Intent) extras.get(Intent.EXTRA_INTENT);
startActivity(confirmIntent);
break;
case PackageInstaller.STATUS_SUCCESS:
Toast.makeText(this, "安装成功!", Toast.LENGTH_SHORT).show();
finish();
break;
case PackageInstaller.STATUS_FAILURE:
case PackageInstaller.STATUS_FAILURE_ABORTED:
case PackageInstaller.STATUS_FAILURE_BLOCKED:
case PackageInstaller.STATUS_FAILURE_CONFLICT:
case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
case PackageInstaller.STATUS_FAILURE_INVALID:
case PackageInstaller.STATUS_FAILURE_STORAGE:
Toast.makeText(this, "安装失败,请重试",
Toast.LENGTH_SHORT).show();
finish();
Log.d(TAG, "Install failed! " + status + ", " + message);
break;
default:
Toast.makeText(this, "安装失败,解压文件可能已丢失或损坏,请重试",
Toast.LENGTH_SHORT).show();
finish();
Log.d(TAG, "Unrecognized status received from installer: " + status);
}
}
}
}

View File

@@ -0,0 +1,279 @@
package com.example.mir4updater.activity.install;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageInstaller;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.blankj.utilcode.util.RomUtils;
import com.example.mir4updater.R;
import com.example.mir4updater.base.mvvm.BaseMvvmActivity;
import com.example.mir4updater.databinding.ActivityInstallBinding;
import com.example.mir4updater.utils.FileUtils;
import com.hjq.toast.Toaster;
import org.zeroturnaround.zip.ZipUtil;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.ObservableEmitter;
import io.reactivex.rxjava3.core.ObservableOnSubscribe;
import io.reactivex.rxjava3.core.Observer;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class InstallActivity extends BaseMvvmActivity<InstallViewModel,ActivityInstallBinding> {
private static final String TAG = "InstallActivity";
private static final String PACKAGE_INSTALLED_ACTION = "TT_SESSION_API_PACKAGE_INSTALLED";
public static final String KEY_XAPK_PATH = "xapk_path";
public static final String KEY_APK_PATHS = "apk_paths";
private String mXapkPath;
private ExecutorService installXapkExectuor;
private PackageInstaller.Session mSession;
@Override
protected int getLayoutId() {
return R.layout.activity_install;
}
@Override
protected void initDataBinding() {
mViewModel.setCtx(this);
mViewModel.setLifecycle(getLifecycleSubject());
mViewModel.setVDBinding(mViewDataBinding);
mViewDataBinding.setClick(new BtnClick());
}
@Override
protected void initView() {
}
public void initData() {
// apkPaths = getIntent().getStringArrayListExtra(KEY_APK_PATHS);
Intent intent = getIntent();
mXapkPath = intent.getStringExtra(KEY_XAPK_PATH);
Log.e(TAG, "initData: " + mXapkPath);
if (TextUtils.isEmpty(mXapkPath) || !new File(mXapkPath).exists()) {
Toaster.show("文件不存在");
finish();
return;
}
Observable.create(new ObservableOnSubscribe<List<String>>() {
@Override
public void subscribe(ObservableEmitter<List<String>> emitter) throws Exception {
long startTime = System.currentTimeMillis();
File mApkFile = new File(mXapkPath);
String dirName = FileUtils.getFileNoExName(mApkFile.getAbsolutePath());
String unpackPath = getExternalCacheDir() + File.separator + "xapk_unzip" + File.separator + dirName;
File unpackDir = new File(unpackPath);
ZipUtil.unpack(mApkFile, unpackDir);
Log.e(TAG, "subscribe: time = " + (System.currentTimeMillis() - startTime) + "ms");
List<File> files = new ArrayList<>(Arrays.asList(unpackDir.listFiles()));
List<String> paths = files.stream().map(new Function<File, String>() {
@Override
public String apply(File file) {
return file.getAbsolutePath();
}
}).collect(Collectors.toList());
List<String> apkPaths = paths.stream().filter(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.endsWith(".apk");
}
}).collect(Collectors.toList());
emitter.onNext(apkPaths);
emitter.onComplete();
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<String>>() {
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(List<String> fileList) {
Log.e(TAG, "installXAPK: " + fileList);
installXapk(fileList);
}
@Override
public void onError(Throwable e) {
Log.e(TAG, "onError: " + e.getMessage());
}
@Override
public void onComplete() {
Log.e(TAG, "onComplete: ");
}
});
}
private void installXapk(List<String> mApkPaths) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
Toast.makeText(this, "暂时不支持安装,请更新到Android 5.0及以上版本", Toast.LENGTH_SHORT).show();
finish();
}
if (mApkPaths == null || mApkPaths.isEmpty()) {
Toast.makeText(this, "解析apk出错或已取消", Toast.LENGTH_SHORT).show();
finish();
}
if (RomUtils.isMeizu() || RomUtils.isVivo()) {
Toast.makeText(this, "魅族或VIVO系统用户如遇安装被中止或者安装失败的情况请尝试联系手机平台客服或者更换系统内置包安装器再重试",
Toast.LENGTH_SHORT).show();
finish();
}
installXapkExectuor = Executors.newSingleThreadExecutor();
installXapkExectuor.execute(() -> {
try {
mSession = initSession();
for (String apkPath : mApkPaths) {
addApkToInstallSession(apkPath, mSession);
}
commitSession(mSession);
} catch (IOException e) {
e.printStackTrace();
abandonSession();
}
});
}
private PackageInstaller.Session initSession() throws IOException {
PackageInstaller.Session session;
PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
int sessionId;
sessionId = packageInstaller.createSession(params);
session = packageInstaller.openSession(sessionId);
return session;
}
private void addApkToInstallSession(String filePath, PackageInstaller.Session session)
throws IOException {
// It's recommended to pass the file size to openWrite(). Otherwise installation may fail
// if the disk is almost full.
try (OutputStream packageInSession = session.openWrite(FileUtils.getFileName(filePath), 0, new File(filePath).length());
InputStream is = new BufferedInputStream(new FileInputStream(filePath))) {
byte[] buffer = new byte[16384];
int n;
while ((n = is.read(buffer)) >= 0) {
packageInSession.write(buffer, 0, n);
}
}
}
private void commitSession(PackageInstaller.Session session) {
// Create an install status receiver.
Intent intent = new Intent(this, InstallActivity.class);
intent.setAction(PACKAGE_INSTALLED_ACTION);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
IntentSender intentSender = pendingIntent.getIntentSender();
// Commit the session (this will start the installation workflow).
session.commit(intentSender);
}
private void abandonSession() {
if (mSession != null) {
mSession.abandon();
mSession.close();
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Bundle extras = intent.getExtras();
if (PACKAGE_INSTALLED_ACTION.equals(intent.getAction())) {
int status = -100;
String message = "";
if (extras != null) {
status = extras.getInt(PackageInstaller.EXTRA_STATUS);
message = extras.getString(PackageInstaller.EXTRA_STATUS_MESSAGE);
Log.e(TAG, "onNewIntent: status = " + status);
Log.e(TAG, "onNewIntent: message = " + message);
}
switch (status) {
case PackageInstaller.STATUS_PENDING_USER_ACTION:
// This test app isn't privileged, so the user has to confirm the install.
Intent confirmIntent = (Intent) extras.get(Intent.EXTRA_INTENT);
startActivity(confirmIntent);
break;
case PackageInstaller.STATUS_SUCCESS:
Toast.makeText(this, "安装成功!", Toast.LENGTH_SHORT).show();
finish();
break;
case PackageInstaller.STATUS_FAILURE:
case PackageInstaller.STATUS_FAILURE_ABORTED:
case PackageInstaller.STATUS_FAILURE_BLOCKED:
case PackageInstaller.STATUS_FAILURE_CONFLICT:
case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
case PackageInstaller.STATUS_FAILURE_INVALID:
case PackageInstaller.STATUS_FAILURE_STORAGE:
Toast.makeText(this, "安装失败,请重试",
Toast.LENGTH_SHORT).show();
finish();
Log.d(TAG, "Install failed! " + status + ", " + message);
break;
default:
Toast.makeText(this, "安装失败,解压文件可能已丢失或损坏,请重试",
Toast.LENGTH_SHORT).show();
finish();
Log.d(TAG, "Unrecognized status received from installer: " + status);
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (installXapkExectuor != null && !installXapkExectuor.isShutdown()) {
installXapkExectuor.shutdown();
}
abandonSession();
}
public class BtnClick {
}
}

View File

@@ -0,0 +1,19 @@
package com.example.mir4updater.activity.install;
import com.example.mir4updater.base.mvvm.BaseViewModel;
import com.example.mir4updater.databinding.ActivityInstallBinding;
import com.example.mir4updater.databinding.ActivityMainBinding;
import com.trello.rxlifecycle4.android.ActivityEvent;
public class InstallViewModel extends BaseViewModel<ActivityInstallBinding, ActivityEvent> {
@Override
public ActivityInstallBinding getVDBinding() {
return binding;
}
@Override
public void onDestroy() {
}
}

View File

@@ -1,7 +1,6 @@
package com.example.mir4updater.activity.main;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
@@ -16,6 +15,7 @@ import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import com.example.mir4updater.R;
import com.example.mir4updater.activity.install.InstallActivity;
import com.example.mir4updater.activity.update.UpdateActivity;
import com.example.mir4updater.activity.upload.UploadActivity;
import com.example.mir4updater.base.mvvm.BaseMvvmActivity;
@@ -24,17 +24,12 @@ import com.example.mir4updater.config.CommonConfig;
import com.example.mir4updater.databinding.ActivityMainBinding;
import com.example.mir4updater.utils.HashUtils;
import com.example.mir4updater.utils.PermissionUtils;
import com.example.mir4updater.utils.XapkUtils;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import com.hjq.toast.Toaster;
import com.trello.rxlifecycle4.RxLifecycle;
import com.trello.rxlifecycle4.android.ActivityEvent;
import com.wuliang.lib.XapkInstaller;
import com.wuliang.lib.XapkInstallerFactoryKt;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.File;
@@ -567,9 +562,13 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
return;
}
XapkUtils xapkUtils =new XapkUtils(MainActivity.this);
xapkUtils.setApkFilePath(file.getAbsolutePath());
xapkUtils.installXapk();
Intent intent = new Intent(MainActivity.this, InstallActivity.class);
intent.putExtra(InstallActivity.KEY_XAPK_PATH, file.getAbsolutePath());
startActivity(intent);
// XapkUtils xapkUtils =new XapkUtils(MainActivity.this);
// xapkUtils.setApkFilePath(file.getAbsolutePath());
// xapkUtils.installXapk();
// TODO: 2025/4/21 google chrome 需要google框架
//Install failed! 7, INSTALL_FAILED_MISSING_SHARED_LIBRARY: Reconciliation failed...: Reconcile failed:

View File

@@ -1,12 +1,25 @@
package com.example.mir4updater.utils;
import android.text.TextUtils;
import java.io.File;
public class FileUtils {
public static String getFileNoExName(String filePath) {
String fileNameEx = filePath.substring(filePath.lastIndexOf(File.separator) + 1);
String fileName = fileNameEx.substring(0,fileNameEx.lastIndexOf("."));
String fileName = fileNameEx.substring(0, fileNameEx.lastIndexOf("."));
return fileName;
}
public static String getFileName(String filePath) {
if (TextUtils.isEmpty(filePath)){
return "";
}
if (!filePath.contains(File.separator)){
return filePath;
}
String fileNameEx = filePath.substring(filePath.lastIndexOf(File.separator) + 1);
return fileNameEx;
}
}

View File

@@ -11,7 +11,6 @@ import android.util.Log;
import androidx.annotation.RequiresApi;
import com.example.mir4updater.activity.InstallActivity;
import com.example.mir4updater.bean.Expansions;
import com.example.mir4updater.bean.SplitApks;
import com.example.mir4updater.receiver.InstallResultReceiver;
@@ -312,14 +311,14 @@ public class XapkUtils {
try {
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
// Intent intent = new Intent(context, InstallResultReceiver.class);
// PendingIntent pendingIntent = PendingIntent.getBroadcast(
// context, sessionId, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
// );
Intent intent = new Intent(context, InstallResultReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context, sessionId, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
);
Intent intent = new Intent(mContext, InstallActivity.class);
intent.setAction(PACKAGE_INSTALLED_ACTION);
PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
// Intent intent = new Intent(mContext, InstallActivity.class);
// intent.setAction(PACKAGE_INSTALLED_ACTION);
// PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
session.commit(pendingIntent.getIntentSender());
} catch (IOException e) {
@@ -346,7 +345,8 @@ public class XapkUtils {
PackageInstaller.Session session = null;
try {
session = pi.openSession(sessionId);
OutputStream out = session.openWrite(FileUtils.getFileNoExName(apkFilePath), 0, apkFile.length());
Log.e(TAG, "copyApkFile: " + FileUtils.getFileName(apkFilePath));
OutputStream out = session.openWrite(FileUtils.getFileName(apkFilePath), 0, apkFile.length());
FileInputStream input = new FileInputStream(apkFile);
int read = 0;
byte[] buffer = new byte[65536];

View File

@@ -1,9 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.InstallActivity">
tools:context=".activity.nstall.InstallActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
<data>
<variable
name="click"
type="com.example.mir4updater.activity.install.InstallActivity.BtnClick" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.372" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:maxLines="1"
android:singleLine="true"
android:text="正在安装"
android:textColor="@color/black"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_icon" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>