diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index b4ea81f57..6a0f87876 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -39,6 +39,7 @@ android { } buildFeatures { + aidl = true compose = true buildConfig = true } diff --git a/manager/src/main/aidl/org/lsposed/lspatch/IShizukuService.aidl b/manager/src/main/aidl/org/lsposed/lspatch/IShizukuService.aidl new file mode 100644 index 000000000..2e4fcaf27 --- /dev/null +++ b/manager/src/main/aidl/org/lsposed/lspatch/IShizukuService.aidl @@ -0,0 +1,9 @@ +package org.lsposed.lspatch; + +interface IShizukuService { + // Executes a shell command and returns the output + String runShellCommand(String cmd) = 1; + + // Allows closing the service from the client side + void destroy() = 2; +} diff --git a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt index e35023e29..918306335 100644 --- a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt +++ b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt @@ -28,7 +28,7 @@ class LSPApplication : Application() { filesDir.mkdir() tmpApkDir = cacheDir.resolve("apk").also { it.mkdir() } prefs = lspApp.getSharedPreferences("settings", Context.MODE_PRIVATE) - ShizukuApi.init() + ShizukuApi.init(this) AppBroadcastReceiver.register(this) globalScope.launch { LSPPackageManager.fetchAppList() } } diff --git a/manager/src/main/java/org/lsposed/lspatch/ShizukuService.kt b/manager/src/main/java/org/lsposed/lspatch/ShizukuService.kt new file mode 100644 index 000000000..3af4fd79b --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ShizukuService.kt @@ -0,0 +1,22 @@ +package org.lsposed.lspatch + +import org.lsposed.lspatch.IShizukuService +import kotlin.system.exitProcess + +class ShizukuService : IShizukuService.Stub() { + override fun runShellCommand(cmd: String): String { + return try { + val process = Runtime.getRuntime().exec(cmd) + val output = process.inputStream.bufferedReader().readText() + val error = process.errorStream.bufferedReader().readText() + process.waitFor() + output + error + } catch (e: Exception) { + e.stackTraceToString() + } + } + + override fun destroy() { + exitProcess(0) + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt b/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt index 1d63c8fef..ff1b26e1b 100644 --- a/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt +++ b/manager/src/main/java/org/lsposed/lspatch/util/ShizukuApi.kt @@ -1,6 +1,9 @@ package org.lsposed.lspatch.util +import android.content.ComponentName +import android.content.Context import android.content.IntentSender +import android.content.ServiceConnection import android.content.pm.* import android.os.Build import android.os.IBinder @@ -11,12 +14,35 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import dev.rikka.tools.refine.Refine +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.withTimeoutOrNull +import org.lsposed.lspatch.IShizukuService +import org.lsposed.lspatch.ShizukuService import rikka.shizuku.Shizuku import rikka.shizuku.ShizukuBinderWrapper import rikka.shizuku.SystemServiceHelper object ShizukuApi { + @Volatile + private var userService: IShizukuService? = null + + // This allows us to "await" the service connection + private var userServiceDeferred = CompletableDeferred() + + private val userServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, service: IBinder) { + val binder = IShizukuService.Stub.asInterface(service) + userService = binder + userServiceDeferred.complete(binder) + } + + override fun onServiceDisconnected(name: ComponentName) { + userService = null + userServiceDeferred = CompletableDeferred() + } + } + private fun IBinder.wrap() = ShizukuBinderWrapper(this) private fun IInterface.asShizukuBinder() = this.asBinder().wrap() @@ -40,14 +66,34 @@ object ShizukuApi { var isBinderAvailable = false var isPermissionGranted by mutableStateOf(false) - fun init() { + fun init(context: Context) { Shizuku.addBinderReceivedListenerSticky { isBinderAvailable = true isPermissionGranted = Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED + if (isPermissionGranted) { + // Trigger the service binding as soon as we have permission + bindUserService(context) + } } Shizuku.addBinderDeadListener { isBinderAvailable = false isPermissionGranted = false + userService = null + userServiceDeferred = CompletableDeferred() + } + } + + private fun bindUserService(context: Context) { + if (userService != null) return + val args = Shizuku.UserServiceArgs(ComponentName(context.packageName, ShizukuService::class.java.name)) + .daemon(false) + .processNameSuffix("service") + .debuggable(true) + + try { + Shizuku.bindUserService(args, userServiceConnection) + } catch (e: Exception) { + e.printStackTrace() } } @@ -71,11 +117,36 @@ object ShizukuApi { packageInstaller.uninstall(packageName, intentSender) } - fun performDexOptMode(packageName: String): Boolean { - return iPackageManager.performDexOptMode( - packageName, - SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false), - "verify", true, true, null - ) + suspend fun performDexOptMode(packageName: String): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 34+ + // Wait up to 3 seconds for the service to connect if it hasn't yet + val service = userService ?: withTimeoutOrNull(3000) { + userServiceDeferred.await() + } ?: return false + + return try { + val command = "cmd package compile -m speed-profile -f $packageName" + val output = service.runShellCommand(command) + // Return true if output contains "Success" + output.contains("Success") + } catch (e: Exception) { + e.printStackTrace() + false + } + } else { + // Legacy reflection-based method for older versions + return try { + iPackageManager.performDexOptMode( + packageName, + SystemProperties.getBoolean("dalvik.vm.usejitprofiles", false), + "verify", + true, + true, + null + ) + } catch (e: Exception) { + false + } + } } }