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()
+ }
}
}
}