From 4eb98a2731132c8ca7428f16b7e0d68a0acc9376 Mon Sep 17 00:00:00 2001 From: aequilibrium Date: Wed, 3 Dec 2025 12:40:39 +0100 Subject: [PATCH] Manifest a reality in which you access Android/obb --- .../android/files/provider/linux/LinuxPath.kt | 49 +++-------------- .../provider/root/LibSuFileServiceLauncher.kt | 27 ++++++++-- .../files/provider/root/RootablePath.kt | 54 ++++++++++++++----- 3 files changed, 71 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxPath.kt b/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxPath.kt index a51b24e62..96634c7b6 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxPath.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/linux/LinuxPath.kt @@ -8,6 +8,7 @@ package me.zhanghai.android.files.provider.linux import android.os.Build import android.os.Parcel import android.os.Parcelable +import android.util.Log import java8.nio.file.LinkOption import java8.nio.file.Path import java8.nio.file.ProviderMismatchException @@ -78,48 +79,10 @@ internal class LinuxPath : ByteStringListPath, RootablePath { } override fun isRootRequired(isAttributeAccess: Boolean): Boolean { - val file = toFile() - return StorageVolumeListLiveData.valueCompat.none { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && !it.isPrimaryCompat) { - return@none false - } - val storageVolumeDirectory = it.pathFileCompat - if (!file.startsWith(storageVolumeDirectory)) { - return@none false - } - return@none file.isAccessibleInStorageVolume(storageVolumeDirectory, isAttributeAccess) - } - } - - private fun File.isAccessibleInStorageVolume( - storageVolumeDirectory: File, - isAttributeAccess: Boolean - ): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val parentDirectory = parentFile - val androidDataDirectory = storageVolumeDirectory.resolve(FILE_ANDROID_DATA) - val isInAndroidDataDirectory = if (isAttributeAccess && parentDirectory != null) { - parentDirectory.startsWith(androidDataDirectory) - } else { - startsWith(androidDataDirectory) - } - val appPackageName = application.packageName - if (isInAndroidDataDirectory) { - val appDataDirectory = androidDataDirectory.resolve(appPackageName) - return startsWith(appDataDirectory) - } - val androidObbDirectory = storageVolumeDirectory.resolve(FILE_ANDROID_OBB) - val isInAndroidObbDirectory = if (isAttributeAccess && parentDirectory != null) { - parentDirectory.startsWith(androidObbDirectory) - } else { - startsWith(androidObbDirectory) - } - if (isInAndroidObbDirectory) { - val appObbDirectory = androidObbDirectory.resolve(appPackageName) - return startsWith(appObbDirectory) - } - } - return true + // ALWAYS return false - let the system fail naturally + // The root fallback should only happen AFTER a real permission failure + Log.d("LinuxPath", "isRootRequired called for ${toFile().path}, attributeAccess=$isAttributeAccess - returning FALSE to try normal path first") + return false } private constructor(source: Parcel) : super(source) { @@ -146,4 +109,4 @@ internal class LinuxPath : ByteStringListPath, RootablePath { } val Path.isLinuxPath: Boolean - get() = this is LinuxPath + get() = this is LinuxPath \ No newline at end of file diff --git a/app/src/main/java/me/zhanghai/android/files/provider/root/LibSuFileServiceLauncher.kt b/app/src/main/java/me/zhanghai/android/files/provider/root/LibSuFileServiceLauncher.kt index f8931983c..a09161d4a 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/root/LibSuFileServiceLauncher.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/root/LibSuFileServiceLauncher.kt @@ -10,6 +10,7 @@ import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.IBinder +import android.util.Log import com.topjohnwu.superuser.NoShellException import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ipc.RootService @@ -30,6 +31,7 @@ import kotlin.coroutines.resumeWithException object LibSuFileServiceLauncher { private val lock = Any() + private const val TAG = "LibSuLauncher" init { Shell.enableVerboseLogging = true @@ -45,19 +47,26 @@ object LibSuFileServiceLauncher { // @see com.topjohnwu.superuser.Shell.rootAccess try { Runtime.getRuntime().exec("su --version") + Log.d(TAG, "su binary found on device") true } catch (e: IOException) { // java.io.IOException: Cannot run program "su": error=2, No such file or directory + Log.d(TAG, "No su binary found on device") false } @Throws(RemoteFileSystemException::class) fun launchService(): IRemoteFileService { + Log.d(TAG, "Attempting to launch root service") + synchronized(lock) { // libsu won't call back when su isn't available. if (!isSuAvailable()) { + Log.w(TAG, "Root isn't available - throwing exception") throw RemoteFileSystemException("Root isn't available") } + Log.d(TAG, "Root is available, proceeding with service launch") + return try { runBlocking { try { @@ -71,6 +80,7 @@ object LibSuFileServiceLauncher { Shell.getShell() continuation.resume(Unit) } catch (e: NoShellException) { + Log.w(TAG, "NoShellException: ${e.message}") continuation.resumeWithException( RemoteFileSystemException(e) ) @@ -84,12 +94,14 @@ object LibSuFileServiceLauncher { name: ComponentName, service: IBinder ) { + Log.d(TAG, "Root service connected successfully") val serviceInterface = IRemoteFileService.Stub.asInterface(service) continuation.resume(serviceInterface) } override fun onServiceDisconnected(name: ComponentName) { + Log.w(TAG, "Root service disconnected") if (continuation.isActive) { continuation.resumeWithException( RemoteFileSystemException( @@ -100,6 +112,7 @@ object LibSuFileServiceLauncher { } override fun onBindingDied(name: ComponentName) { + Log.w(TAG, "Root service binding died") if (continuation.isActive) { continuation.resumeWithException( RemoteFileSystemException("libsu binding died") @@ -108,6 +121,7 @@ object LibSuFileServiceLauncher { } override fun onNullBinding(name: ComponentName) { + Log.w(TAG, "Root service binding is null") if (continuation.isActive) { continuation.resumeWithException( RemoteFileSystemException("libsu binding is null") @@ -116,8 +130,10 @@ object LibSuFileServiceLauncher { } } launch(Dispatchers.Main.immediate) { + Log.d(TAG, "Binding to root service") RootService.bind(intent, connection) continuation.invokeOnCancellation { + Log.d(TAG, "Service binding cancelled, unbinding") launch(Dispatchers.Main.immediate) { RootService.unbind(connection) } @@ -126,10 +142,12 @@ object LibSuFileServiceLauncher { } } } catch (e: TimeoutCancellationException) { + Log.w(TAG, "Timeout while launching root service: ${e.message}") throw RemoteFileSystemException(e) } } } catch (e: InterruptedException) { + Log.w(TAG, "Interrupted while launching root service: ${e.message}") throw RemoteFileSystemException(e) } } @@ -144,9 +162,12 @@ private class LibSuShellInitializer : Shell.Initializer() { class LibSuFileService : RootService() { override fun onCreate() { super.onCreate() - + Log.d("LibSuFileService", "Root file service created") RootFileService.main() } - override fun onBind(intent: Intent): IBinder = RemoteFileServiceInterface() -} + override fun onBind(intent: Intent): IBinder { + Log.d("LibSuFileService", "Root file service bound") + return RemoteFileServiceInterface() + } +} \ No newline at end of file diff --git a/app/src/main/java/me/zhanghai/android/files/provider/root/RootablePath.kt b/app/src/main/java/me/zhanghai/android/files/provider/root/RootablePath.kt index be81ef844..0eaff6a27 100644 --- a/app/src/main/java/me/zhanghai/android/files/provider/root/RootablePath.kt +++ b/app/src/main/java/me/zhanghai/android/files/provider/root/RootablePath.kt @@ -27,12 +27,24 @@ fun callRootable( path as? RootablePath ?: throw IllegalArgumentException("$path is not a RootablePath") return when (rootStrategy) { RootStrategy.NEVER -> localObject.block() - RootStrategy.AUTOMATIC -> - if (path.isRootRequired(isAttributeAccess)) { - rootObject.block() - } else { + RootStrategy.AUTOMATIC -> { + // ALWAYS try local first, only use root if local fails with permission error + try { localObject.block() + } catch (e: IOException) { + // Only retry with root for permission-related errors + if (isPermissionError(e)) { + try { + rootObject.block() + } catch (rootE: IOException) { + // If root also fails, throw the original local error + throw e + } + } else { + throw e + } } + } RootStrategy.ALWAYS -> rootObject.block() } } @@ -49,16 +61,32 @@ fun callRootable( path1 as? RootablePath ?: throw IllegalArgumentException("$path1 is not a RootablePath") path2 as? RootablePath ?: throw IllegalArgumentException("$path2 is not a RootablePath") return when (rootStrategy) { - RootStrategy.NEVER -> - localObject.block() - RootStrategy.AUTOMATIC -> - if (path1.isRootRequired(isAttributeAccess) - || path2.isRootRequired(isAttributeAccess)) { - rootObject.block() - } else { + RootStrategy.NEVER -> localObject.block() + RootStrategy.AUTOMATIC -> { + try { localObject.block() + } catch (e: IOException) { + if (isPermissionError(e)) { + try { + rootObject.block() + } catch (rootE: IOException) { + throw e + } + } else { + throw e + } } - RootStrategy.ALWAYS -> - rootObject.block() + } + RootStrategy.ALWAYS -> rootObject.block() } } + +/** + * Minimal permission error detection + */ +private fun isPermissionError(e: IOException): Boolean { + val message = e.message ?: "" + return e is java8.nio.file.AccessDeniedException || + message.contains("permission", ignoreCase = true) || + message.contains("denied", ignoreCase = true) +} \ No newline at end of file