11package 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
55import android.content.pm.PackageInstaller
6- import android.content.pm.PackageManager
7- import android.content.pm.VersionedPackage
86import android.net.LocalServerSocket
97import android.net.LocalSocket
108import android.net.LocalSocketAddress
9+ import android.os.Bundle
1110import android.os.IBinder
1211import android.os.ParcelFileDescriptor
1312import android.os.SystemClock
@@ -16,6 +15,8 @@ import java.io.DataInputStream
1615import java.io.DataOutputStream
1716import java.io.File
1817import java.io.FileInputStream
18+ import java.lang.reflect.InvocationHandler
19+ import java.lang.reflect.Proxy
1920import java.util.concurrent.CountDownLatch
2021import java.util.concurrent.TimeUnit
2122import 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 */
3236class 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 {
0 commit comments