Shevery v13.7.0.r15 hotfix-2
Release r15 hotfix-2
Bug Fixes and Technical Improvements
-
Android 17 / API 37 Compatibility Layer
- Fixed dynamic reflection type mismatch when passing default device IDs as integer parameters (switched to default String).
- Resolved parameter transposition in package checks.
- Refactored UID permission auditing to look up the correct signature.
-
Comput Console Deadlock Resolution
- Replaced sequential blocking stream reads with concurrent coroutine-based async/await draining, eliminating deadlocks when reading stdout and stderr pipes.
-
KeyStore API Key Encryption
- Implemented real AES-GCM-NoPadding encryption backed by Android KeyStore, securing the Gemini API key.
- Automatically migrates existing plain text keys.
-
WebView JS Bridge Isolation
- Constrained WebView client navigations to open HTTP/HTTPS links in system browsers.
- Restricted Javascript interface usage to same-origin checks comparing caller context to local module root paths.
-
Dhizuku Integration
- Fully restored the Dhizuku integration and verified build compatibility.
Commits
2200390fix(project): resolve Comput screen deadlocks, Gemini KeyStore encryption, WebView JS bridge security, and Android 17 compat
Line-by-Line Changes Diff
--- a/docs/adb-modules-api.md
+++ b/docs/adb-modules-api.md
@@ -252,14 +252,10 @@
-The optional AI checker is hidden by default. It is toggled from Settings by tapping the translation contributors row five times quickly. After unlock:
-
-- the AI settings row appears in module settings;
-- the Gemini star appears in ReCommand dialogs;
-- the API key is stored encrypted with Android Keystore;
-- supported model ids are `gemini-3.1-pro-preview`, `gemini-3-flash-preview`, and `gemini-3.1-flash-lite-preview`.
-
-Repeating the same five-tap gesture hides the AI settings row and Gemini star again.
+The optional AI checker is fully integrated into the manager:
+- Gemini console explanation is available in the Comput screen.
+- The API key is securely encrypted using Android Keystore.
+- Configurable settings are directly accessible under Comput Console settings.
--- a/manager/src/main/java/moe/shizuku/manager/logs/ComputScreen.kt
+++ b/manager/src/main/java/moe/shizuku/manager/logs/ComputScreen.kt
@@ -71,6 +71,8 @@
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
@@ -202,9 +204,15 @@
- val stdoutText = ParcelFileDescriptor.AutoCloseInputStream(remote.getInputStream()).bufferedReader().use { it.readText() }
- val stderrText = ParcelFileDescriptor.AutoCloseInputStream(remote.getErrorStream()).bufferedReader().use { it.readText() }
+ val (stdoutText, stderrText) = kotlinx.coroutines.coroutineScope {
+ val stdoutDeferred = async {
+ ParcelFileDescriptor.AutoCloseInputStream(remote.getInputStream()).bufferedReader().use { it.readText() }
+ }
+ val stderrDeferred = async {
+ ParcelFileDescriptor.AutoCloseInputStream(remote.getErrorStream()).bufferedReader().use { it.readText() }
+ }
+ stdoutDeferred.await() to stderrDeferred.await()
+ }
--- a/manager/src/main/java/moe/shizuku/manager/module/ModuleJsBridge.kt
+++ b/manager/src/main/java/moe/shizuku/manager/module/ModuleJsBridge.kt
+ private fun isOriginValid(): Boolean {
+ val latch = java.util.concurrent.CountDownLatch(1)
+ var url: String? = null
+ webView.post {
+ url = webView.url
+ latch.countDown()
+ }
+ try {
+ latch.await(500, java.util.concurrent.TimeUnit.MILLISECONDS)
+ } catch (e: Exception) {
+ return false
+ }
+ val currentUrl = url ?: return false
+ if (!currentUrl.startsWith("file://", ignoreCase = true)) {
+ return false
+ }
+ val cleanUrl = currentUrl.substring(7).split("?")[0].split("#")[0]
+ val root = module.webRoot ?: return false
+ return try {
+ val rootPath = root.canonicalPath
+ val filePath = File(cleanUrl).canonicalPath
+ filePath.startsWith(rootPath)
+ } catch (e: Exception) {
+ false
+ }
+ }
--- a/server/src/main/java/rikka/shizuku/server/util/Android17Compat.java
+++ b/server/src/main/java/rikka/shizuku/server/util/Android17Compat.java
@@ -148,7 +148,8 @@
- return (int) invokeMethod(pm, sCheckPermissionMethod, permissionName, packageName, userId);
+ // Pass packageName first, permissionName second to match IPermissionManager signature
+ return (int) invokeMethod(pm, sCheckPermissionMethod, packageName, permissionName, userId);
@@ -168,17 +169,16 @@
- sCheckPermissionUidMethod = findMethod(pm, "checkPermission", String.class, int.class);
+ sCheckPermissionUidMethod = findMethod(pm, "checkUidPermission", int.class, String.class);
- if (paramTypes.length == 3 && paramTypes[1] == int.class && paramTypes[2] == int.class) {
- // (String permission, int deviceId, int uid)
- return (int) sCheckPermissionUidMethod.invoke(pm, permissionName, DEVICE_ID_DEFAULT, uid);
+ if (paramTypes.length == 3 && paramTypes[0] == int.class && paramTypes[1] == String.class && paramTypes[2] == int.class) {
+ // (int uid, String permission, int deviceId)
+ return (int) sCheckPermissionUidMethod.invoke(pm, uid, permissionName, 0);
- return (int) sCheckPermissionUidMethod.invoke(pm, permissionName, uid);
@@ -227,7 +227,7 @@
- sRevokeRuntimePermissionMethod.invoke(pm, packageName, permissionName, DEVICE_ID_DEFAULT, userId, "shizuku");
+ sRevokeRuntimePermissionMethod.invoke(pm, packageName, permissionName, "default", userId, "shizuku");
@@ -272,7 +272,11 @@
- args[prefixLen] = DEVICE_ID_DEFAULT;
+ if (paramTypes[prefixLen] == String.class) {
+ args[prefixLen] = "default";
+ } else {
+ args[prefixLen] = 0; // DEVICE_ID_DEFAULT
+ }
+```