Unverified Commit baa19f0c authored by 南宫雪珊's avatar 南宫雪珊 Committed by GitHub

Rewrite app installation

Fix  #4960
parent 5a49bd3a
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" /> <uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" android:maxSdkVersion="29"
......
package com.topjohnwu.magisk.utils; package com.topjohnwu.magisk.utils;
import android.app.Activity; import static android.content.pm.PackageInstaller.EXTRA_STATUS;
import static android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID;
import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION;
import static android.content.pm.PackageInstaller.STATUS_SUCCESS;
import android.app.PendingIntent;
import android.content.BroadcastReceiver; 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.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.util.Log;
import com.topjohnwu.magisk.FileProvider;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import io.michaelrocks.paranoid.Obfuscate; import io.michaelrocks.paranoid.Obfuscate;
@Obfuscate @Obfuscate
public class APKInstall { public final class APKInstall {
// @WorkerThread
public static Intent installIntent(Context c, File apk) { public static void installapk(Context context, File apk) {
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); //noinspection InlinedApi
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); var action = APKInstall.class.getName();
if (Build.VERSION.SDK_INT >= 24) { var intent = new Intent(action).setPackage(context.getPackageName());
intent.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk)); var pending = PendingIntent.getBroadcast(context, 0, intent, flag);
} else {
//noinspection ResultOfMethodCallIgnored SetWorldReadable var installer = context.getPackageManager().getPackageInstaller();
apk.setReadable(true, false); var params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
intent.setData(Uri.fromFile(apk)); if (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());
} catch (IOException e) {
Log.e(APKInstall.class.getSimpleName(), "", e);
} }
return intent;
} }
public static void install(Context c, File apk) { public static void transfer(InputStream in, OutputStream out) throws IOException {
c.startActivity(installIntent(c, apk)); int size = 8192;
var buffer = new byte[size];
int read;
while ((read = in.read(buffer, 0, size)) >= 0) {
out.write(buffer, 0, read);
}
} }
public static void registerInstallReceiver(Context c, BroadcastReceiver r) { public static InstallReceiver register(Context context, String packageName, Runnable onSuccess) {
IntentFilter filter = new IntentFilter(); var receiver = new InstallReceiver(context, packageName, onSuccess);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED); var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package"); filter.addDataScheme("package");
c.getApplicationContext().registerReceiver(r, filter); context.registerReceiver(receiver, filter);
context.registerReceiver(receiver, new IntentFilter(APKInstall.class.getName()));
return receiver;
} }
public static void installHideResult(Activity c, File apk) { public static class InstallReceiver extends BroadcastReceiver {
Intent intent = installIntent(c, apk); private final Context context;
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true); private final String packageName;
c.startActivityForResult(intent, 0); // Ignore result, use install receiver private final Runnable onSuccess;
private final CountDownLatch latch = new CountDownLatch(1);
private Intent intent = null;
private InstallReceiver(Context context, String packageName, Runnable onSuccess) {
this.context = context;
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;
String pkg = data.getSchemeSpecificPart();
if (pkg.equals(packageName)) {
onSuccess.run();
context.unregisterReceiver(this);
}
return;
}
int status = i.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
switch (status) {
case STATUS_PENDING_USER_ACTION:
intent = i.getParcelableExtra(Intent.EXTRA_INTENT);
break;
case STATUS_SUCCESS:
if (onSuccess != null) onSuccess.run();
default:
context.unregisterReceiver(this);
}
latch.countDown();
}
// @WorkerThread @Nullable
public Intent waitIntent() {
try {
//noinspection ResultOfMethodCallIgnored
latch.await(5, TimeUnit.SECONDS);
} catch (Exception ignored) {
}
return intent;
}
} }
} }
package com.topjohnwu.magisk.core package com.topjohnwu.magisk.core
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.pm.ProviderInfo import android.content.pm.ProviderInfo
import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler import com.topjohnwu.magisk.core.su.SuCallbackHandler
import java.io.File import java.io.File
open class Provider : FileProvider() { class Provider : ContentProvider() {
override fun attachInfo(context: Context, info: ProviderInfo?) { override fun attachInfo(context: Context, info: ProviderInfo) {
super.attachInfo(context.wrap(), info) super.attachInfo(context.wrap(), info)
} }
...@@ -36,4 +38,11 @@ open class Provider : FileProvider() { ...@@ -36,4 +38,11 @@ open class Provider : FileProvider() {
fun PREFS_URI(pkg: String) = fun PREFS_URI(pkg: String) =
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build() Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
} }
override fun onCreate() = true
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
} }
...@@ -73,8 +73,12 @@ sealed class Subject : Parcelable { ...@@ -73,8 +73,12 @@ sealed class Subject : Parcelable {
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
override fun pendingIntent(context: Context) = override fun pendingIntent(context: Context): PendingIntent {
APKInstall.installIntent(context, file.toFile()).toPending(context) val receiver = APKInstall.register(context, null, null)
APKInstall.installapk(context, file.toFile())
val intent = receiver.waitIntent() ?: Intent()
return intent.toPending(context)
}
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
......
package com.topjohnwu.magisk.core.tasks package com.topjohnwu.magisk.core.tasks
import android.app.Activity import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
...@@ -27,7 +26,6 @@ import timber.log.Timber ...@@ -27,7 +26,6 @@ import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.lang.ref.WeakReference
import java.security.SecureRandom import java.security.SecureRandom
object HideAPK { object HideAPK {
...@@ -41,8 +39,6 @@ object HideAPK { ...@@ -41,8 +39,6 @@ object HideAPK {
const val MAX_LABEL_LENGTH = 32 const val MAX_LABEL_LENGTH = 32
private val svc get() = ServiceLocator.networkService private val svc get() = ServiceLocator.networkService
private val Context.APK_URI get() = Provider.APK_URI(packageName)
private val Context.PREFS_URI get() = Provider.PREFS_URI(packageName)
private fun genPackageName(): String { private fun genPackageName(): String {
val random = SecureRandom() val random = SecureRandom()
...@@ -92,35 +88,16 @@ object HideAPK { ...@@ -92,35 +88,16 @@ object HideAPK {
return true return true
} }
private class WaitPackageReceiver( private fun launchApp(activity: Activity, pkg: String) {
private val pkg: String, val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
activity: Activity Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
) : BroadcastReceiver() { val self = activity.packageName
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
private val activity = WeakReference(activity) activity.grantUriPermission(pkg, Provider.APK_URI(self), flag)
activity.grantUriPermission(pkg, Provider.PREFS_URI(self), flag)
private fun launchApp(): Unit = activity.get()?.run { intent.putExtra(Const.Key.PREV_PKG, self)
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return activity.startActivity(intent)
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg activity.finish()
grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.putExtra(Const.Key.PREV_PKG, packageName)
startActivity(intent)
finish()
} ?: Unit
override fun onReceive(context: Context, intent: Intent) {
when (intent.action ?: return) {
Intent.ACTION_PACKAGE_REPLACED, Intent.ACTION_PACKAGE_ADDED -> {
val newPkg = intent.data?.encodedSchemeSpecificPart.orEmpty()
if (newPkg == pkg) {
context.unregisterReceiver(this)
launchApp()
}
}
}
}
} }
private suspend fun patchAndHide(activity: Activity, label: String): Boolean { private suspend fun patchAndHide(activity: Activity, label: String): Boolean {
...@@ -141,9 +118,14 @@ object HideAPK { ...@@ -141,9 +118,14 @@ object HideAPK {
return false return false
// Install and auto launch app // Install and auto launch app
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(pkg, activity)) val receiver = APKInstall.register(activity, pkg) {
if (!Shell.su("adb_pm_install $repack").exec().isSuccess) launchApp(activity, pkg)
APKInstall.installHideResult(activity, repack) }
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
if (!Shell.su(cmd).exec().isSuccess) {
APKInstall.installapk(activity, repack)
receiver.waitIntent()?.let { activity.startActivity(it) }
}
return true return true
} }
...@@ -157,8 +139,8 @@ object HideAPK { ...@@ -157,8 +139,8 @@ object HideAPK {
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
patchAndHide(activity, label) patchAndHide(activity, label)
} }
dialog.dismiss()
if (!result) { if (!result) {
dialog.dismiss()
Utils.toast(R.string.failure, Toast.LENGTH_LONG) Utils.toast(R.string.failure, Toast.LENGTH_LONG)
} }
} }
...@@ -171,11 +153,15 @@ object HideAPK { ...@@ -171,11 +153,15 @@ object HideAPK {
show() show()
} }
val apk = DynAPK.current(activity) val apk = DynAPK.current(activity)
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(APPLICATION_ID, activity)) val receiver = APKInstall.register(activity, APPLICATION_ID) {
Shell.su("adb_pm_install $apk").submit { launchApp(activity, APPLICATION_ID)
dialog.dismiss() dialog.dismiss()
if (!it.isSuccess) }
APKInstall.installHideResult(activity, apk) val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
Shell.su(cmd).submit(Shell.EXECUTOR) { ret ->
if (ret.isSuccess) return@submit
APKInstall.installapk(activity, apk)
receiver.waitIntent()?.let { activity.startActivity(it) }
} }
} }
} }
...@@ -129,9 +129,11 @@ adb_pm_install() { ...@@ -129,9 +129,11 @@ adb_pm_install() {
local tmp=/data/local/tmp/temp.apk local tmp=/data/local/tmp/temp.apk
cp -f "$1" $tmp cp -f "$1" $tmp
chmod 644 $tmp chmod 644 $tmp
su 2000 -c pm install $tmp || pm install $tmp su 2000 -c pm install $tmp || pm install $tmp || su 1000 -c pm install $tmp
local res=$? local res=$?
rm -f $tmp rm -f $tmp
# Note: change this will kill self
[ $res != 0 ] && appops set "$2" REQUEST_INSTALL_PACKAGES allow
return $res return $res
} }
......
...@@ -105,7 +105,7 @@ fun genStubManifest(srcDir: File, outDir: File): String { ...@@ -105,7 +105,7 @@ fun genStubManifest(srcDir: File, outDir: File): String {
cmpList.add(Component( cmpList.add(Component(
"com.topjohnwu.magisk.core.Provider", "com.topjohnwu.magisk.core.Provider",
"FileProvider", "dummy.DummyProvider",
""" """
|<provider |<provider
| android:name="%s" | android:name="%s"
......
...@@ -29,5 +29,4 @@ ...@@ -29,5 +29,4 @@
-allowaccessmodification -allowaccessmodification
-keepclassmembers class com.topjohnwu.magisk.dummy.* { <init>(); } -keepclassmembers class com.topjohnwu.magisk.dummy.* { <init>(); }
-keepclassmembers class com.topjohnwu.magisk.DownloadActivity { <init>(); } -keepclassmembers class com.topjohnwu.magisk.DownloadActivity { <init>(); }
-keepclassmembers class com.topjohnwu.magisk.FileProvider { <init>(); }
-keepclassmembers class com.topjohnwu.magisk.DelegateRootService { <init>(); } -keepclassmembers class com.topjohnwu.magisk.DelegateRootService { <init>(); }
...@@ -12,6 +12,7 @@ import android.app.Activity; ...@@ -12,6 +12,7 @@ import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
...@@ -79,6 +80,7 @@ public class DownloadActivity extends Activity { ...@@ -79,6 +80,7 @@ public class DownloadActivity extends Activity {
private void error(Throwable e) { private void error(Throwable e) {
Log.e(getClass().getSimpleName(), "", e); Log.e(getClass().getSimpleName(), "", e);
Toast.makeText(themed, e.getMessage(), Toast.LENGTH_LONG).show();
finish(); finish();
} }
...@@ -111,22 +113,22 @@ public class DownloadActivity extends Activity { ...@@ -111,22 +113,22 @@ public class DownloadActivity extends Activity {
private void dlAPK() { private void dlAPK() {
dialog = ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true); dialog = ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true);
Runnable onSuccess = () -> {
dialog.dismiss();
Toast.makeText(themed, relaunch_app, Toast.LENGTH_LONG).show();
finish();
};
// Download and upgrade the app // Download and upgrade the app
File apk = dynLoad ? DynAPK.current(this) : new File(getCacheDir(), "manager.apk"); File apk = dynLoad ? DynAPK.current(this) : new File(getCacheDir(), "manager.apk");
request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR).getAsFile(apk, file -> { request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR).getAsFile(apk, file -> {
if (dynLoad) { if (dynLoad) {
DynLoad.setup(this); DynLoad.setup(this);
runOnUiThread(() -> { onSuccess.run();
dialog.dismiss();
Toast.makeText(themed, relaunch_app, Toast.LENGTH_LONG).show();
finish();
});
} else { } else {
runOnUiThread(() -> { var receiver = APKInstall.register(this, BuildConfig.APPLICATION_ID, onSuccess);
dialog.dismiss(); APKInstall.installapk(this, file);
APKInstall.install(this, file); Intent intent = receiver.waitIntent();
finish(); if (intent != null) startActivity(intent);
});
} }
}); });
} }
...@@ -141,15 +143,10 @@ public class DownloadActivity extends Activity { ...@@ -141,15 +143,10 @@ public class DownloadActivity extends Activity {
InputStream is = new CipherInputStream(new ByteArrayInputStream(Bytes.res()), cipher); InputStream is = new CipherInputStream(new ByteArrayInputStream(Bytes.res()), cipher);
try (InputStream gzip = new GZIPInputStream(is); try (InputStream gzip = new GZIPInputStream(is);
OutputStream out = new FileOutputStream(apk)) { OutputStream out = new FileOutputStream(apk)) {
byte[] buf = new byte[4096]; APKInstall.transfer(gzip, out);
for (int read; (read = gzip.read(buf)) >= 0;) {
out.write(buf, 0, read);
}
} }
DynAPK.addAssetPath(getResources().getAssets(), apk.getPath()); DynAPK.addAssetPath(getResources().getAssets(), apk.getPath());
} catch (Exception e) { } catch (Exception ignored) {
// Should not happen
e.printStackTrace();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment