Directly stream APK into install session

This commit is contained in:
topjohnwu
2022-02-03 03:22:55 -08:00
parent 79620c97d1
commit 10f991b8d0
6 changed files with 67 additions and 44 deletions

View File

@ -18,9 +18,11 @@ import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -28,28 +30,50 @@ import io.michaelrocks.paranoid.Obfuscate;
@Obfuscate
public final class APKInstall {
private static final String ACTION_SESSION_UPDATE = "ACTION_SESSION_UPDATE";
// @WorkerThread
public static void installapk(Context context, File apk) {
public static void install(Context context, File apk) {
try (var src = new FileInputStream(apk);
var out = openStream(context, true)) {
if (out != null)
transfer(src, out);
} catch (IOException e) {
Log.e(APKInstall.class.getSimpleName(), "", e);
}
}
public static OutputStream openStream(Context context, boolean silent) {
//noinspection InlinedApi
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
var action = APKInstall.class.getName();
var intent = new Intent(action).setPackage(context.getPackageName());
var intent = new Intent(ACTION_SESSION_UPDATE).setPackage(context.getPackageName());
var pending = PendingIntent.getBroadcast(context, 0, intent, flag);
var installer = context.getPackageManager().getPackageInstaller();
var params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (silent && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
}
try (Session session = installer.openSession(installer.createSession(params))) {
OutputStream out = session.openWrite(apk.getName(), 0, apk.length());
try (var in = new FileInputStream(apk); out) {
transfer(in, out);
}
session.commit(pending.getIntentSender());
try {
Session session = installer.openSession(installer.createSession(params));
var out = session.openWrite(UUID.randomUUID().toString(), 0, -1);
return new FilterOutputStream(out) {
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
}
@Override
public void close() throws IOException {
super.close();
session.commit(pending.getIntentSender());
session.close();
}
};
} catch (IOException e) {
Log.e(APKInstall.class.getSimpleName(), "", e);
}
return null;
}
public static void transfer(InputStream in, OutputStream out) throws IOException {
@ -62,46 +86,47 @@ public final class APKInstall {
}
public static InstallReceiver register(Context context, String packageName, Runnable onSuccess) {
var receiver = new InstallReceiver(context, packageName, onSuccess);
var receiver = new InstallReceiver(packageName, onSuccess);
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package");
context.registerReceiver(receiver, filter);
context.registerReceiver(receiver, new IntentFilter(APKInstall.class.getName()));
context.registerReceiver(receiver, new IntentFilter(ACTION_SESSION_UPDATE));
return receiver;
}
public static class InstallReceiver extends BroadcastReceiver {
private final Context context;
private final String packageName;
private final Runnable onSuccess;
private final CountDownLatch latch = new CountDownLatch(1);
private Intent intent = null;
private Intent userAction = null;
private InstallReceiver(Context context, String packageName, Runnable onSuccess) {
this.context = context;
private InstallReceiver(String packageName, Runnable onSuccess) {
this.packageName = packageName;
this.onSuccess = onSuccess;
}
@Override
public void onReceive(Context c, Intent i) {
if (Intent.ACTION_PACKAGE_ADDED.equals(i.getAction())) {
Uri data = i.getData();
if (data == null || onSuccess == null) return;
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
Uri data = intent.getData();
if (data == null)
return;
String pkg = data.getSchemeSpecificPart();
if (pkg.equals(packageName)) {
onSuccess.run();
if (onSuccess != null)
onSuccess.run();
context.unregisterReceiver(this);
}
return;
}
int status = i.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
switch (status) {
case STATUS_PENDING_USER_ACTION:
intent = i.getParcelableExtra(Intent.EXTRA_INTENT);
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
break;
case STATUS_SUCCESS:
if (onSuccess != null) onSuccess.run();
if (onSuccess != null)
onSuccess.run();
default:
context.unregisterReceiver(this);
}
@ -113,9 +138,8 @@ public final class APKInstall {
try {
//noinspection ResultOfMethodCallIgnored
latch.await(5, TimeUnit.SECONDS);
} catch (Exception ignored) {
}
return intent;
} catch (Exception ignored) {}
return userAction;
}
}
}