Refactor APKInstall

This commit is contained in:
topjohnwu 2022-02-13 19:54:59 -08:00
parent 256ff31d11
commit 668e549208
5 changed files with 122 additions and 98 deletions

View File

@ -10,11 +10,9 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.PackageInstaller.Session;
import android.content.pm.PackageInstaller.SessionParams; import android.content.pm.PackageInstaller.SessionParams;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.util.Log;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -31,51 +29,6 @@ import io.michaelrocks.paranoid.Obfuscate;
@Obfuscate @Obfuscate
public final class APKInstall { public final class APKInstall {
private static final String ACTION_SESSION_UPDATE = "ACTION_SESSION_UPDATE";
// @WorkerThread
public static void install(Context context, File apk) {
try (var src = new FileInputStream(apk);
var out = openStream(context)) {
if (out != null)
transfer(src, out);
} catch (IOException e) {
Log.e(APKInstall.class.getSimpleName(), "", e);
}
}
public static OutputStream openStream(Context context) {
//noinspection InlinedApi
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
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) {
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
}
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 { public static void transfer(InputStream in, OutputStream out) throws IOException {
int size = 8192; int size = 8192;
var buffer = new byte[size]; var buffer = new byte[size];
@ -85,21 +38,37 @@ public final class APKInstall {
} }
} }
public static InstallReceiver register(Context context, String packageName, Runnable onSuccess) { public static Session startSession(Context context) {
var receiver = new InstallReceiver(packageName, onSuccess); return startSession(context, null, null);
}
public static Session startSession(Context context, String pkg, Runnable onSuccess) {
var receiver = new InstallReceiver(pkg, onSuccess);
context = context.getApplicationContext();
if (pkg != null) {
// If pkg is not null, look for package added event
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package"); filter.addDataScheme("package");
context.registerReceiver(receiver, filter); context.registerReceiver(receiver, filter);
context.registerReceiver(receiver, new IntentFilter(ACTION_SESSION_UPDATE)); }
context.registerReceiver(receiver, new IntentFilter(receiver.sessionId));
return receiver; return receiver;
} }
public static class InstallReceiver extends BroadcastReceiver { public interface Session {
OutputStream openStream(Context context) throws IOException;
void install(Context context, File apk) throws IOException;
Intent waitIntent();
}
private static class InstallReceiver extends BroadcastReceiver implements Session {
private final String packageName; private final String packageName;
private final Runnable onSuccess; private final Runnable onSuccess;
private final CountDownLatch latch = new CountDownLatch(1); private final CountDownLatch latch = new CountDownLatch(1);
private Intent userAction = null; private Intent userAction = null;
final String sessionId = UUID.randomUUID().toString();
private InstallReceiver(String packageName, Runnable onSuccess) { private InstallReceiver(String packageName, Runnable onSuccess) {
this.packageName = packageName; this.packageName = packageName;
this.onSuccess = onSuccess; this.onSuccess = onSuccess;
@ -113,27 +82,32 @@ public final class APKInstall {
return; return;
String pkg = data.getSchemeSpecificPart(); String pkg = data.getSchemeSpecificPart();
if (pkg.equals(packageName)) { if (pkg.equals(packageName)) {
if (onSuccess != null) onSuccess(context);
onSuccess.run();
context.unregisterReceiver(this);
}
return;
} }
} else if (sessionId.equals(intent.getAction())) {
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID); int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
switch (status) { switch (status) {
case STATUS_PENDING_USER_ACTION: case STATUS_PENDING_USER_ACTION:
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT); userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
break; break;
case STATUS_SUCCESS: case STATUS_SUCCESS:
if (onSuccess != null) if (packageName == null) {
onSuccess.run(); onSuccess(context);
default: }
context.unregisterReceiver(this); break;
} }
latch.countDown(); latch.countDown();
} }
}
private void onSuccess(Context context) {
if (onSuccess != null)
onSuccess.run();
context.getApplicationContext().unregisterReceiver(this);
}
// @WorkerThread @Nullable // @WorkerThread @Nullable
@Override
public Intent waitIntent() { public Intent waitIntent() {
try { try {
//noinspection ResultOfMethodCallIgnored //noinspection ResultOfMethodCallIgnored
@ -141,5 +115,41 @@ public final class APKInstall {
} catch (Exception ignored) {} } catch (Exception ignored) {}
return userAction; return userAction;
} }
@Override
public OutputStream openStream(Context context) throws IOException {
// noinspection InlinedApi
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
var intent = new Intent(sessionId).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) {
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
}
var session = installer.openSession(installer.createSession(params));
var out = session.openWrite(sessionId, 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();
}
};
}
@Override
public void install(Context context, File apk) throws IOException {
try (var src = new FileInputStream(apk);
var out = openStream(context)) {
transfer(src, out);
}
}
} }
} }

View File

@ -72,9 +72,9 @@ class DownloadService : NotificationService() {
} }
} }
private fun openApkSession(): OutputStream { private fun APKInstall.Session.open(): OutputStream {
Config.showUpdateDone = true Config.showUpdateDone = true
return APKInstall.openStream(this) return openStream(this@DownloadService)
} }
private suspend fun handleApp(stream: InputStream, subject: Subject.App) { private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
@ -110,9 +110,9 @@ class DownloadService : NotificationService() {
apk.delete() apk.delete()
// Install // Install
val receiver = APKInstall.register(this, null, null) val session = APKInstall.startSession(this)
patched.inputStream().copyAndClose(openApkSession()) patched.inputStream().copyAndClose(session.open())
subject.intent = receiver.waitIntent() subject.intent = session.waitIntent()
patched.delete() patched.delete()
} else { } else {
@ -131,9 +131,9 @@ class DownloadService : NotificationService() {
throw e throw e
} }
} else { } else {
val receiver = APKInstall.register(this, null, null) val session = APKInstall.startSession(this)
writeTee(openApkSession()) writeTee(session.open())
subject.intent = receiver.waitIntent() subject.intent = session.waitIntent()
} }
} }

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.core.tasks package com.topjohnwu.magisk.core.tasks
import android.app.Activity import android.app.Activity
import android.app.ProgressDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
@ -20,8 +21,7 @@ import com.topjohnwu.magisk.signing.SignApk
import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
@ -108,7 +108,8 @@ object HideAPK {
Timber.e(e) Timber.e(e)
stub.createNewFile() stub.createNewFile()
val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}" val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}"
if (!Shell.su(cmd).exec().isSuccess) return false if (!Shell.su(cmd).exec().isSuccess)
return false
} }
// Generate a new random package name and signature // Generate a new random package name and signature
@ -120,20 +121,22 @@ object HideAPK {
return false return false
// Install and auto launch app // Install and auto launch app
val receiver = APKInstall.register(activity, pkg) { val session = APKInstall.startSession(activity, pkg) {
launchApp(activity, pkg) launchApp(activity, pkg)
} }
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}" try {
if (!Shell.su(cmd).exec().isSuccess) { session.install(activity, repack)
APKInstall.install(activity, repack) } catch (e: IOException) {
receiver.waitIntent()?.let { activity.startActivity(it) } Timber.e(e)
return false
} }
session.waitIntent()?.let { activity.startActivity(it) }
return true return true
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
suspend fun hide(activity: Activity, label: String) { suspend fun hide(activity: Activity, label: String) {
val dialog = android.app.ProgressDialog(activity).apply { val dialog = ProgressDialog(activity).apply {
setTitle(activity.getString(R.string.hide_app_title)) setTitle(activity.getString(R.string.hide_app_title))
isIndeterminate = true isIndeterminate = true
setCancelable(false) setCancelable(false)
@ -148,24 +151,28 @@ object HideAPK {
} }
} }
@DelicateCoroutinesApi
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
fun restore(activity: Activity) { fun restore(activity: Activity) {
val dialog = android.app.ProgressDialog(activity).apply { val dialog = ProgressDialog(activity).apply {
setTitle(activity.getString(R.string.restore_img_msg)) setTitle(activity.getString(R.string.restore_img_msg))
isIndeterminate = true isIndeterminate = true
setCancelable(false) setCancelable(false)
show() show()
} }
val apk = StubApk.current(activity) val apk = StubApk.current(activity)
val receiver = APKInstall.register(activity, APPLICATION_ID) { val session = APKInstall.startSession(activity, APPLICATION_ID) {
launchApp(activity, APPLICATION_ID) launchApp(activity, APPLICATION_ID)
dialog.dismiss() dialog.dismiss()
} }
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}" GlobalScope.launch(Dispatchers.IO) {
Shell.su(cmd).submit(Shell.EXECUTOR) { ret -> try {
if (ret.isSuccess) return@submit session.install(activity, apk)
APKInstall.install(activity, apk) } catch (e: IOException) {
receiver.waitIntent()?.let { activity.startActivity(it) } Timber.e(e)
return@launch
}
session.waitIntent()?.let { activity.startActivity(it) }
} }
} }
} }

View File

@ -99,6 +99,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
| <intent-filter> | <intent-filter>
| <action android:name="android.intent.action.LOCALE_CHANGED" /> | <action android:name="android.intent.action.LOCALE_CHANGED" />
| <action android:name="android.intent.action.UID_REMOVED" /> | <action android:name="android.intent.action.UID_REMOVED" />
| <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
| </intent-filter> | </intent-filter>
| <intent-filter> | <intent-filter>
| <action android:name="android.intent.action.PACKAGE_REPLACED" /> | <action android:name="android.intent.action.PACKAGE_REPLACED" />

View File

@ -26,6 +26,7 @@ import org.json.JSONException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.CipherInputStream; import javax.crypto.CipherInputStream;
@ -126,10 +127,15 @@ public class DownloadActivity extends Activity {
if (dynLoad) { if (dynLoad) {
runOnUiThread(onSuccess); runOnUiThread(onSuccess);
} else { } else {
var receiver = APKInstall.register(this, BuildConfig.APPLICATION_ID, onSuccess); var session = APKInstall.startSession(this);
APKInstall.install(this, file); try {
Intent intent = receiver.waitIntent(); session.install(this, file);
if (intent != null) startActivity(intent); Intent intent = session.waitIntent();
if (intent != null)
startActivity(intent);
} catch (IOException e) {
e.printStackTrace();
}
} }
}); });
} }