Skip to content

Commit 35fc40f

Browse files
committed
Refactor ShizukuInstallerServiceImpl to use reflection for hidden Android framework APIs.
- Replace direct `IPackageInstaller` and `IPackageInstallerSession` references with reflection to improve compatibility across Android versions. - Implement dynamic method lookups for `createSession`, `openSession`, `openWrite`, `commit`, and `uninstall` to handle varying method signatures. - Update `createStatusReceiver` to use a dynamic proxy for `IIntentSender` and reflectively instantiate `IntentSender`. - Update `Shizuku.unbindUserService` call in `ShizukuServiceManager` to include the `remove` flag.
1 parent ab5dcd1 commit 35fc40f

2 files changed

Lines changed: 192 additions & 73 deletions

File tree

core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuInstallerServiceImpl.kt

Lines changed: 191 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package zed.rainxch.core.data.services.shizuku
22

3-
import android.content.pm.IPackageInstaller
4-
import android.content.pm.IPackageInstallerSession
3+
import android.content.Intent
4+
import android.content.IntentSender
55
import android.content.pm.PackageInstaller
6-
import android.content.pm.PackageManager
7-
import android.content.pm.VersionedPackage
86
import android.net.LocalServerSocket
97
import android.net.LocalSocket
108
import android.net.LocalSocketAddress
9+
import android.os.Bundle
1110
import android.os.IBinder
1211
import android.os.ParcelFileDescriptor
1312
import android.os.SystemClock
@@ -16,6 +15,8 @@ import java.io.DataInputStream
1615
import java.io.DataOutputStream
1716
import java.io.File
1817
import java.io.FileInputStream
18+
import java.lang.reflect.InvocationHandler
19+
import java.lang.reflect.Proxy
1920
import java.util.concurrent.CountDownLatch
2021
import java.util.concurrent.TimeUnit
2122
import java.util.concurrent.atomic.AtomicInteger
@@ -27,6 +28,9 @@ import java.util.concurrent.atomic.AtomicInteger
2728
* This class runs in Shizuku's process, NOT in the app's process.
2829
* It has shell-level (UID 2000) or root-level (UID 0) privileges.
2930
*
31+
* Uses reflection to access hidden Android framework APIs, since their method
32+
* signatures vary across Android versions and hidden-api-stub versions.
33+
*
3034
* MUST have a default no-arg constructor for Shizuku's UserService framework.
3135
*/
3236
class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() {
@@ -35,7 +39,6 @@ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() {
3539
private const val INSTALL_TIMEOUT_SECONDS = 120L
3640
private const val UNINSTALL_TIMEOUT_SECONDS = 60L
3741

38-
// PackageInstaller status codes
3942
private const val STATUS_SUCCESS = 0
4043
private const val STATUS_FAILURE = -1
4144
private const val STATUS_FAILURE_ABORTED = -2
@@ -47,33 +50,56 @@ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() {
4750
private const val STATUS_FAILURE_TIMEOUT = -8
4851
}
4952

50-
private fun getPackageInstaller(): IPackageInstaller {
53+
/**
54+
* Obtains the system IPackageInstaller binder via IPackageManager (reflection).
55+
*/
56+
private fun getPackageInstallerBinder(): Any {
5157
val binder: IBinder = SystemServiceHelper.getSystemService("package")
52-
val pm = android.content.pm.IPackageManager.Stub.asInterface(binder)
53-
return pm.packageInstaller
58+
59+
// IPackageManager.Stub.asInterface(binder)
60+
val ipmClass = Class.forName("android.content.pm.IPackageManager\$Stub")
61+
val asInterface = ipmClass.getMethod("asInterface", IBinder::class.java)
62+
val pm = asInterface.invoke(null, binder)
63+
64+
// pm.getPackageInstaller()
65+
val getInstaller = pm.javaClass.getMethod("getPackageInstaller")
66+
return getInstaller.invoke(pm)!!
5467
}
5568

5669
override fun installPackage(apkPath: String): Int {
5770
val file = File(apkPath)
5871
if (!file.exists()) return STATUS_FAILURE_INVALID
5972

6073
return try {
61-
val installer = getPackageInstaller()
74+
val installer = getPackageInstallerBinder()
75+
val installerClass = installer.javaClass
76+
6277
val params = PackageInstaller.SessionParams(
6378
PackageInstaller.SessionParams.MODE_FULL_INSTALL
6479
)
6580
params.setSize(file.length())
6681

67-
val installerPackageName = "com.android.shell"
68-
val sessionId = installer.createSession(params, installerPackageName, null, android.os.Process.myUid())
82+
// createSession — try various signatures across Android versions
83+
val sessionId = createSession(installer, installerClass, params)
6984

70-
val session = IPackageInstallerSession.Stub.asInterface(
71-
installer.openSession(sessionId)
72-
)
85+
// openSession returns an IBinder for the session
86+
val openSessionMethod = installerClass.getMethod("openSession", Int::class.javaPrimitiveType)
87+
val sessionBinder = openSessionMethod.invoke(installer, sessionId)
88+
89+
// IPackageInstallerSession.Stub.asInterface(binder)
90+
val sessionStubClass = Class.forName("android.content.pm.IPackageInstallerSession\$Stub")
91+
val sessionAsInterface = sessionStubClass.getMethod("asInterface", IBinder::class.java)
92+
val session = sessionAsInterface.invoke(null, sessionBinder as IBinder)
93+
val sessionClass = session.javaClass
7394

7495
// Write APK to session
75-
val sizeBytes = file.length()
76-
val pfd = session.openWrite("base.apk", 0, sizeBytes)
96+
val openWrite = sessionClass.getMethod(
97+
"openWrite",
98+
String::class.java,
99+
Long::class.javaPrimitiveType,
100+
Long::class.javaPrimitiveType
101+
)
102+
val pfd = openWrite.invoke(session, "base.apk", 0L, file.length()) as ParcelFileDescriptor
77103
val output = ParcelFileDescriptor.AutoCloseOutputStream(pfd)
78104

79105
FileInputStream(file).use { input ->
@@ -83,23 +109,21 @@ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() {
83109
}
84110
}
85111

86-
// Commit session with a status receiver via LocalSocket
112+
// Set up LocalSocket for synchronous result callback
87113
val resultCode = AtomicInteger(STATUS_FAILURE_TIMEOUT)
88114
val latch = CountDownLatch(1)
89-
90115
val socketName = "shizuku_install_${SystemClock.elapsedRealtimeNanos()}"
91116
val serverSocket = LocalServerSocket(socketName)
92117

93-
// Use a thread to listen for the result
94118
val listenerThread = Thread {
95119
try {
96120
val client = serverSocket.accept()
97-
val input = DataInputStream(client.inputStream)
98-
val status = input.readInt()
121+
val dis = DataInputStream(client.inputStream)
122+
val status = dis.readInt()
99123
resultCode.set(mapInstallStatus(status))
100-
input.close()
124+
dis.close()
101125
client.close()
102-
} catch (e: Exception) {
126+
} catch (_: Exception) {
103127
resultCode.set(STATUS_FAILURE)
104128
} finally {
105129
try { serverSocket.close() } catch (_: Exception) {}
@@ -109,11 +133,16 @@ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() {
109133
listenerThread.isDaemon = true
110134
listenerThread.start()
111135

112-
// Create an IntentSender using a LocalSocket-based approach
113136
val statusReceiver = createStatusReceiver(socketName)
114-
session.commit(statusReceiver, false)
115137

116-
// Wait for result
138+
// session.commit(intentSender, false)
139+
val commitMethod = sessionClass.getMethod(
140+
"commit",
141+
IntentSender::class.java,
142+
Boolean::class.javaPrimitiveType
143+
)
144+
commitMethod.invoke(session, statusReceiver, false)
145+
117146
if (!latch.await(INSTALL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
118147
resultCode.set(STATUS_FAILURE_TIMEOUT)
119148
try { serverSocket.close() } catch (_: Exception) {}
@@ -127,23 +156,23 @@ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() {
127156

128157
override fun uninstallPackage(packageName: String): Int {
129158
return try {
130-
val installer = getPackageInstaller()
159+
val installer = getPackageInstallerBinder()
160+
val installerClass = installer.javaClass
131161

132162
val resultCode = AtomicInteger(STATUS_FAILURE_TIMEOUT)
133163
val latch = CountDownLatch(1)
134-
135164
val socketName = "shizuku_uninstall_${SystemClock.elapsedRealtimeNanos()}"
136165
val serverSocket = LocalServerSocket(socketName)
137166

138167
val listenerThread = Thread {
139168
try {
140169
val client = serverSocket.accept()
141-
val input = DataInputStream(client.inputStream)
142-
val status = input.readInt()
170+
val dis = DataInputStream(client.inputStream)
171+
val status = dis.readInt()
143172
resultCode.set(mapInstallStatus(status))
144-
input.close()
173+
dis.close()
145174
client.close()
146-
} catch (e: Exception) {
175+
} catch (_: Exception) {
147176
resultCode.set(STATUS_FAILURE)
148177
} finally {
149178
try { serverSocket.close() } catch (_: Exception) {}
@@ -154,14 +183,9 @@ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() {
154183
listenerThread.start()
155184

156185
val statusReceiver = createStatusReceiver(socketName)
157-
val versionedPackage = VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST)
158-
installer.uninstall(
159-
versionedPackage,
160-
"com.android.shell",
161-
0,
162-
statusReceiver,
163-
0
164-
)
186+
187+
// Try uninstall via reflection — signature varies by Android version
188+
performUninstall(installer, installerClass, packageName, statusReceiver)
165189

166190
if (!latch.await(UNINSTALL_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
167191
resultCode.set(STATUS_FAILURE_TIMEOUT)
@@ -175,44 +199,139 @@ class ShizukuInstallerServiceImpl() : IShizukuInstallerService.Stub() {
175199
}
176200

177201
/**
178-
* Creates an IntentSender that reports the install/uninstall status
179-
* back to the given local socket. This is the standard approach for
180-
* getting synchronous results from PackageInstaller in a Shizuku UserService.
202+
* Calls IPackageInstaller.createSession with the correct signature for the
203+
* current Android version. Tries multiple overloads.
181204
*/
182-
private fun createStatusReceiver(socketName: String): android.content.IntentSender {
183-
// Use a Binder-based callback approach since we're in a privileged process.
184-
// We create a lightweight Intent with an IIntentSender that writes the result
185-
// to a LocalSocket.
186-
val binder = object : android.content.IIntentSender.Stub() {
187-
override fun send(
188-
code: Int,
189-
intent: android.content.Intent?,
190-
resolvedType: String?,
191-
whitelistToken: IBinder?,
192-
finishedReceiver: android.content.IIntentReceiver?,
193-
requiredPermission: String?,
194-
options: android.os.Bundle?
195-
) {
196-
val status = intent?.getIntExtra(
197-
PackageInstaller.EXTRA_STATUS,
198-
PackageInstaller.STATUS_FAILURE
199-
) ?: PackageInstaller.STATUS_FAILURE
205+
private fun createSession(
206+
installer: Any,
207+
installerClass: Class<*>,
208+
params: PackageInstaller.SessionParams
209+
): Int {
210+
val callerPackage = "com.android.shell"
211+
val uid = android.os.Process.myUid()
200212

201-
try {
202-
val socket = LocalSocket()
203-
socket.connect(LocalSocketAddress(socketName, LocalSocketAddress.Namespace.ABSTRACT))
204-
val output = DataOutputStream(socket.outputStream)
205-
output.writeInt(status)
206-
output.flush()
207-
output.close()
208-
socket.close()
209-
} catch (_: Exception) {
210-
// Socket may already be closed
213+
// API 33+: createSession(SessionParams, String, String, int)
214+
try {
215+
val method = installerClass.getMethod(
216+
"createSession",
217+
PackageInstaller.SessionParams::class.java,
218+
String::class.java,
219+
String::class.java,
220+
Int::class.javaPrimitiveType
221+
)
222+
return method.invoke(installer, params, callerPackage, null, uid) as Int
223+
} catch (_: NoSuchMethodException) {}
224+
225+
// API 26-32: createSession(SessionParams, String, String)
226+
try {
227+
val method = installerClass.getMethod(
228+
"createSession",
229+
PackageInstaller.SessionParams::class.java,
230+
String::class.java,
231+
String::class.java
232+
)
233+
return method.invoke(installer, params, callerPackage, null) as Int
234+
} catch (_: NoSuchMethodException) {}
235+
236+
throw IllegalStateException("Could not find createSession method")
237+
}
238+
239+
/**
240+
* Calls IPackageInstaller.uninstall with the correct signature for the
241+
* current Android version. Tries multiple overloads.
242+
*/
243+
private fun performUninstall(
244+
installer: Any,
245+
installerClass: Class<*>,
246+
packageName: String,
247+
statusReceiver: IntentSender
248+
) {
249+
val versionedPackageClass = Class.forName("android.content.pm.VersionedPackage")
250+
val versionedPackage = versionedPackageClass
251+
.getConstructor(String::class.java, Int::class.javaPrimitiveType)
252+
.newInstance(packageName, -1) // VERSION_CODE_HIGHEST = -1
253+
254+
val callerPackage = "com.android.shell"
255+
256+
// API 33+: uninstall(VersionedPackage, String, int, IntentSender, int)
257+
try {
258+
val method = installerClass.getMethod(
259+
"uninstall",
260+
versionedPackageClass,
261+
String::class.java,
262+
Int::class.javaPrimitiveType,
263+
IntentSender::class.java,
264+
Int::class.javaPrimitiveType
265+
)
266+
method.invoke(installer, versionedPackage, callerPackage, 0, statusReceiver, 0)
267+
return
268+
} catch (_: NoSuchMethodException) {}
269+
270+
// API 26-32: uninstall(VersionedPackage, String, int, IntentSender)
271+
try {
272+
val method = installerClass.getMethod(
273+
"uninstall",
274+
versionedPackageClass,
275+
String::class.java,
276+
Int::class.javaPrimitiveType,
277+
IntentSender::class.java
278+
)
279+
method.invoke(installer, versionedPackage, callerPackage, 0, statusReceiver)
280+
return
281+
} catch (_: NoSuchMethodException) {}
282+
283+
throw IllegalStateException("Could not find uninstall method")
284+
}
285+
286+
/**
287+
* Creates an IntentSender that reports install/uninstall status back
288+
* through a LocalSocket. Uses reflection to construct IntentSender
289+
* from an IIntentSender proxy, since these are hidden APIs.
290+
*/
291+
private fun createStatusReceiver(socketName: String): IntentSender {
292+
val iIntentSenderClass = Class.forName("android.content.IIntentSender")
293+
294+
// Create a dynamic proxy for IIntentSender that writes result to LocalSocket
295+
val proxy = Proxy.newProxyInstance(
296+
iIntentSenderClass.classLoader,
297+
arrayOf(iIntentSenderClass)
298+
) { _, method, args ->
299+
when (method.name) {
300+
"send" -> {
301+
// Extract the Intent argument (second param in all known signatures)
302+
val intent = args?.filterIsInstance<Intent>()?.firstOrNull()
303+
val status = intent?.getIntExtra(
304+
PackageInstaller.EXTRA_STATUS,
305+
PackageInstaller.STATUS_FAILURE
306+
) ?: PackageInstaller.STATUS_FAILURE
307+
308+
try {
309+
val socket = LocalSocket()
310+
socket.connect(
311+
LocalSocketAddress(socketName, LocalSocketAddress.Namespace.ABSTRACT)
312+
)
313+
val dos = DataOutputStream(socket.outputStream)
314+
dos.writeInt(status)
315+
dos.flush()
316+
dos.close()
317+
socket.close()
318+
} catch (_: Exception) {}
319+
320+
// Return 0 if method returns int, null otherwise
321+
if (method.returnType == Int::class.javaPrimitiveType) 0 else null
322+
}
323+
"asBinder" -> {
324+
// Return the proxy itself as a binder stand-in
325+
null
211326
}
327+
else -> null
212328
}
213329
}
214330

215-
return android.content.IntentSender(binder)
331+
// IntentSender(IIntentSender) — hidden constructor, use reflection
332+
val constructor = IntentSender::class.java.getDeclaredConstructor(iIntentSenderClass)
333+
constructor.isAccessible = true
334+
return constructor.newInstance(proxy)
216335
}
217336

218337
private fun mapInstallStatus(status: Int): Int {

core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/shizuku/ShizukuServiceManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ class ShizukuServiceManager(
189189

190190
continuation.invokeOnCancellation {
191191
try {
192-
Shizuku.unbindUserService(args, connection)
192+
Shizuku.unbindUserService(args, connection, true)
193193
} catch (_: Exception) {}
194194
}
195195
}

0 commit comments

Comments
 (0)