@@ -3,6 +3,7 @@ package com.yureitzk.nophotopickerapi
33import android.app.Activity
44import android.content.Intent
55import android.os.Build
6+ import android.os.ext.SdkExtensions
67import android.provider.MediaStore
78import de.robv.android.xposed.*
89import de.robv.android.xposed.callbacks.XC_LoadPackage
@@ -11,227 +12,67 @@ class MainHook : IXposedHookLoadPackage {
1112
1213 companion object {
1314 private const val TAG = " NoPhotoPicker"
14- private var isSystemFramework = false
1515 }
1616
17- override fun handleLoadPackage (lpparam : XC_LoadPackage .LoadPackageParam ) {
18- when (lpparam.packageName) {
19- " android" -> {
20- isSystemFramework = true
21- XposedBridge .log(" $TAG : Hooking system framework" )
17+ fun XC_LoadPackage.LoadPackageParam.isSystemFramework (): Boolean {
18+ return packageName == " android" || appInfo == null
19+ }
2220
23- // Try multiple system hooks for better compatibility
21+ override fun handleLoadPackage (lpparam : XC_LoadPackage .LoadPackageParam ) {
22+ when {
23+ lpparam.isSystemFramework() -> {
2424 hookSystemServices(lpparam)
25- XposedBridge .log(" $TAG : System framework hooks completed" )
2625 }
27- else -> {
28- // App-specific hooks for compatibility
29- if (! isSystemFramework) {
30- hookActivity(lpparam)
31- hookActivityResult(lpparam)
32- }
26+ lpparam.packageName != null -> {
27+ hookActivity(lpparam)
28+ hookActivityResult(lpparam)
3329 }
3430 }
3531 }
3632
3733 private fun hookSystemServices (lpparam : XC_LoadPackage .LoadPackageParam ) {
38- // Try multiple system service hooks for different Android versions
39-
40- // Hook ActivityTaskManagerService
41- try {
42- hookActivityTaskManager(lpparam)
43- } catch (t: Throwable ) {
44- XposedBridge .log(" $TAG : Failed to hook ActivityTaskManagerService: ${t.message} " )
45- }
46-
47- // Hook ActivityManagerService
48- try {
49- hookActivityManagerService(lpparam)
50- } catch (t: Throwable ) {
51- XposedBridge .log(" $TAG : Failed to hook ActivityManagerService: ${t.message} " )
52- }
53-
54- // Hook ActivityStarter
55- try {
56- hookActivityStarter(lpparam)
57- } catch (t: Throwable ) {
58- XposedBridge .log(" $TAG : Failed to hook ActivityStarter: ${t.message} " )
59- }
60-
61- // Hook PackageManagerService
62- try {
63- hookPackageManagerService(lpparam)
64- } catch (t: Throwable ) {
65- XposedBridge .log(" $TAG : Failed to hook PackageManagerService: ${t.message} " )
66- }
67- }
68-
69- private fun hookActivityTaskManager (lpparam : XC_LoadPackage .LoadPackageParam ) {
70- try {
71- val classLoader = lpparam.classLoader
72-
73- // Try different class names for different Android versions
74- val activityTaskManagerClass = XposedHelpers .findClassIfExists(
75- " com.android.server.wm.ActivityTaskManagerService" ,
76- classLoader
77- ) ? : XposedHelpers .findClassIfExists(
78- " android.app.ActivityTaskManager" ,
79- classLoader
80- ) ? : return
81-
82- XposedBridge .hookAllMethods(
83- activityTaskManagerClass,
84- " startActivity" ,
85- object : XC_MethodHook () {
86- override fun beforeHookedMethod (param : MethodHookParam ) {
87- try {
88- // Find Intent in arguments (position varies by API)
89- for (i in param.args.indices) {
90- if (param.args[i] is Intent ) {
91- val intent = param.args[i] as Intent
92- if (isPhotoPickerIntent(intent)) {
93- logIntentDetails(intent, " ActivityTaskManagerService.startActivity" )
94- param.args[i] = buildDocumentPickerIntent(intent)
95- return
96- }
97- }
98- }
99- } catch (e: Throwable ) {
100- XposedBridge .log(" $TAG : Error in ActivityTaskManagerService hook: ${e.message} " )
101- }
102- }
103- }
104- )
105-
106- XposedBridge .log(" $TAG : Successfully hooked ActivityTaskManagerService" )
107- } catch (t: Throwable ) {
108- throw t
109- }
110- }
111-
112- private fun hookActivityManagerService (lpparam : XC_LoadPackage .LoadPackageParam ) {
113- try {
114- val classLoader = lpparam.classLoader
115- val activityManagerClass = XposedHelpers .findClassIfExists(
116- " com.android.server.am.ActivityManagerService" ,
117- classLoader
118- ) ? : return
119-
120- XposedBridge .hookAllMethods(
121- activityManagerClass,
122- " startActivity" ,
123- object : XC_MethodHook () {
124- override fun beforeHookedMethod (param : MethodHookParam ) {
125- try {
126- val args = param.args
127- for (i in args.indices) {
128- if (args[i] is Intent ) {
129- val intent = args[i] as Intent
130- if (isPhotoPickerIntent(intent)) {
131- logIntentDetails(intent, " ActivityManagerService.startActivity" )
132- args[i] = buildDocumentPickerIntent(intent)
133- return
134- }
135- }
136- }
137- } catch (e: Throwable ) {
138- XposedBridge .log(" $TAG : Error in ActivityManagerService hook: ${e.message} " )
139- }
140- }
141- }
142- )
143-
144- XposedBridge .log(" $TAG : Successfully hooked ActivityManagerService" )
145- } catch (t: Throwable ) {
146- throw t
34+ // Try to find which service class exists
35+ val classLoader = lpparam.classLoader
36+ val serviceClasses = listOf (
37+ " com.android.server.wm.ActivityTaskManagerService" ,
38+ " com.android.server.am.ActivityManagerService" ,
39+ " com.android.server.am.ActivityStarter"
40+ )
41+
42+ for (className in serviceClasses) {
43+ val serviceClass = XposedHelpers .findClassIfExists(className, classLoader)
44+ if (serviceClass != null ) {
45+ XposedBridge .hookAllMethods(
46+ serviceClass,
47+ " startActivity" ,
48+ createIntentInterceptor(" System:$className " )
49+ )
50+ XposedBridge .log(" $TAG : Hooked $className " )
51+ return
52+ }
14753 }
14854 }
14955
150- private fun hookActivityStarter (lpparam : XC_LoadPackage .LoadPackageParam ) {
151- try {
152- val classLoader = lpparam.classLoader
153- val activityStarterClass = XposedHelpers .findClassIfExists(
154- " com.android.server.wm.ActivityStarter" ,
155- classLoader
156- ) ? : return
157-
158- XposedBridge .hookAllMethods(
159- activityStarterClass,
160- " execute" ,
161- object : XC_MethodHook () {
162- override fun beforeHookedMethod (param : MethodHookParam ) {
163- try {
164- // ActivityStarter holds intent in mRequest field
165- val intentField = XposedHelpers .getObjectField(param.thisObject, " mIntent" )
166- if (intentField is Intent ) {
167- if (isPhotoPickerIntent(intentField)) {
168- logIntentDetails(intentField, " ActivityStarter.execute" )
169- XposedHelpers .setObjectField(
170- param.thisObject,
171- " mIntent" ,
172- buildDocumentPickerIntent(intentField)
173- )
174- }
175- }
176- } catch (e: Throwable ) {
177- // Ignore - field might not exist or have different name
56+ private fun createIntentInterceptor (source : String ): XC_MethodHook {
57+ return object : XC_MethodHook () {
58+ override fun beforeHookedMethod (param : MethodHookParam ) {
59+ val args = param.args
60+ for (i in args.indices) {
61+ if (args[i] is Intent ) {
62+ val intent = args[i] as Intent
63+ if (isPhotoPickerIntent(intent)) {
64+ logIntentDetails(intent, " $source .startActivity" )
65+ args[i] = buildDocumentPickerIntent(intent)
66+ return
17867 }
17968 }
18069 }
181- )
182-
183- XposedBridge .log(" $TAG : Successfully hooked ActivityStarter" )
184- } catch (t: Throwable ) {
185- throw t
186- }
187- }
188-
189- private fun hookPackageManagerService (lpparam : XC_LoadPackage .LoadPackageParam ) {
190- try {
191- val classLoader = lpparam.classLoader
192-
193- // Try different class names
194- val pmsClassNames = arrayOf(
195- " com.android.server.pm.PackageManagerService" ,
196- " com.android.server.pm.PackageManagerService\$ IPackageManagerImpl" ,
197- " android.content.pm.IPackageManager"
198- )
199-
200- var pmsClass: Class <* >? = null
201- for (className in pmsClassNames) {
202- pmsClass = XposedHelpers .findClassIfExists(className, classLoader)
203- if (pmsClass != null ) break
20470 }
205-
206- if (pmsClass != null ) {
207- XposedBridge .hookAllMethods(
208- pmsClass,
209- " resolveIntent" ,
210- object : XC_MethodHook () {
211- override fun beforeHookedMethod (param : MethodHookParam ) {
212- try {
213- val intent = param.args.getOrNull(0 ) as ? Intent ? : return
214- if (isPhotoPickerIntent(intent)) {
215- logIntentDetails(intent, " PackageManagerService.resolveIntent" )
216- param.args[0 ] = buildDocumentPickerIntent(intent)
217- }
218- } catch (e: Throwable ) {
219- // Ignore
220- }
221- }
222- }
223- )
224-
225- XposedBridge .log(" $TAG : Successfully hooked PackageManagerService" )
226- }
227- } catch (t: Throwable ) {
228- throw t
22971 }
23072 }
23173
23274 private fun hookActivity (lpparam : XC_LoadPackage .LoadPackageParam ) {
23375 try {
234- // Hook Activity.startActivity
23576 XposedHelpers .findAndHookMethod(
23677 Activity ::class .java,
23778 " startActivity" ,
@@ -247,7 +88,6 @@ class MainHook : IXposedHookLoadPackage {
24788 }
24889 )
24990
250- // Hook Activity.startActivityForResult
25191 XposedHelpers .findAndHookMethod(
25292 Activity ::class .java,
25393 " startActivityForResult" ,
@@ -280,20 +120,27 @@ class MainHook : IXposedHookLoadPackage {
280120 Intent ::class .java,
281121 object : XC_MethodHook () {
282122 override fun beforeHookedMethod (param : MethodHookParam ) {
123+ val requestCode = param.args[0 ] as Int
283124 val resultCode = param.args[1 ] as Int
284125 val data = param.args[2 ] as ? Intent
285126
286- if (resultCode == Activity .RESULT_OK && data != null ) {
287- val hasData = data.data != null
288- val hasClipData = data.clipData != null
127+ // Skip if canceled or no data
128+ if (resultCode != Activity .RESULT_OK || data == null ) return
129+
130+ val hasContent = when {
131+ data.data != null -> true
132+ data.clipData?.let { clipData ->
133+ (0 until clipData.itemCount).any { clipData.getItemAt(it).uri != null }
134+ } ? : false -> true
135+ data.hasExtra(Intent .EXTRA_STREAM ) -> true
136+ data.hasExtra(Intent .EXTRA_CONTENT_ANNOTATIONS ) -> true
137+ else -> false
138+ }
289139
290- if (! hasData && ! hasClipData) {
291- XposedBridge .log(" $TAG : Empty result detected, changing to RESULT_CANCELED" )
292- param.args[1 ] = Activity .RESULT_CANCELED
293- param.args[2 ] = null
294- } else {
295- XposedBridge .log(" $TAG : Valid result - hasData: $hasData , hasClipData: $hasClipData " )
296- }
140+ if (! hasContent) {
141+ XposedBridge .log(" $TAG : Empty result detected for request $requestCode " )
142+ param.args[1 ] = Activity .RESULT_CANCELED
143+ param.args[2 ] = null
297144 }
298145 }
299146 }
@@ -303,19 +150,32 @@ class MainHook : IXposedHookLoadPackage {
303150 }
304151 }
305152
153+ private fun getMaxItems (intent : Intent ): Int {
154+ return if (
155+ Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ||
156+ SdkExtensions .getExtensionVersion(Build .VERSION_CODES .R ) >= 2
157+ ) {
158+ intent.getIntExtra(MediaStore .EXTRA_PICK_IMAGES_MAX , - 1 )
159+ } else {
160+ - 1
161+ }
162+ }
163+
306164 private fun isPhotoPickerIntent (intent : Intent ): Boolean {
307- return intent.action == MediaStore .ACTION_PICK_IMAGES ||
308- intent.action == " androidx.activity.result.contract.action.PickVisualMedia" ||
309- intent.component?.className?.contains(" PhotoPicker" , true ) == true ||
310- intent.component?.className?.contains(" MediaPicker" , true ) == true ||
311- intent.hasExtra(" android.provider.extra.PICK_IMAGES" ) ||
312- intent.hasExtra(" androidx.activity.result.contract.extra.PICK_VISUAL_MEDIA_ENABLE_PHOTO_PICKER" )
165+ return when {
166+ Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ->
167+ intent.action == MediaStore .ACTION_PICK_IMAGES
168+ SdkExtensions .getExtensionVersion(Build .VERSION_CODES .R ) >= 2 ->
169+ intent.action == MediaStore .ACTION_PICK_IMAGES ||
170+ intent.action == " androidx.activity.result.contract.action.PickVisualMedia"
171+
172+ else -> false
173+ }
313174 }
314175
315176 private fun logIntentDetails (intent : Intent , source : String ) {
316177 XposedBridge .log(" $TAG : [$source ] Photo picker detected" )
317178 XposedBridge .log(" Action: ${intent.action} " )
318- XposedBridge .log(" Component: ${intent.component} " )
319179 }
320180
321181 private fun buildDocumentPickerIntent (original : Intent ): Intent {
@@ -335,13 +195,11 @@ class MainHook : IXposedHookLoadPackage {
335195
336196 // Handle multi-select with API compatibility
337197 val allowMultiple = original.getBooleanExtra(Intent .EXTRA_ALLOW_MULTIPLE , false )
338- val maxItems = original.getIntExtra( MediaStore . EXTRA_PICK_IMAGES_MAX , - 1 )
198+ val maxItems = getMaxItems(original )
339199
340200 val shouldAllowMultiple = when {
341201 allowMultiple -> true
342202 maxItems > 1 -> true
343- // API 33+ check without calling the method on older APIs
344- Build .VERSION .SDK_INT >= 33 && maxItems >= 2 -> true
345203 else -> false
346204 }
347205
@@ -354,4 +212,4 @@ class MainHook : IXposedHookLoadPackage {
354212 XposedBridge .log(" $TAG : Created document picker intent" )
355213 }
356214 }
357- }
215+ }
0 commit comments