Skip to content

Commit 3621b8f

Browse files
committed
#2028 fix: work around Shizuku bug on Mediatek devices that prevents Expert mode from starting
1 parent 20f587a commit 3621b8f

2 files changed

Lines changed: 140 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
## Fixed
1313

1414
- #2030 do not filter out unknown evdev key events.
15+
- #2028 work around Shizuku bug on Mediatek devices that prevents Expert mode from starting
1516

1617
## [4.0.3](https://github.com/sds100/KeyMapper/releases/tag/v4.0.3)
1718

sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt

Lines changed: 139 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ import java.util.zip.ZipEntry
3434
import java.util.zip.ZipFile
3535
import javax.inject.Inject
3636
import javax.inject.Singleton
37+
import kotlinx.coroutines.CompletableDeferred
3738
import kotlinx.coroutines.Dispatchers
3839
import kotlinx.coroutines.runBlocking
3940
import kotlinx.coroutines.sync.Mutex
4041
import kotlinx.coroutines.sync.withLock
4142
import kotlinx.coroutines.withContext
43+
import kotlinx.coroutines.withTimeoutOrNull
4244
import rikka.shizuku.Shizuku
4345
import timber.log.Timber
4446

@@ -63,69 +65,160 @@ class SystemBridgeStarter @Inject constructor(
6365

6466
private val startMutex: Mutex = Mutex()
6567

66-
private val shizukuStarterConnection: ServiceConnection = object : ServiceConnection {
67-
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
68-
Timber.i("Shizuku starter service connected")
69-
70-
val service = IShizukuStarterService.Stub.asInterface(binder)
71-
72-
Timber.i("Starting System Bridge with Shizuku starter service")
73-
try {
74-
runBlocking {
75-
startSystemBridgeWithLock(
76-
commandExecutor = { command ->
77-
val output = service.executeCommand(command)
78-
79-
if (output == null) {
80-
KMError.UnknownIOError
81-
} else {
82-
Success(output)
83-
}
84-
},
85-
)
86-
}
87-
} catch (e: RemoteException) {
88-
Timber.e("Exception starting with Shizuku starter service: $e")
89-
} finally {
90-
try {
91-
service.destroy()
92-
} catch (_: DeadObjectException) {
93-
// Do nothing. Service is already dead.
94-
}
95-
}
96-
}
68+
private companion object {
69+
/**
70+
* How long to wait for the Shizuku user service to connect before
71+
* assuming it failed (e.g. due to the OEM bug on Xiaomi/MediaTek devices).
72+
*/
73+
private const val SHIZUKU_USER_SERVICE_TIMEOUT_MS = 5000L
74+
}
9775

98-
override fun onServiceDisconnected(name: ComponentName?) {
99-
// Do nothing. The service is supposed to immediately kill itself
100-
// after starting the command.
101-
}
76+
private fun buildShizukuUserServiceArgs(): Shizuku.UserServiceArgs {
77+
val serviceComponentName = ComponentName(ctx, ShizukuStarterService::class.java)
78+
return Shizuku.UserServiceArgs(serviceComponentName)
79+
.daemon(false)
80+
.processNameSuffix("service")
81+
.debuggable(BuildConfig.DEBUG)
82+
.version(buildConfigProvider.versionCode)
10283
}
10384

104-
fun startWithShizuku() {
85+
suspend fun startWithShizuku() {
10586
if (!Shizuku.pingBinder()) {
10687
Timber.w("Shizuku is not running. Cannot start System Bridge with Shizuku.")
10788
return
10889
}
10990

91+
val serviceConnected = CompletableDeferred<Unit>()
92+
11093
// Shizuku will start a service which will then start the System Bridge. Shizuku won't be
11194
// used to start the System Bridge directly because native libraries need to be used
11295
// and we want to limit the dependency on Shizuku as much as possible. Also, the System
11396
// Bridge should still be running even if Shizuku dies.
114-
val serviceComponentName = ComponentName(ctx, ShizukuStarterService::class.java)
115-
val args = Shizuku.UserServiceArgs(serviceComponentName)
116-
.daemon(false)
117-
.processNameSuffix("service")
118-
.debuggable(BuildConfig.DEBUG)
119-
.version(buildConfigProvider.versionCode)
97+
val connection = object : ServiceConnection {
98+
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
99+
Timber.i("Shizuku starter service connected")
100+
101+
// Signal that the user service started successfully before doing the work.
102+
serviceConnected.complete(Unit)
103+
104+
val service = IShizukuStarterService.Stub.asInterface(binder)
105+
106+
Timber.i("Starting System Bridge with Shizuku starter service")
107+
try {
108+
runBlocking {
109+
startSystemBridgeWithLock(
110+
commandExecutor = { command ->
111+
val output = service.executeCommand(command)
112+
113+
if (output == null) {
114+
KMError.UnknownIOError
115+
} else {
116+
Success(output)
117+
}
118+
},
119+
)
120+
}
121+
} catch (e: RemoteException) {
122+
Timber.e("Exception starting with Shizuku starter service: $e")
123+
} finally {
124+
try {
125+
service.destroy()
126+
} catch (_: DeadObjectException) {
127+
// Do nothing. Service is already dead.
128+
}
129+
}
130+
}
131+
132+
override fun onServiceDisconnected(name: ComponentName?) {
133+
// Do nothing. The service is supposed to immediately kill itself
134+
// after starting the command.
135+
}
136+
}
137+
138+
val args = buildShizukuUserServiceArgs()
120139

121140
try {
122-
Shizuku.bindUserService(
123-
args,
124-
shizukuStarterConnection,
125-
)
141+
Shizuku.bindUserService(args, connection)
126142
} catch (e: Exception) {
127143
Timber.e("Exception when starting System Bridge with Shizuku. $e")
144+
return
145+
}
146+
147+
// Wait for the user service to connect. On most devices this is near-instant.
148+
// On Xiaomi/MediaTek devices with OEM-modified LoadedApk.makeApplicationInner(),
149+
// the user service process crashes with a NPE before it can connect
150+
// (see https://github.com/RikkaApps/Shizuku/issues/1198).
151+
val connected = withTimeoutOrNull(SHIZUKU_USER_SERVICE_TIMEOUT_MS) {
152+
serviceConnected.await()
153+
}
154+
155+
if (connected != null) {
156+
// User service connected successfully; the existing flow handles the rest.
157+
return
158+
}
159+
160+
// Timeout expired. Use peekUserService to confirm the service isn't running.
161+
val serviceVersion = Shizuku.peekUserService(args, connection)
162+
163+
if (serviceVersion >= 0) {
164+
// The service is running but just slow to connect. Let it proceed.
165+
Timber.w(
166+
"Shizuku user service is running (version=$serviceVersion) but took " +
167+
"longer than ${SHIZUKU_USER_SERVICE_TIMEOUT_MS}ms to connect.",
168+
)
169+
return
128170
}
171+
172+
// The service failed to start. Fall back to Shizuku.newProcess() via reflection.
173+
Timber.w("Falling back to Shizuku.newProcess() workaround.")
174+
startWithShizukuNewProcess()
175+
}
176+
177+
/**
178+
* Fallback for starting the System Bridge via Shizuku when the user service fails to start.
179+
* This can happen on Xiaomi/MediaTek devices where an OEM modification to
180+
* LoadedApk.makeApplicationInner() causes a NPE during user service process creation.
181+
*
182+
* This method uses reflection to call the private Shizuku.newProcess() method, which
183+
* spawns a process directly on the Shizuku server side without going through
184+
* LoadedApk.makeApplication(), bypassing the OEM bug entirely.
185+
*
186+
* Note: Shizuku.newProcess() is deprecated and planned for removal in Shizuku API 14.
187+
* This workaround should be removed once the upstream Shizuku bug is fixed.
188+
*/
189+
@Suppress("DiscouragedPrivateApi")
190+
private suspend fun startWithShizukuNewProcess() {
191+
Timber.i("Starting System Bridge with Shizuku newProcess fallback")
192+
193+
startSystemBridgeWithLock(
194+
commandExecutor = { command ->
195+
try {
196+
val method = Shizuku::class.java.getDeclaredMethod(
197+
"newProcess",
198+
Array<String>::class.java,
199+
Array<String>::class.java,
200+
String::class.java,
201+
)
202+
method.isAccessible = true
203+
204+
val process = method.invoke(
205+
null,
206+
arrayOf("sh", "-c", command),
207+
null,
208+
null,
209+
) as Process
210+
211+
val stdout = process.inputStream.bufferedReader().readText()
212+
val stderr = process.errorStream.bufferedReader().readText()
213+
process.waitFor()
214+
215+
Success("$stdout\n$stderr")
216+
} catch (e: Exception) {
217+
Timber.e("Shizuku newProcess fallback failed: $e")
218+
KMError.Exception(e)
219+
}
220+
},
221+
)
129222
}
130223

131224
@RequiresApi(Build.VERSION_CODES.R)

0 commit comments

Comments
 (0)