diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt index 7e68355..57fe607 100644 --- a/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/MainActivity.kt @@ -5,12 +5,27 @@ import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.Result import androidx.annotation.NonNull +import android.content.Intent +import android.content.IntentSender +import android.content.pm.IPackageInstaller +import android.content.pm.IPackageInstallerSession +import android.content.pm.PackageInstaller import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.os.Process import rikka.shizuku.Shizuku import rikka.shizuku.Shizuku.OnBinderDeadListener import rikka.shizuku.Shizuku.OnBinderReceivedListener import rikka.shizuku.Shizuku.OnRequestPermissionResultListener +import rikka.shizuku.ShizukuBinderWrapper +import dev.imranr.obtainium.util.IIntentSenderAdaptor +import dev.imranr.obtainium.util.IntentSenderUtils +import dev.imranr.obtainium.util.PackageInstallerUtils +import dev.imranr.obtainium.util.ShizukuSystemServerApi +import java.io.IOException +import java.util.concurrent.CountDownLatch import com.topjohnwu.superuser.Shell class MainActivity: FlutterActivity() { @@ -46,8 +61,107 @@ class MainActivity: FlutterActivity() { } } + private fun shizukuInstallApk(uri: Uri) { + val packageInstaller: PackageInstaller + var session: PackageInstaller.Session? = null + val cr = contentResolver + val res = StringBuilder() + val installerPackageName: String + var installerAttributionTag: String? = null + val userId: Int + val isRoot: Boolean + try { + val _packageInstaller: IPackageInstaller = + ShizukuSystemServerApi.PackageManager_getPackageInstaller() + isRoot = Shizuku.getUid() == 0 + + // the reason for use "com.android.shell" as installer package under adb is that getMySessions will check installer package's owner + installerPackageName = if (isRoot) packageName else "com.android.shell" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + installerAttributionTag = attributionTag + } + userId = if (isRoot) Process.myUserHandle().hashCode() else 0 + packageInstaller = PackageInstallerUtils.createPackageInstaller( + _packageInstaller, + installerPackageName, + installerAttributionTag, + userId + ) + val sessionId: Int + res.append("createSession: ") + val params = + PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + var installFlags: Int = PackageInstallerUtils.getInstallFlags(params) + installFlags = + installFlags or (0x00000004 /*PackageManager.INSTALL_ALLOW_TEST*/ or 0x00000002) /*PackageManager.INSTALL_REPLACE_EXISTING*/ + PackageInstallerUtils.setInstallFlags(params, installFlags) + sessionId = packageInstaller.createSession(params) + res.append(sessionId).append('\n') + res.append('\n').append("write: ") + val _session = IPackageInstallerSession.Stub.asInterface( + ShizukuBinderWrapper( + _packageInstaller.openSession(sessionId).asBinder() + ) + ) + session = PackageInstallerUtils.createSession(_session) + val name = "apk.apk" + val `is` = cr.openInputStream(uri) + val os = session.openWrite(name, 0, -1) + val buf = ByteArray(8192) + var len: Int + try { + while (`is`!!.read(buf).also { len = it } > 0) { + os.write(buf, 0, len) + os.flush() + session.fsync(os) + } + } finally { + try { + `is`!!.close() + } catch (e: IOException) { + e.printStackTrace() + } + try { + os.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + res.append('\n').append("commit: ") + val results = arrayOf(null) + val countDownLatch = CountDownLatch(1) + val intentSender: IntentSender = + IntentSenderUtils.newInstance(object : IIntentSenderAdaptor() { + override fun send(intent: Intent?) { + results[0] = intent + countDownLatch.countDown() + } + }) + session.commit(intentSender) + countDownLatch.await() + val result = results[0] + val status = + result!!.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE) + val message = result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + res.append('\n').append("status: ").append(status).append(" (").append(message) + .append(")") + } catch (tr: Throwable) { + tr.printStackTrace() + res.append(tr) + } finally { + if (session != null) { + try { + session.close() + } catch (tr: Throwable) { + res.append(tr) + } + } + } + } + private fun installWithShizuku(apkFilePath: String, result: Result) { shizukuCheckPermission() + shizukuInstallApk(Uri.parse("file://$apkFilePath")) result.success(0) } diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/ApplicationUtils.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/ApplicationUtils.java new file mode 100644 index 0000000..6aa7489 --- /dev/null +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/ApplicationUtils.java @@ -0,0 +1,37 @@ +package dev.imranr.obtainium.util; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.os.Build; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ApplicationUtils { + + private static Application application; + + public static Application getApplication() { + return application; + } + + public static void setApplication(Application application) { + ApplicationUtils.application = application; + } + + public static String getProcessName() { + if (Build.VERSION.SDK_INT >= 28) + return Application.getProcessName(); + else { + try { + @SuppressLint("PrivateApi") + Class activityThread = Class.forName("android.app.ActivityThread"); + @SuppressLint("DiscouragedPrivateApi") + Method method = activityThread.getDeclaredMethod("currentProcessName"); + return (String) method.invoke(null); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/IIntentSenderAdaptor.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/IIntentSenderAdaptor.java new file mode 100644 index 0000000..2178c77 --- /dev/null +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/IIntentSenderAdaptor.java @@ -0,0 +1,23 @@ +package dev.imranr.obtainium.util; + +import android.content.IIntentReceiver; +import android.content.IIntentSender; +import android.content.Intent; +import android.os.Bundle; +import android.os.IBinder; + +public abstract class IIntentSenderAdaptor extends IIntentSender.Stub { + + public abstract void send(Intent intent); + + @Override + public int send(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { + send(intent); + return 0; + } + + @Override + public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { + send(intent); + } +} diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/IntentSenderUtils.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/IntentSenderUtils.java new file mode 100644 index 0000000..ab6acba --- /dev/null +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/IntentSenderUtils.java @@ -0,0 +1,14 @@ +package dev.imranr.obtainium.util; + +import android.content.IIntentSender; +import android.content.IntentSender; + +import java.lang.reflect.InvocationTargetException; + +public class IntentSenderUtils { + + public static IntentSender newInstance(IIntentSender binder) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + //noinspection JavaReflectionMemberAccess + return IntentSender.class.getConstructor(IIntentSender.class).newInstance(binder); + } +} diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/PackageInstallerUtils.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/PackageInstallerUtils.java new file mode 100644 index 0000000..9d5ae14 --- /dev/null +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/PackageInstallerUtils.java @@ -0,0 +1,41 @@ +package dev.imranr.obtainium.util; + +import android.content.Context; +import android.content.pm.IPackageInstaller; +import android.content.pm.IPackageInstallerSession; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.os.Build; + +import java.lang.reflect.InvocationTargetException; + +@SuppressWarnings({"JavaReflectionMemberAccess"}) +public class PackageInstallerUtils { + + public static PackageInstaller createPackageInstaller(IPackageInstaller installer, String installerPackageName, String installerAttributionTag, int userId) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, String.class, int.class) + .newInstance(installer, installerPackageName, installerAttributionTag, userId); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return PackageInstaller.class.getConstructor(IPackageInstaller.class, String.class, int.class) + .newInstance(installer, installerPackageName, userId); + } else { + return PackageInstaller.class.getConstructor(Context.class, PackageManager.class, IPackageInstaller.class, String.class, int.class) + .newInstance(ApplicationUtils.getApplication(), ApplicationUtils.getApplication().getPackageManager(), installer, installerPackageName, userId); + } + } + + public static PackageInstaller.Session createSession(IPackageInstallerSession session) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + return PackageInstaller.Session.class.getConstructor(IPackageInstallerSession.class) + .newInstance(session); + + } + + public static int getInstallFlags(PackageInstaller.SessionParams params) throws NoSuchFieldException, IllegalAccessException { + return (int) PackageInstaller.SessionParams.class.getDeclaredField("installFlags").get(params); + } + + public static void setInstallFlags(PackageInstaller.SessionParams params, int newValue) throws NoSuchFieldException, IllegalAccessException { + PackageInstaller.SessionParams.class.getDeclaredField("installFlags").set(params, newValue); + } +} diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/ShizukuSystemServerApi.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/ShizukuSystemServerApi.java new file mode 100644 index 0000000..6dcdf75 --- /dev/null +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/ShizukuSystemServerApi.java @@ -0,0 +1,68 @@ +package dev.imranr.obtainium.util; + +import android.content.Context; +import android.content.pm.IPackageInstaller; +import android.content.pm.IPackageManager; +import android.content.pm.UserInfo; +import android.os.Build; +import android.os.IUserManager; +import android.os.RemoteException; + +import java.util.List; + +import rikka.shizuku.ShizukuBinderWrapper; +import rikka.shizuku.SystemServiceHelper; + +public class ShizukuSystemServerApi { + + private static final Singleton PACKAGE_MANAGER = new Singleton() { + @Override + protected IPackageManager create() { + return IPackageManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package"))); + } + }; + + private static final Singleton USER_MANAGER = new Singleton() { + @Override + protected IUserManager create() { + return IUserManager.Stub.asInterface(new ShizukuBinderWrapper(SystemServiceHelper.getSystemService(Context.USER_SERVICE))); + } + }; + + public static IPackageInstaller PackageManager_getPackageInstaller() throws RemoteException { + IPackageInstaller packageInstaller = PACKAGE_MANAGER.get().getPackageInstaller(); + return IPackageInstaller.Stub.asInterface(new ShizukuBinderWrapper(packageInstaller.asBinder())); + } + + public static List UserManager_getUsers(boolean excludePartial, boolean excludeDying, boolean excludePreCreated) throws RemoteException { + if (Build.VERSION.SDK_INT >= 30) { + return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated); + } else { + try { + return USER_MANAGER.get().getUsers(excludeDying); + } catch (NoSuchFieldError e) { + return USER_MANAGER.get().getUsers(excludePartial, excludeDying, excludePreCreated); + } + } + } + + // method 2: use transactRemote directly + /*public static List UserManager_getUsers(boolean excludeDying) { + Parcel data = SystemServiceHelper.obtainParcel(Context.USER_SERVICE, "android.os.IUserManager", "getUsers"); + Parcel reply = Parcel.obtain(); + data.writeInt(excludeDying ? 1 : 0); + + List res = null; + try { + ShizukuService.transactRemote(data, reply, 0); + reply.readException(); + res = reply.createTypedArrayList(UserInfo.CREATOR); + } catch (RemoteException e) { + Log.e("ShizukuSample", "UserManager#getUsers", e); + } finally { + data.recycle(); + reply.recycle(); + } + return res; + }*/ +} diff --git a/android/app/src/main/kotlin/dev/imranr/obtainium/util/Singleton.java b/android/app/src/main/kotlin/dev/imranr/obtainium/util/Singleton.java new file mode 100644 index 0000000..e535245 --- /dev/null +++ b/android/app/src/main/kotlin/dev/imranr/obtainium/util/Singleton.java @@ -0,0 +1,17 @@ +package dev.imranr.obtainium.util; + +public abstract class Singleton { + + private T mInstance; + + protected abstract T create(); + + public final T get() { + synchronized (this) { + if (mInstance == null) { + mInstance = create(); + } + return mInstance; + } + } +}