|
@@ -6,6 +6,9 @@ import android.content.ComponentName;
|
|
|
import android.content.Context;
|
|
import android.content.Context;
|
|
|
import android.content.Intent;
|
|
import android.content.Intent;
|
|
|
import android.content.pm.PackageInstaller;
|
|
import android.content.pm.PackageInstaller;
|
|
|
|
|
+import android.database.Cursor;
|
|
|
|
|
+import android.net.Uri;
|
|
|
|
|
+import android.app.KeyguardManager;
|
|
|
import android.os.Build;
|
|
import android.os.Build;
|
|
|
import android.os.Bundle;
|
|
import android.os.Bundle;
|
|
|
import android.util.Log;
|
|
import android.util.Log;
|
|
@@ -25,10 +28,10 @@ import com.ble.mylockview.admin.KioskDeviceAdminReceiver;
|
|
|
import com.ble.mylockview.admin.KioskManager;
|
|
import com.ble.mylockview.admin.KioskManager;
|
|
|
|
|
|
|
|
import java.io.File;
|
|
import java.io.File;
|
|
|
-import java.io.FileInputStream;
|
|
|
|
|
import java.io.IOException;
|
|
import java.io.IOException;
|
|
|
import java.io.InputStream;
|
|
import java.io.InputStream;
|
|
|
import java.io.OutputStream;
|
|
import java.io.OutputStream;
|
|
|
|
|
+import java.util.Locale;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* UpdateHelper 主 Activity
|
|
* UpdateHelper 主 Activity
|
|
@@ -50,6 +53,7 @@ public class MainActivity extends AppCompatActivity {
|
|
|
private Button backToMainButton;
|
|
private Button backToMainButton;
|
|
|
|
|
|
|
|
private String apkPath;
|
|
private String apkPath;
|
|
|
|
|
+ private Uri apkUri;
|
|
|
private String mainPackage;
|
|
private String mainPackage;
|
|
|
private PackageInstaller.Session currentSession;
|
|
private PackageInstaller.Session currentSession;
|
|
|
private int currentSessionId = -1;
|
|
private int currentSessionId = -1;
|
|
@@ -61,6 +65,29 @@ public class MainActivity extends AppCompatActivity {
|
|
|
@Override
|
|
@Override
|
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
|
super.onCreate(savedInstanceState);
|
|
super.onCreate(savedInstanceState);
|
|
|
|
|
+
|
|
|
|
|
+ // 锁屏之上显示 + 点亮屏幕(让安装界面直接覆盖在锁屏层上)
|
|
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
|
|
|
+ setShowWhenLocked(true);
|
|
|
|
|
+ setTurnScreenOn(true);
|
|
|
|
|
+
|
|
|
|
|
+ KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
|
|
|
|
+ if (km != null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ km.requestDismissKeyguard(this, null);
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ // 部分设备/策略下可能不允许消除锁屏
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ getWindow().addFlags(
|
|
|
|
|
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
|
|
|
|
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
|
|
|
|
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
|
|
|
|
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 全屏显示
|
|
// 全屏显示
|
|
|
//requestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
//requestWindowFeature(Window.FEATURE_NO_TITLE);
|
|
@@ -83,7 +110,10 @@ public class MainActivity extends AppCompatActivity {
|
|
|
|
|
|
|
|
// 获取传递的参数
|
|
// 获取传递的参数
|
|
|
Intent intent = getIntent();
|
|
Intent intent = getIntent();
|
|
|
- // 支持两种方式传递参数:直接 extra 或通过 Intent data
|
|
|
|
|
|
|
+ // 支持两种方式传递参数:
|
|
|
|
|
+ // 1) Android 11+ 推荐:Intent data 传入 content:// Uri(带临时读权限)
|
|
|
|
|
+ // 2) 兼容旧逻辑:extra 传入 apk_path(文件路径字符串)
|
|
|
|
|
+ apkUri = intent.getData();
|
|
|
apkPath = intent.getStringExtra(EXTRA_APK_PATH);
|
|
apkPath = intent.getStringExtra(EXTRA_APK_PATH);
|
|
|
if (apkPath == null || apkPath.isEmpty()) {
|
|
if (apkPath == null || apkPath.isEmpty()) {
|
|
|
apkPath = intent.getStringExtra("apk_path");
|
|
apkPath = intent.getStringExtra("apk_path");
|
|
@@ -94,9 +124,9 @@ public class MainActivity extends AppCompatActivity {
|
|
|
mainPackage = intent.getStringExtra("main_package");
|
|
mainPackage = intent.getStringExtra("main_package");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (apkPath == null || apkPath.isEmpty()) {
|
|
|
|
|
|
|
+ if ((apkUri == null) && (apkPath == null || apkPath.isEmpty())) {
|
|
|
// Log.e(TAG, "APK 路径为空");
|
|
// Log.e(TAG, "APK 路径为空");
|
|
|
- showError("APK 路径为空");
|
|
|
|
|
|
|
+ showError("APK Uri / 路径为空");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -330,7 +360,8 @@ public class MainActivity extends AppCompatActivity {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
// 设置安装标志
|
|
// 设置安装标志
|
|
|
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
|
|
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
|
|
|
+ // 只有 Android 12 及以上才支持此方法
|
|
|
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED);
|
|
params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -340,21 +371,12 @@ public class MainActivity extends AppCompatActivity {
|
|
|
|
|
|
|
|
currentSession = installer.openSession(currentSessionId);
|
|
currentSession = installer.openSession(currentSessionId);
|
|
|
|
|
|
|
|
- InputStream in = null;
|
|
|
|
|
- OutputStream out = null;
|
|
|
|
|
-
|
|
|
|
|
try {
|
|
try {
|
|
|
- // 打开 APK 文件
|
|
|
|
|
- File apkFile = new File(apkPath);
|
|
|
|
|
- if (!apkFile.exists()) {
|
|
|
|
|
- throw new IOException("APK 文件不存在: " + apkPath);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- long apkSize = apkFile.length();
|
|
|
|
|
- // Log.d(TAG, "APK 文件大小: " + apkSize + " bytes (" + (apkSize / 1024 / 1024) + " MB)");
|
|
|
|
|
|
|
+ final long apkSize = resolveApkSizeBytes();
|
|
|
|
|
+ final boolean sizeKnown = apkSize > 0;
|
|
|
|
|
|
|
|
// 在写入大文件前,建议系统进行垃圾回收(不保证立即执行)
|
|
// 在写入大文件前,建议系统进行垃圾回收(不保证立即执行)
|
|
|
- if (apkSize > 100 * 1024 * 1024) { // 大于 100MB
|
|
|
|
|
|
|
+ if (sizeKnown && apkSize > 100 * 1024 * 1024) { // 大于 100MB
|
|
|
System.gc(); // 建议垃圾回收
|
|
System.gc(); // 建议垃圾回收
|
|
|
try {
|
|
try {
|
|
|
Thread.sleep(100); // 给 GC 一点时间
|
|
Thread.sleep(100); // 给 GC 一点时间
|
|
@@ -365,54 +387,41 @@ public class MainActivity extends AppCompatActivity {
|
|
|
|
|
|
|
|
runOnUiThread(() -> updateStatus("正在安装..."));
|
|
runOnUiThread(() -> updateStatus("正在安装..."));
|
|
|
|
|
|
|
|
- // 打开输入流和输出流
|
|
|
|
|
- in = new FileInputStream(apkFile);
|
|
|
|
|
- out = currentSession.openWrite("package", 0, apkSize);
|
|
|
|
|
-
|
|
|
|
|
- // 使用较小的缓冲区(32KB)以减少内存占用,同时保持合理的性能
|
|
|
|
|
- // 对于大文件,较小的缓冲区可以降低峰值内存使用
|
|
|
|
|
- byte[] buffer = new byte[32768]; // 32KB 缓冲区(从 64KB 减小到 32KB)
|
|
|
|
|
- long totalWritten = 0;
|
|
|
|
|
- int c;
|
|
|
|
|
- int lastPercent = -1;
|
|
|
|
|
-
|
|
|
|
|
- while ((c = in.read(buffer)) != -1) {
|
|
|
|
|
- out.write(buffer, 0, c);
|
|
|
|
|
- totalWritten += c;
|
|
|
|
|
-
|
|
|
|
|
- if (apkSize > 0) {
|
|
|
|
|
- int percent = (int) (totalWritten * 100 / apkSize);
|
|
|
|
|
- if (percent != lastPercent) {
|
|
|
|
|
- lastPercent = percent;
|
|
|
|
|
- // Log.d(TAG, "安装进度: " + percent + "%");
|
|
|
|
|
-
|
|
|
|
|
- final int finalPercent = percent;
|
|
|
|
|
- runOnUiThread(() -> {
|
|
|
|
|
- updateProgress(finalPercent);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // 打开输入流(Android 11+ 优先使用 content:// Uri;否则兼容文件路径)
|
|
|
|
|
+ try (InputStream in = openApkInputStream();
|
|
|
|
|
+ OutputStream out = currentSession.openWrite("package", 0, sizeKnown ? apkSize : -1)) {
|
|
|
|
|
+
|
|
|
|
|
+ // 使用较小的缓冲区(32KB)以减少内存占用,同时保持合理的性能
|
|
|
|
|
+ byte[] buffer = new byte[32768];
|
|
|
|
|
+ long totalWritten = 0;
|
|
|
|
|
+ int c;
|
|
|
|
|
+ int lastPercent = -1;
|
|
|
|
|
+
|
|
|
|
|
+ while ((c = in.read(buffer)) != -1) {
|
|
|
|
|
+ out.write(buffer, 0, c);
|
|
|
|
|
+ totalWritten += c;
|
|
|
|
|
+
|
|
|
|
|
+ if (sizeKnown) {
|
|
|
|
|
+ int percent = (int) (totalWritten * 100 / apkSize);
|
|
|
|
|
+ if (percent != lastPercent) {
|
|
|
|
|
+ lastPercent = percent;
|
|
|
|
|
+ final int finalPercent = percent;
|
|
|
|
|
+ runOnUiThread(() -> updateProgress(finalPercent));
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // size 未知时,简单做“写入中”的状态刷新,避免 UI 长时间无变化
|
|
|
|
|
+ if ((totalWritten % (5L * 1024 * 1024)) < buffer.length) { // 每约 5MB 更新一次
|
|
|
|
|
+ final long writtenBytes = totalWritten;
|
|
|
|
|
+ runOnUiThread(() -> updateStatus(String.format(
|
|
|
|
|
+ Locale.getDefault(),
|
|
|
|
|
+ "正在写入安装包...(已写入 %.1fMB)",
|
|
|
|
|
+ writtenBytes / 1024f / 1024f
|
|
|
|
|
+ )));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- // 确保数据写入磁盘
|
|
|
|
|
- currentSession.fsync(out);
|
|
|
|
|
-
|
|
|
|
|
- // 关闭流
|
|
|
|
|
- if (out != null) {
|
|
|
|
|
- try {
|
|
|
|
|
- out.close();
|
|
|
|
|
- } catch (IOException e) {
|
|
|
|
|
- // Log.w(TAG, "关闭输出流失败", e);
|
|
|
|
|
- }
|
|
|
|
|
- out = null;
|
|
|
|
|
- }
|
|
|
|
|
- if (in != null) {
|
|
|
|
|
- try {
|
|
|
|
|
- in.close();
|
|
|
|
|
- } catch (IOException e) {
|
|
|
|
|
- // Log.w(TAG, "关闭输入流失败", e);
|
|
|
|
|
- }
|
|
|
|
|
- in = null;
|
|
|
|
|
|
|
+ currentSession.fsync(out);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 再次建议 GC,释放文件流占用的内存
|
|
// 再次建议 GC,释放文件流占用的内存
|
|
@@ -458,17 +467,11 @@ public class MainActivity extends AppCompatActivity {
|
|
|
|
|
|
|
|
} catch (OutOfMemoryError e) {
|
|
} catch (OutOfMemoryError e) {
|
|
|
// 处理内存不足错误
|
|
// 处理内存不足错误
|
|
|
- String errorMsg = "内存不足,无法安装大文件。APK 大小: " +
|
|
|
|
|
- (new File(apkPath).length() / 1024 / 1024) + " MB";
|
|
|
|
|
|
|
+ String errorMsg = "内存不足,无法安装大文件。";
|
|
|
// Log.e(TAG, errorMsg, e);
|
|
// Log.e(TAG, errorMsg, e);
|
|
|
|
|
|
|
|
- // 清理资源
|
|
|
|
|
- cleanupResources(in, out);
|
|
|
|
|
throw new IOException(errorMsg, e);
|
|
throw new IOException(errorMsg, e);
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- // 确保流被关闭
|
|
|
|
|
- cleanupResources(in, out);
|
|
|
|
|
-
|
|
|
|
|
// 如果会话存在且未提交,则放弃会话
|
|
// 如果会话存在且未提交,则放弃会话
|
|
|
if (currentSession != null) {
|
|
if (currentSession != null) {
|
|
|
try {
|
|
try {
|
|
@@ -486,23 +489,58 @@ public class MainActivity extends AppCompatActivity {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 清理文件流资源
|
|
|
|
|
|
|
+ * 打开 APK 输入流(优先 Uri,其次文件路径)
|
|
|
*/
|
|
*/
|
|
|
- private void cleanupResources(InputStream in, OutputStream out) {
|
|
|
|
|
- if (out != null) {
|
|
|
|
|
- try {
|
|
|
|
|
- out.close();
|
|
|
|
|
- } catch (IOException ignored) {
|
|
|
|
|
- // 忽略关闭异常
|
|
|
|
|
|
|
+ private InputStream openApkInputStream() throws IOException {
|
|
|
|
|
+ if (apkUri != null) {
|
|
|
|
|
+ InputStream in = getContentResolver().openInputStream(apkUri);
|
|
|
|
|
+ if (in == null) {
|
|
|
|
|
+ throw new IOException("无法打开 APK Uri 输入流: " + apkUri);
|
|
|
}
|
|
}
|
|
|
|
|
+ return in;
|
|
|
}
|
|
}
|
|
|
- if (in != null) {
|
|
|
|
|
- try {
|
|
|
|
|
- in.close();
|
|
|
|
|
- } catch (IOException ignored) {
|
|
|
|
|
- // 忽略关闭异常
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (apkPath == null || apkPath.isEmpty()) {
|
|
|
|
|
+ throw new IOException("APK 路径为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ File apkFile = new File(apkPath);
|
|
|
|
|
+ if (!apkFile.exists()) {
|
|
|
|
|
+ throw new IOException("APK 文件不存在: " + apkPath);
|
|
|
|
|
+ }
|
|
|
|
|
+ return new java.io.FileInputStream(apkFile);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 尝试解析 APK 大小(用于 openWrite length & 进度条)
|
|
|
|
|
+ * 可能返回 -1(未知)。
|
|
|
|
|
+ */
|
|
|
|
|
+ private long resolveApkSizeBytes() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (apkUri != null) {
|
|
|
|
|
+ Cursor cursor = getContentResolver().query(apkUri, null, null, null, null);
|
|
|
|
|
+ if (cursor != null) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ int sizeIndex = cursor.getColumnIndex(android.provider.OpenableColumns.SIZE);
|
|
|
|
|
+ if (sizeIndex >= 0 && cursor.moveToFirst()) {
|
|
|
|
|
+ long size = cursor.getLong(sizeIndex);
|
|
|
|
|
+ return size > 0 ? size : -1;
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ cursor.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return -1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (apkPath != null && !apkPath.isEmpty()) {
|
|
|
|
|
+ File apkFile = new File(apkPath);
|
|
|
|
|
+ return apkFile.exists() ? apkFile.length() : -1;
|
|
|
}
|
|
}
|
|
|
|
|
+ } catch (Exception ignored) {
|
|
|
|
|
+ // 忽略异常,返回未知大小
|
|
|
}
|
|
}
|
|
|
|
|
+ return -1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -546,15 +584,16 @@ public class MainActivity extends AppCompatActivity {
|
|
|
|
|
|
|
|
// Log.d(TAG, "onNewIntent 被调用");
|
|
// Log.d(TAG, "onNewIntent 被调用");
|
|
|
|
|
|
|
|
- // 检查是否是新的安装请求(有 apk_path)
|
|
|
|
|
|
|
+ // 检查是否是新的安装请求(优先 Intent data 的 Uri;否则 apk_path)
|
|
|
|
|
+ Uri newApkUri = intent.getData();
|
|
|
String newApkPath = intent.getStringExtra(EXTRA_APK_PATH);
|
|
String newApkPath = intent.getStringExtra(EXTRA_APK_PATH);
|
|
|
if (newApkPath == null || newApkPath.isEmpty()) {
|
|
if (newApkPath == null || newApkPath.isEmpty()) {
|
|
|
newApkPath = intent.getStringExtra("apk_path");
|
|
newApkPath = intent.getStringExtra("apk_path");
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- if (newApkPath != null && !newApkPath.isEmpty()) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (newApkUri != null || (newApkPath != null && !newApkPath.isEmpty())) {
|
|
|
// 这是新的安装请求
|
|
// 这是新的安装请求
|
|
|
- // Log.d(TAG, "检测到新的安装请求: " + newApkPath);
|
|
|
|
|
|
|
+ // Log.d(TAG, "检测到新的安装请求: " + (newApkUri != null ? newApkUri : newApkPath));
|
|
|
|
|
|
|
|
if (isInstalling) {
|
|
if (isInstalling) {
|
|
|
// 正在安装中,提示用户
|
|
// 正在安装中,提示用户
|
|
@@ -574,6 +613,7 @@ public class MainActivity extends AppCompatActivity {
|
|
|
// Log.d(TAG, "开始新的安装任务");
|
|
// Log.d(TAG, "开始新的安装任务");
|
|
|
|
|
|
|
|
// 更新参数
|
|
// 更新参数
|
|
|
|
|
+ apkUri = newApkUri;
|
|
|
apkPath = newApkPath;
|
|
apkPath = newApkPath;
|
|
|
String newMainPackage = intent.getStringExtra(EXTRA_MAIN_PACKAGE);
|
|
String newMainPackage = intent.getStringExtra(EXTRA_MAIN_PACKAGE);
|
|
|
if (newMainPackage == null || newMainPackage.isEmpty()) {
|
|
if (newMainPackage == null || newMainPackage.isEmpty()) {
|