From aea0edc3ae6ae0dc4c35e06f319ba3fbf57df64d Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:22:13 +0200 Subject: [PATCH] add shizuku install --- app/src/main/AndroidManifest.xml | 8 ++ app/src/main/java/com/deniscerri/ytdl/App.kt | 1 + .../ytdl/ui/more/settings/SettingsActivity.kt | 7 ++ .../settings/updating/UpdateSettingsModule.kt | 22 ++++- .../deniscerri/ytdl/util/ApkInstallUtil.kt | 81 ++++++++++++++++--- .../java/com/deniscerri/ytdl/util/UiUtil.kt | 14 +++- 6 files changed, 118 insertions(+), 15 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 77467adf..a023e1b7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -586,6 +586,14 @@ android:resource="@xml/provider_paths" /> + + diff --git a/app/src/main/java/com/deniscerri/ytdl/App.kt b/app/src/main/java/com/deniscerri/ytdl/App.kt index 83382021..358080b3 100644 --- a/app/src/main/java/com/deniscerri/ytdl/App.kt +++ b/app/src/main/java/com/deniscerri/ytdl/App.kt @@ -14,6 +14,7 @@ import com.deniscerri.ytdl.core.models.ExecuteException import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository import com.deniscerri.ytdl.services.BgUtilsPoTokenGeneratorService +import com.deniscerri.ytdl.util.ApkInstallUtil import com.deniscerri.ytdl.util.BgUtilsPoTokenGeneratorUtil import com.deniscerri.ytdl.util.Extensions.hasReachedEnd import com.deniscerri.ytdl.util.NotificationUtil diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsActivity.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsActivity.kt index add32b88..18589038 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/SettingsActivity.kt @@ -179,6 +179,13 @@ class SettingsActivity : BaseActivity(), SettingHost { } } } + + ApkInstallUtil.registerShizukuPermissionListener() + } + + override fun onDestroy() { + super.onDestroy() + ApkInstallUtil.unregisterShizukuPermissionListener() } override fun onResume() { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt index 72f858fa..edcbc5c2 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/updating/UpdateSettingsModule.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.content.pm.PackageManager import android.view.View import android.widget.TextView +import android.widget.Toast import androidx.core.content.PackageManagerCompat import androidx.core.content.edit import androidx.lifecycle.ViewModelProvider @@ -18,6 +19,7 @@ import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel import com.deniscerri.ytdl.ui.more.settings.SettingHost import com.deniscerri.ytdl.ui.more.settings.SettingModule import com.deniscerri.ytdl.util.ApkInstallUtil +import com.deniscerri.ytdl.util.ApkInstallUtil.REQUEST_CODE_SHIZUKU import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UpdateUtil @@ -25,6 +27,7 @@ import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import rikka.shizuku.Shizuku import java.io.File @@ -97,9 +100,22 @@ object UpdateSettingsModule : SettingModule { "apk_install_method" -> { pref.apply { setOnPreferenceChangeListener { _, newValue -> - host.findPref("apk_install_external_apk_id")?.isVisible = (newValue as String) == "external" - host.refreshUI() - true + var resp = true + if ((newValue as String) == "shizuku") { + ApkInstallUtil.requestShizukuPermission { granted, error -> + if (!granted) { + Snackbar.make(host.hostView!!, error ?: "Shizuku permission not granted", Snackbar.LENGTH_LONG).show() + resp = false + } + } + } + + if (resp) { + host.findPref("apk_install_external_apk_id")?.isVisible = (newValue as String) == "external" + host.refreshUI() + } + + resp } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/ApkInstallUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/ApkInstallUtil.kt index 6032810e..cb5e16cf 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/ApkInstallUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/ApkInstallUtil.kt @@ -26,6 +26,8 @@ object ApkInstallUtil { // can't take a per-call lambda directly. private var pendingInstallCallback: ((Result) -> Unit)? = null + private var pendingShizukuPermissionCallback: ((Boolean) -> Unit)? = null + /** * Call this once per Activity/Fragment (e.g. in onCreate) to create the launcher. * Wires the launcher's result back into whatever callback was passed to installApk(). @@ -45,6 +47,62 @@ object ApkInstallUtil { } } + private val shizukuPermissionListener = + Shizuku.OnRequestPermissionResultListener { requestCode, grantResult -> + if (requestCode != REQUEST_CODE_SHIZUKU) return@OnRequestPermissionResultListener + + val granted = grantResult == android.content.pm.PackageManager.PERMISSION_GRANTED + val callback = pendingShizukuPermissionCallback + pendingShizukuPermissionCallback = null + + callback?.let { + Handler(Looper.getMainLooper()).post { it(granted) } + } + } + + fun registerShizukuPermissionListener() { + Shizuku.addRequestPermissionResultListener(shizukuPermissionListener) + } + + fun unregisterShizukuPermissionListener() { + Shizuku.removeRequestPermissionResultListener(shizukuPermissionListener) + } + + fun requestShizukuPermission(onResult: (granted: Boolean, error: String?) -> Unit) { + if (!Shizuku.pingBinder()) { + onResult(false, "Please start the Shizuku service first") + return + } + if (Shizuku.isPreV11()) { + onResult(false, "Shizuku version not supported") + return + } + + try { + when { + Shizuku.checkSelfPermission() == android.content.pm.PackageManager.PERMISSION_GRANTED -> { + onResult(true, null) + } + Shizuku.shouldShowRequestPermissionRationale() -> { + onResult(false, "Shizuku permission was denied. Please enable it manually.") + } + else -> { + pendingShizukuPermissionCallback = { granted -> + if (granted) onResult(true, null) + else onResult(false, "Shizuku permission denied") + } + Shizuku.requestPermission(REQUEST_CODE_SHIZUKU) + } + } + } catch (e: IllegalStateException) { + // Binder wasn't actually available despite pingBinder() check — + // service likely died/restarted between the check and this call. + pendingShizukuPermissionCallback = null + onResult(false, "Shizuku service is not ready. Please try again.") + } + } + + fun installApk( context: Context, apkFile: File, @@ -75,14 +133,16 @@ object ApkInstallUtil { } } "shizuku" -> { - if (!checkShizukuPermission()) { - onResult(Result.failure(Exception("Please start Shizuku service!"))) - return + requestShizukuPermission { granted, error -> + if (!granted) { + onResult(Result.failure(Exception(error))) + return@requestShizukuPermission + } + Thread { + val result = installApkWithShizuku(apkFile) + Handler(Looper.getMainLooper()).post { onResult(result) } + }.start() } - Thread { - val result = installApkWithShizuku(apkFile) - Handler(Looper.getMainLooper()).post { onResult(result) } - }.start() } "external" -> { val packageName = preferences.getString("apk_install_external_apk_id", "")!! @@ -96,7 +156,7 @@ object ApkInstallUtil { } } - private const val REQUEST_CODE_SHIZUKU = 1001 + const val REQUEST_CODE_SHIZUKU = 1001 fun checkShizukuPermission(): Boolean { if (!Shizuku.pingBinder()) { @@ -131,7 +191,8 @@ object ApkInstallUtil { private fun installApkWithShizuku(apkFile: File): Result { return try { - val command = arrayOf("pm", "install", "-r", apkFile.absolutePath) + val apkSize = apkFile.length() + val command = arrayOf("pm", "install", "-r", "-S", apkSize.toString()) val process: Process = newShizukuProcess(command, null, null) process.outputStream.use { stdin -> @@ -144,7 +205,7 @@ object ApkInstallUtil { val errorOutput = process.errorStream.bufferedReader().readText() val exitCode = process.waitFor() - if (exitCode == 0) Result.success(Unit) + if (exitCode == 0 && output.contains("Success")) Result.success(Unit) else Result.failure(Exception(errorOutput.ifBlank { output })) } catch (e: Exception) { e.printStackTrace() diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index 9cb32ecd..7c05fc05 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -2907,10 +2907,13 @@ object UiUtil { var tmpDownloadJob : Job? = null var positiveButton: Button? = null + var negativeButton: Button? = null + val updateDialog = MaterialAlertDialogBuilder(context) .setTitle("${item.tag_name} (${FileUtil.convertFileSize(item.downloadSize)})") .setMessage(item.body) .setIcon(R.drawable.ic_update_app) + .setCancelable(false) .setNegativeButton(context.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> tmpDownloadJob?.cancel() } @@ -2932,6 +2935,8 @@ object UiUtil { val lifecycleScope = lifecycleOwner.lifecycleScope positiveButton = view.getButton(android.app.AlertDialog.BUTTON_POSITIVE) + negativeButton = view.getButton(android.app.AlertDialog.BUTTON_NEGATIVE) + positiveButton?.setOnClickListener { positiveButton.isEnabled = false positiveButton.text = "0%" @@ -2960,8 +2965,13 @@ object UiUtil { fileResp.onSuccess { file -> lifecycleScope.launch { withContext(Dispatchers.Main) { - view.dismiss() - ApkInstallUtil.installApk(context, file, installLauncher, onResult) + positiveButton.text = context.getString(R.string.please_wait) + negativeButton.isEnabled = false + + ApkInstallUtil.installApk(context, file, installLauncher) { result -> + onResult(result) + view.dismiss() + } } } }