@@ -34,11 +34,13 @@ import java.util.zip.ZipEntry
3434import java.util.zip.ZipFile
3535import javax.inject.Inject
3636import javax.inject.Singleton
37+ import kotlinx.coroutines.CompletableDeferred
3738import kotlinx.coroutines.Dispatchers
3839import kotlinx.coroutines.runBlocking
3940import kotlinx.coroutines.sync.Mutex
4041import kotlinx.coroutines.sync.withLock
4142import kotlinx.coroutines.withContext
43+ import kotlinx.coroutines.withTimeoutOrNull
4244import rikka.shizuku.Shizuku
4345import 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