From 3f8ede89680d45546fce5cf969653a8b7f107622 Mon Sep 17 00:00:00 2001 From: alex5402 Date: Sat, 31 Jan 2026 11:48:11 +0530 Subject: [PATCH] feat: Implement legacy storage permission handling for Android 10 and below, and improve compatibility by fixing service hook and attribution source issues. --- .../fake/hook/ClassInvocationStub.java | 4 ++ .../blackbox/fake/hook/HookManager.java | 4 +- .../context/providers/SystemProviderStub.java | 13 +++-- app/src/main/AndroidManifest.xml | 6 +++ .../blackboxa/view/main/MainActivity.kt | 53 +++++++++++++++++++ build.gradle | 2 +- 6 files changed, 76 insertions(+), 6 deletions(-) diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/hook/ClassInvocationStub.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/hook/ClassInvocationStub.java index 1aaf8aa..a5da868 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/hook/ClassInvocationStub.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/hook/ClassInvocationStub.java @@ -50,6 +50,10 @@ public abstract class ClassInvocationStub implements InvocationHandler, IInjectH @Override public void injectHook() { mBase = getWho(); + // Skip hook if service doesn't exist on this Android version + if (mBase == null) { + return; + } mProxyInvocation = Proxy.newProxyInstance(mBase.getClass().getClassLoader(), MethodParameterUtils.getAllInterface(mBase.getClass()), this); if (!onlyProxy) { inject(mBase, mProxyInvocation); diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/hook/HookManager.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/hook/HookManager.java index 8651216..67eb09f 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/hook/HookManager.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/hook/HookManager.java @@ -91,7 +91,7 @@ import top.niunaijun.blackbox.utils.compat.BuildCompat; import top.niunaijun.blackbox.fake.service.ISettingsProviderProxy; import top.niunaijun.blackbox.fake.service.FeatureFlagUtilsProxy; import top.niunaijun.blackbox.fake.service.WorkManagerProxy; -import top.niunaijun.blackbox.fake.service.IInputMethodManagerProxy; +// Removed IInputMethodManagerProxy - causes ClassCastException on Android 15 /** * updated by alex5402 on 3/30/21. @@ -177,7 +177,7 @@ public class HookManager { addInjector(new IMediaRouterServiceProxy()); addInjector(new IPowerManagerProxy()); addInjector(new IContextHubServiceProxy()); - addInjector(new IInputMethodManagerProxy()); + // Removed IInputMethodManagerProxy - causes ClassCastException on Android 15 due to queryLocalInterface returning non-IInterface proxy addInjector(new IVibratorServiceProxy()); addInjector(new IPersistentDataBlockServiceProxy()); addInjector(AppInstrumentation.get()); diff --git a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/context/providers/SystemProviderStub.java b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/context/providers/SystemProviderStub.java index d71531e..6492d70 100644 --- a/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/context/providers/SystemProviderStub.java +++ b/Bcore/src/main/java/top/niunaijun/blackbox/fake/service/context/providers/SystemProviderStub.java @@ -60,9 +60,12 @@ public class SystemProviderStub extends ClassInvocationStub implements BContentP if ("call".equals(methodName)) { // Only fix AttributionSource in call() args, don't replace method name if (args != null) { + Class attributionSourceClass = BRAttributionSource.getRealClass(); for (int i = 0; i < args.length; i++) { Object arg = args[i]; - if (arg != null && arg.getClass().getName().equals(BRAttributionSource.getRealClass().getName())) { + // Check for null - AttributionSource doesn't exist on Android < S (31) + if (arg != null && attributionSourceClass != null && + arg.getClass().getName().equals(attributionSourceClass.getName())) { ContextCompat.fixAttributionSourceState(arg, BlackBoxCore.getHostUid()); } } @@ -79,8 +82,12 @@ public class SystemProviderStub extends ClassInvocationStub implements BContentP if (!isSystemProviderAuthority(authority)) { args[0] = BlackBoxCore.getHostPkg(); } - } else if (arg != null && arg.getClass().getName().equals(BRAttributionSource.getRealClass().getName())) { - ContextCompat.fixAttributionSourceState(arg, BlackBoxCore.getHostUid()); + } else if (arg != null) { + Class attrSourceClass = BRAttributionSource.getRealClass(); + // Check for null - AttributionSource doesn't exist on Android < S (31) + if (attrSourceClass != null && arg.getClass().getName().equals(attrSourceClass.getName())) { + ContextCompat.fixAttributionSourceState(arg, BlackBoxCore.getHostUid()); + } } } return method.invoke(mBase, args); diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3fcf9bf..80b647d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,12 @@ + + + + + = android.os.Build.VERSION_CODES.R) { + // Android 11+: Need MANAGE_EXTERNAL_STORAGE if (!android.os.Environment.isExternalStorageManager()) { Log.w(TAG, "MANAGE_EXTERNAL_STORAGE permission not granted") showStoragePermissionDialog() } + } else { + // Android 10 and below: Need READ/WRITE_EXTERNAL_STORAGE + if (androidx.core.content.ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE + ) != android.content.pm.PackageManager.PERMISSION_GRANTED || + androidx.core.content.ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.READ_EXTERNAL_STORAGE + ) != android.content.pm.PackageManager.PERMISSION_GRANTED + ) { + Log.w( + TAG, + "Storage permissions not granted on Android ${android.os.Build.VERSION.SDK_INT}" + ) + requestLegacyStoragePermission() + } } } catch (e: Exception) { Log.e(TAG, "Error checking storage permission: ${e.message}") } } + private fun requestLegacyStoragePermission() { + try { + androidx.core.app.ActivityCompat.requestPermissions( + this, + arrayOf( + android.Manifest.permission.READ_EXTERNAL_STORAGE, + android.Manifest.permission.WRITE_EXTERNAL_STORAGE + ), + STORAGE_PERMISSION_REQUEST_CODE + ) + } catch (e: Exception) { + Log.e(TAG, "Error requesting storage permission: ${e.message}") + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == STORAGE_PERMISSION_REQUEST_CODE) { + if (grantResults.isNotEmpty() && + grantResults.all { + it == android.content.pm.PackageManager.PERMISSION_GRANTED + } + ) { + Log.d(TAG, "Storage permissions granted") + } else { + Log.w(TAG, "Storage permissions denied") + } + } + } + private fun showStoragePermissionDialog() { try { MaterialDialog(this).show { diff --git a/build.gradle b/build.gradle index 9cad877..f42bd9e 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ ext { // targetSdkVersion = 35 minSdk = 21 versionCode = 1 - versionName = "3.0.13-beta2" + versionName = "3.1.0-beta" xVersion = '1.1.0' hiddenApiBypass = '4.3' } \ No newline at end of file