diff --git a/app-common-io/src/main/aidl/eu/darken/sdmse/common/files/local/ipc/FileOpsConnection.aidl b/app-common-io/src/main/aidl/eu/darken/sdmse/common/files/local/ipc/FileOpsConnection.aidl index 508d618c6..4749c847f 100644 --- a/app-common-io/src/main/aidl/eu/darken/sdmse/common/files/local/ipc/FileOpsConnection.aidl +++ b/app-common-io/src/main/aidl/eu/darken/sdmse/common/files/local/ipc/FileOpsConnection.aidl @@ -1,5 +1,6 @@ package eu.darken.sdmse.common.files.local.ipc; +import eu.darken.sdmse.common.ipc.RemoteFileHandle; import eu.darken.sdmse.common.ipc.RemoteInputStream; import eu.darken.sdmse.common.ipc.RemoteOutputStream; import eu.darken.sdmse.common.files.local.LocalPath; @@ -10,8 +11,7 @@ import eu.darken.sdmse.common.files.Permissions; interface FileOpsConnection { - RemoteInputStream readFile(in LocalPath path); - RemoteOutputStream writeFile(in LocalPath path); + RemoteFileHandle file(in LocalPath path, boolean readWrite); boolean mkdirs(in LocalPath path); boolean createNewFile(in LocalPath path); diff --git a/app-common-io/src/main/aidl/eu/darken/sdmse/common/ipc/RemoteFileHandle.aidl b/app-common-io/src/main/aidl/eu/darken/sdmse/common/ipc/RemoteFileHandle.aidl new file mode 100644 index 000000000..11555e46f --- /dev/null +++ b/app-common-io/src/main/aidl/eu/darken/sdmse/common/ipc/RemoteFileHandle.aidl @@ -0,0 +1,12 @@ +package eu.darken.sdmse.common.ipc; + +interface RemoteFileHandle { + boolean readWrite(); + // We need inout for the buffer, as FileHandle will pass partially filled buffers that need to stay partially filled when returned + int read(long fileOffset, inout byte[] buffer, int bufferOffset, int byteCount); + void write(long fileOffset, inout byte[] buffer, int bufferOffset, int byteCount); + void resize(long size); + long size(); + void flush(); + void close(); +} \ No newline at end of file diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathExtensions.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathExtensions.kt index 94cc8f42e..cb27dc518 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathExtensions.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathExtensions.kt @@ -14,8 +14,7 @@ import eu.darken.sdmse.common.files.saf.isAncestorOf import eu.darken.sdmse.common.files.saf.isParentOf import eu.darken.sdmse.common.files.saf.startsWith import kotlinx.coroutines.flow.Flow -import okio.Sink -import okio.Source +import okio.FileHandle import java.io.File import java.io.IOException import java.time.Instant @@ -145,12 +144,11 @@ suspend fun T.deleteAll( this.delete(gateway) } -suspend fun T.write(gateway: APathGateway, out APathLookupExtended>): Sink { - return gateway.write(this) -} - -suspend fun T.read(gateway: APathGateway, out APathLookupExtended>): Source { - return gateway.read(this) +suspend fun T.file( + gateway: APathGateway, out APathLookupExtended>, + readWrite: Boolean, +): FileHandle { + return gateway.file(this, readWrite) } suspend fun T.createSymlink( diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathGateway.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathGateway.kt index 077bca919..ff39e720b 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathGateway.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathGateway.kt @@ -2,8 +2,7 @@ package eu.darken.sdmse.common.files import eu.darken.sdmse.common.sharedresource.HasSharedResource import kotlinx.coroutines.flow.Flow -import okio.Sink -import okio.Source +import okio.FileHandle import java.time.Instant interface APathGateway< @@ -53,9 +52,7 @@ interface APathGateway< suspend fun canRead(path: P): Boolean - suspend fun read(path: P): Source - - suspend fun write(path: P): Sink + suspend fun file(path: P, readWrite: Boolean): FileHandle suspend fun delete(path: P) diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathLookupExtensions.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathLookupExtensions.kt index e88af69c1..2429118aa 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathLookupExtensions.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/APathLookupExtensions.kt @@ -3,8 +3,7 @@ package eu.darken.sdmse.common.files import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE import eu.darken.sdmse.common.debug.logging.log import kotlinx.coroutines.flow.Flow -import okio.Sink -import okio.Source +import okio.FileHandle val APathLookup<*>.isDirectory: Boolean @@ -42,13 +41,10 @@ suspend fun

> PL.deleteAll( filter: (APathLookup<*>) -> Boolean = { true } ) = lookedUp.deleteAll(gateway, filter) -suspend fun

> PL.write( - gateway: APathGateway, out APathLookupExtended

> -): Sink = lookedUp.write(gateway) - -suspend fun

> PL.read( - gateway: APathGateway, out APathLookupExtended

> -): Source = lookedUp.read(gateway) +suspend fun

> PL.file( + gateway: APathGateway, out APathLookupExtended

>, + readWrite: Boolean +): FileHandle = lookedUp.file(gateway, readWrite) suspend fun

> PL.canRead( gateway: APathGateway, out APathLookupExtended

> diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/FileHandleWithCallbacks.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/FileHandleWithCallbacks.kt new file mode 100644 index 000000000..d5f521b5f --- /dev/null +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/FileHandleWithCallbacks.kt @@ -0,0 +1,39 @@ +package eu.darken.sdmse.common.files + +import okio.FileHandle + +class FileHandleWithCallbacks( + private val wrapped: FileHandle, + private val onPostClosed: () -> Unit +) : FileHandle(wrapped.readWrite) { + + override fun protectedFlush() = wrapped.flush() + + override fun protectedResize(size: Long) = wrapped.resize(size) + + override fun protectedSize(): Long = wrapped.size() + + override fun protectedRead( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int + ): Int = wrapped.read(fileOffset, array, arrayOffset, byteCount) + + override fun protectedWrite( + fileOffset: Long, + array: ByteArray, + arrayOffset: Int, + byteCount: Int + ) = wrapped.write(fileOffset, array, arrayOffset, byteCount) + + override fun protectedClose() { + try { + wrapped.close() + } finally { + onPostClosed() + } + } +} + +fun FileHandle.callbacks(onPostClosed: () -> Unit): FileHandle = FileHandleWithCallbacks(this, onPostClosed) \ No newline at end of file diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/GatewaySwitch.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/GatewaySwitch.kt index bdf5fb6c7..3158d8db4 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/files/GatewaySwitch.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/GatewaySwitch.kt @@ -16,9 +16,8 @@ import eu.darken.sdmse.common.storage.PathMapper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.plus +import okio.FileHandle import okio.IOException -import okio.Sink -import okio.Source import java.time.Instant import javax.inject.Inject import javax.inject.Singleton @@ -179,12 +178,8 @@ class GatewaySwitch @Inject constructor( return useGateway(path) { canRead(path) } } - override suspend fun read(path: APath): Source { - return useGateway(path) { read(path) } - } - - override suspend fun write(path: APath): Sink { - return useGateway(path) { write(path) } + override suspend fun file(path: APath, readWrite: Boolean): FileHandle { + return useGateway(path) { file(path, readWrite) } } override suspend fun delete(path: APath) { diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/LocalGateway.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/LocalGateway.kt index e89c4a18a..dc2793ce7 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/LocalGateway.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/LocalGateway.kt @@ -23,6 +23,7 @@ import eu.darken.sdmse.common.files.core.local.parentsInclusive import eu.darken.sdmse.common.files.local.ipc.FileOpsClient import eu.darken.sdmse.common.funnel.IPCFunnel import eu.darken.sdmse.common.hasApiLevel +import eu.darken.sdmse.common.ipc.fileHandle import eu.darken.sdmse.common.pkgs.pkgops.LibcoreTool import eu.darken.sdmse.common.root.RootManager import eu.darken.sdmse.common.root.canUseRootNow @@ -38,10 +39,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.plus import kotlinx.coroutines.withContext -import okio.Sink -import okio.Source -import okio.sink -import okio.source +import okio.FileHandle import java.io.File import java.io.IOException import java.time.Instant @@ -664,82 +662,56 @@ class LocalGateway @Inject constructor( } } - override suspend fun read(path: LocalPath): Source = read(path, Mode.AUTO) + override suspend fun file(path: LocalPath, readWrite: Boolean): FileHandle = file(path, readWrite, Mode.AUTO) - suspend fun read(path: LocalPath, mode: Mode = Mode.AUTO): Source = runIO { + suspend fun file(path: LocalPath, readWrite: Boolean, mode: Mode = Mode.AUTO): FileHandle = runIO { try { - val javaFile = path.asFile() + val file = path.asFile() val canNormalOpen = when (mode) { Mode.ROOT -> false Mode.ADB -> false - else -> javaFile.isReadable() - } - - when { - mode == Mode.NORMAL || mode == Mode.AUTO && canNormalOpen -> { - log(TAG, VERBOSE) { "read($mode->NORMAL): $path" } - javaFile.source() - } - - hasRoot() && (mode == Mode.ROOT || mode == Mode.AUTO) -> { - log(TAG, VERBOSE) { "read($mode->ROOT): $path" } - // We need to keep the resource alive until the caller is done with the Source object - val resource = rootManager.serviceClient.get() - rootOps { it.readFile(path).callbacks { resource.close() } } - } - - hasShizuku() && (mode == Mode.ADB || mode == Mode.AUTO) -> { - log(TAG, VERBOSE) { "read($mode->ADB): $path" } - // We need to keep the resource alive until the caller is done with the Source object - val resource = shizukuManager.serviceClient.get() - adbOps { it.readFile(path).callbacks { resource.close() } } + else -> when { + readWrite -> (file.exists() && file.canWrite()) || !file.exists() && file.parentFile?.canWrite() == true + else -> file.isReadable() } - - else -> throw IOException("No matching mode available.") - } - } catch (e: IOException) { - log(TAG, WARN) { "read(path=$path, mode=$mode) failed." } - throw ReadException(path = path, cause = e) - } - } - - override suspend fun write(path: LocalPath): Sink = write(path, Mode.AUTO) - - suspend fun write(path: LocalPath, mode: Mode = Mode.AUTO): Sink = runIO { - try { - val file = path.asFile() - - val canOpen = when (mode) { - Mode.ROOT -> false - Mode.ADB -> false - else -> (file.exists() && file.canWrite()) || !file.exists() && file.parentFile?.canWrite() == true } when { - mode == Mode.NORMAL || mode == Mode.AUTO && canOpen -> { - log(TAG, VERBOSE) { "write($mode->NORMAL): $path" } - file.sink() + mode == Mode.NORMAL || mode == Mode.AUTO && canNormalOpen -> { + log(TAG, VERBOSE) { "file($mode->NORMAL): $path" } + file.fileHandle(readWrite) } hasRoot() && (mode == Mode.ROOT || mode == Mode.AUTO) -> { - log(TAG, VERBOSE) { "write($mode->ROOT): $path" } - // We need to keep the resource alive until the caller is done with the Sink object + log(TAG, VERBOSE) { "file($mode->ROOT, RW=$readWrite): $path" } + // We need to keep the resource alive until the caller is done with the object val resource = rootManager.serviceClient.get() - rootOps { it.writeFile(path).callbacks { resource.close() } } + rootOps { + it.file(path, readWrite).callbacks { + resource.close() + log(TAG, VERBOSE) { "file($mode->ROOT, RW=$readWrite): Closing resource for $path" } + } + } } hasShizuku() && (mode == Mode.ADB || mode == Mode.AUTO) -> { - log(TAG, VERBOSE) { "write($mode->ADB): $path" } - // We need to keep the resource alive until the caller is done with the Sink object + log(TAG, VERBOSE) { "file($mode->ADB, RW=$readWrite): $path" } + // We need to keep the resource alive until the caller is done with the object val resource = shizukuManager.serviceClient.get() - adbOps { it.writeFile(path).callbacks { resource.close() } } + adbOps { + it.file(path, readWrite).callbacks { + resource.close() + log(TAG, VERBOSE) { "file($mode->ADB, RW=$readWrite): Closing resource for $path" } + } + } } else -> throw IOException("No matching mode available.") } } catch (e: IOException) { - log(TAG, WARN) { "write(path=$path, mode=$mode) failed." } - throw WriteException(path = path, cause = e) + log(TAG, WARN) { "file(path=$path, mode=$mode, RW=$readWrite) failed." } + if (readWrite) throw WriteException(path = path, cause = e) + else throw ReadException(path = path, cause = e) } } diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/ipc/FileOpsClient.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/ipc/FileOpsClient.kt index d817d93a5..930715657 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/ipc/FileOpsClient.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/ipc/FileOpsClient.kt @@ -14,11 +14,9 @@ import eu.darken.sdmse.common.files.local.LocalPath import eu.darken.sdmse.common.files.local.LocalPathLookup import eu.darken.sdmse.common.files.local.LocalPathLookupExtended import eu.darken.sdmse.common.ipc.IpcClientModule -import eu.darken.sdmse.common.ipc.sink -import eu.darken.sdmse.common.ipc.source +import eu.darken.sdmse.common.ipc.fileHandle import kotlinx.coroutines.flow.Flow -import okio.Sink -import okio.Source +import okio.FileHandle import java.time.Instant class FileOpsClient @AssistedInject constructor( @@ -95,14 +93,8 @@ class FileOpsClient @AssistedInject constructor( throw e.unwrapPropagation() } - fun readFile(path: LocalPath): Source = try { - fileOpsConnection.readFile(path).source() - } catch (e: Exception) { - throw e.unwrapPropagation() - } - - fun writeFile(path: LocalPath): Sink = try { - fileOpsConnection.writeFile(path).sink() + fun file(path: LocalPath, readWrite: Boolean): FileHandle = try { + fileOpsConnection.file(path, readWrite).fileHandle(readWrite) } catch (e: Exception) { throw e.unwrapPropagation() } diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/ipc/FileOpsHost.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/ipc/FileOpsHost.kt index f7f589a31..7ece6df0b 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/ipc/FileOpsHost.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/local/ipc/FileOpsHost.kt @@ -24,19 +24,20 @@ import eu.darken.sdmse.common.files.local.setOwnership import eu.darken.sdmse.common.files.local.setPermissions import eu.darken.sdmse.common.funnel.IPCFunnel import eu.darken.sdmse.common.ipc.IpcHostModule +import eu.darken.sdmse.common.ipc.RemoteFileHandle import eu.darken.sdmse.common.ipc.RemoteInputStream -import eu.darken.sdmse.common.ipc.RemoteOutputStream -import eu.darken.sdmse.common.ipc.remoteInputStream -import eu.darken.sdmse.common.ipc.toRemoteOutputStream +import eu.darken.sdmse.common.ipc.fileHandle +import eu.darken.sdmse.common.ipc.remoteFileHandle import eu.darken.sdmse.common.pkgs.pkgops.LibcoreTool import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.plus import kotlinx.coroutines.runBlocking -import java.io.FileInputStream -import java.io.FileOutputStream import java.io.IOException import javax.inject.Inject +/** + * ROOT-side + */ class FileOpsHost @Inject constructor( @AppScope private val appScope: CoroutineScope, private val dispatcherProvider: DispatcherProvider, @@ -138,19 +139,11 @@ class FileOpsHost @Inject constructor( throw e.wrapToPropagate() } - override fun readFile(path: LocalPath): RemoteInputStream = try { - if (Bugs.isTrace) log(TAG, VERBOSE) { "readFile($path)..." } - FileInputStream(path.asFile()).remoteInputStream() + override fun file(path: LocalPath, readWrite: Boolean): RemoteFileHandle = try { + if (Bugs.isTrace) log(TAG, VERBOSE) { "file($path, $readWrite)..." } + path.asFile().fileHandle(readWrite).remoteFileHandle() } catch (e: Exception) { - log(TAG, ERROR) { "readFile(path=$path) failed\n${e.asLog()}" } - throw e.wrapToPropagate() - } - - override fun writeFile(path: LocalPath): RemoteOutputStream = try { - if (Bugs.isTrace) log(TAG, VERBOSE) { "writeFile($path)..." } - FileOutputStream(path.asFile()).toRemoteOutputStream() - } catch (e: Exception) { - log(TAG, ERROR) { "writeFile(path=$path) failed\n${e.asLog()}" } + log(TAG, ERROR) { "file(path=$path) failed\n${e.asLog()}" } throw e.wrapToPropagate() } diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/DocumentFileExtensions.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/DocumentFileExtensions.kt index 716e92654..0860fa504 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/DocumentFileExtensions.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/DocumentFileExtensions.kt @@ -16,8 +16,8 @@ import java.io.IOException import java.util.Date -enum class FileMode constructor(val value: String) { - WRITE("w"), READ("r") +enum class FileMode(val value: String) { + READ_WRITE("rw"), WRITE("w"), READ("r") } internal fun DocumentFile.fstat(contentResolver: ContentResolver): StructStat? { diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/ParcelFileDescriptorFileHandle.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/ParcelFileDescriptorFileHandle.kt new file mode 100644 index 000000000..a2c3f390f --- /dev/null +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/ParcelFileDescriptorFileHandle.kt @@ -0,0 +1,73 @@ +package eu.darken.sdmse.common.files.saf + +import android.os.ParcelFileDescriptor +import android.system.ErrnoException +import android.system.Os +import okio.FileHandle +import java.io.IOException + +fun ParcelFileDescriptor.toFileHandle(readWrite: Boolean): FileHandle { + return object : FileHandle(readWrite) { + @Throws(IOException::class) + override fun protectedRead(fileOffset: Long, array: ByteArray, arrayOffset: Int, byteCount: Int): Int = try { + val bytesRead = Os.pread( + fileDescriptor, + array, + arrayOffset, + byteCount, + fileOffset + ) + if (bytesRead == 0) -1 else bytesRead + } catch (e: ErrnoException) { + throw IOException("Error reading from file descriptor ${this@toFileHandle}", e) + } + + @Throws(IOException::class) + override fun protectedWrite(fileOffset: Long, array: ByteArray, arrayOffset: Int, byteCount: Int) { + var totalBytesWritten = 0 + while (totalBytesWritten < byteCount) { + try { + val bytesWritten = Os.pwrite( + fileDescriptor, + array, + arrayOffset + totalBytesWritten, + byteCount - totalBytesWritten, + fileOffset + totalBytesWritten + ) + if (bytesWritten <= 0) throw IOException("Error writing to file descriptor, wrote 0 bytes") + totalBytesWritten += bytesWritten + } catch (e: ErrnoException) { + throw IOException("Error writing to file descriptor", e) + } + } + } + + @Throws(IOException::class) + override fun protectedSize(): Long = try { + Os.fstat(fileDescriptor).st_size + } catch (e: ErrnoException) { + throw IOException("Error getting file size ${this@toFileHandle}", e) + } + + @Throws(IOException::class) + override fun protectedResize(size: Long) = try { + Os.ftruncate(fileDescriptor, size) + } catch (e: ErrnoException) { + throw IOException("Error resizing the file ${this@toFileHandle}", e) + } + + @Throws(IOException::class) + override fun protectedFlush() = try { + Os.fsync(fileDescriptor) + } catch (e: ErrnoException) { + throw IOException("Error flushing file descriptor ${this@toFileHandle}", e) + } + + @Throws(IOException::class) + override fun protectedClose() = try { + Os.close(fileDescriptor) + } catch (e: ErrnoException) { + throw IOException("Error closing file descriptor ${this@toFileHandle}", e) + } + } +} \ No newline at end of file diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/SAFGateway.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/SAFGateway.kt index 7aaad0ec2..4bc38b8be 100644 --- a/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/SAFGateway.kt +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/files/saf/SAFGateway.kt @@ -3,7 +3,6 @@ package eu.darken.sdmse.common.files.saf import android.content.ContentResolver import android.content.Context import android.content.Intent -import android.os.ParcelFileDescriptor import android.provider.DocumentsContract import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.sdmse.common.coroutine.AppScope @@ -30,11 +29,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.plus import kotlinx.coroutines.withContext -import okio.Sink -import okio.Source -import okio.buffer -import okio.sink -import okio.source +import okio.FileHandle import java.io.IOException import java.time.Instant import java.util.LinkedList @@ -359,36 +354,22 @@ class SAFGateway @Inject constructor( } } - override suspend fun read(path: SAFPath): Source = runIO { + override suspend fun file(path: SAFPath, readWrite: Boolean): FileHandle = runIO { try { val docFile = findDocFile(path) - log(TAG, VERBOSE) { "read(): $path -> $docFile" } + log(TAG, VERBOSE) { "file(readWrite0$readWrite): $path -> $docFile" } - if (!docFile.readable) throw IOException("readable=false") + if (readWrite && !docFile.writable) throw IOException("writable=false") + else if (!docFile.readable) throw IOException("readable=false") - val pfd = docFile.openPFD(contentResolver, FileMode.READ) - ParcelFileDescriptor.AutoCloseInputStream(pfd).source().buffer() + val pfd = docFile.openPFD(contentResolver, if (readWrite) FileMode.READ_WRITE else FileMode.READ) + pfd.toFileHandle(readWrite) } catch (e: Exception) { - log(TAG, WARN) { "Failed to read from $path: ${e.asLog()}" } + log(TAG, WARN) { "Failed to access from $path: ${e.asLog()}" } throw ReadException(path = path, cause = e) } } - override suspend fun write(path: SAFPath): Sink = runIO { - try { - val docFile = findDocFile(path) - log(TAG, VERBOSE) { "write(): $path -> $docFile" } - - if (!docFile.writable) throw IOException("writable=false") - - val pfd = docFile.openPFD(contentResolver, FileMode.WRITE) - ParcelFileDescriptor.AutoCloseOutputStream(pfd).sink().buffer() - } catch (e: Exception) { - log(TAG, WARN) { "Failed to write to $path: ${e.asLog()}" } - throw WriteException(path = path, cause = e) - } - } - override suspend fun setModifiedAt(path: SAFPath, modifiedAt: Instant): Boolean = runIO { try { val docFile = findDocFile(path) diff --git a/app-common-io/src/main/java/eu/darken/sdmse/common/ipc/RemoteFileHandleExtensions.kt b/app-common-io/src/main/java/eu/darken/sdmse/common/ipc/RemoteFileHandleExtensions.kt new file mode 100644 index 000000000..b18cfc698 --- /dev/null +++ b/app-common-io/src/main/java/eu/darken/sdmse/common/ipc/RemoteFileHandleExtensions.kt @@ -0,0 +1,120 @@ +package eu.darken.sdmse.common.ipc + + +import android.os.RemoteException +import eu.darken.sdmse.common.collections.toHex +import eu.darken.sdmse.common.debug.Bugs +import eu.darken.sdmse.common.debug.logging.Logging.Priority.ERROR +import eu.darken.sdmse.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.sdmse.common.debug.logging.asLog +import eu.darken.sdmse.common.debug.logging.log +import okio.FileHandle +import okio.FileSystem +import okio.IOException +import okio.Path.Companion.toOkioPath +import java.io.File + + +internal fun File.fileHandle(readWrite: Boolean): FileHandle { + val okioPath = this.toOkioPath() + return if (readWrite) { + FileSystem.SYSTEM.openReadWrite(okioPath, mustCreate = false, mustExist = true) + } else { + FileSystem.SYSTEM.openReadOnly(okioPath) + } +} + +/** + * Use this on the root side + */ +internal fun FileHandle.remoteFileHandle(): RemoteFileHandle.Stub = object : RemoteFileHandle.Stub() { + + override fun readWrite(): Boolean = this@remoteFileHandle.readWrite + + override fun read(fileOffset: Long, array: ByteArray, arrayOffset: Int, byteCount: Int): Int = try { + this@remoteFileHandle.read(fileOffset, array, arrayOffset, byteCount).also { + if (Bugs.isTraceDeepDive) { + log(VERBOSE) { "read(rootside-p): $fileOffset, ${array.size}, $arrayOffset, $byteCount, read $it into ${array.toHex()}" } + } + } + } catch (e: IOException) { + log(ERROR) { "read() failed: ${e.asLog()}" } + -2 + } + + override fun write(fileOffset: Long, array: ByteArray, arrayOffset: Int, byteCount: Int) = try { + this@remoteFileHandle.write(fileOffset, array, arrayOffset, byteCount) + } catch (e: IOException) { + log(ERROR) { "write() failed: ${e.asLog()}" } + } + + override fun flush() = this@remoteFileHandle.flush() + + override fun resize(size: Long) = this@remoteFileHandle.resize(size) + + override fun size(): Long = try { + this@remoteFileHandle.size() + } catch (e: IOException) { + log(ERROR) { "size() failed: ${e.asLog()}" } + -2 + } + + override fun close() { + this@remoteFileHandle.close() + } + +} + +/** + * Use this on the non-root side. + */ +internal fun RemoteFileHandle.fileHandle(readWrite: Boolean): FileHandle = object : FileHandle(readWrite) { + + @Throws(IOException::class) + override fun protectedRead(fileOffset: Long, array: ByteArray, arrayOffset: Int, byteCount: Int): Int = try { + this@fileHandle.read(fileOffset, array, arrayOffset, byteCount).also { + if (Bugs.isTraceDeepDive) { + log(VERBOSE) { "read(appside-p): $fileOffset, ${array.size}, $arrayOffset, $byteCount, read $it into ${array.toHex()}" } + } + if (it == -2) throw IOException("Remote Exception") + } + } catch (e: RemoteException) { + throw IOException("Remote Exception", e) + } + + @Throws(IOException::class) + override fun protectedWrite(fileOffset: Long, array: ByteArray, arrayOffset: Int, byteCount: Int) = try { + this@fileHandle.write(fileOffset, array, arrayOffset, byteCount) + } catch (e: RemoteException) { + throw IOException("Remote Exception", e) + } + + @Throws(IOException::class) + override fun protectedFlush() = try { + this@fileHandle.flush() + } catch (e: RemoteException) { + throw IOException("Remote Exception", e) + } + + @Throws(IOException::class) + override fun protectedResize(size: Long) = try { + this@fileHandle.resize(size) + } catch (e: RemoteException) { + throw IOException("Remote Exception", e) + } + + @Throws(IOException::class) + override fun protectedSize(): Long = try { + this@fileHandle.size() + } catch (e: RemoteException) { + throw IOException("Remote Exception", e) + } + + @Throws(IOException::class) + override fun protectedClose() = try { + this@fileHandle.close() + } catch (e: RemoteException) { + throw IOException("Remote Exception", e) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/sdmse/common/coil/BitmapFetcher.kt b/app/src/main/java/eu/darken/sdmse/common/coil/BitmapFetcher.kt index df3f8a62e..4bf429e9f 100644 --- a/app/src/main/java/eu/darken/sdmse/common/coil/BitmapFetcher.kt +++ b/app/src/main/java/eu/darken/sdmse/common/coil/BitmapFetcher.kt @@ -3,7 +3,6 @@ package eu.darken.sdmse.common.coil import android.content.Context import coil.ImageLoader import coil.decode.DataSource -import coil.decode.ImageSource import coil.fetch.FetchResult import coil.fetch.Fetcher import coil.fetch.SourceResult @@ -13,7 +12,6 @@ import eu.darken.sdmse.common.MimeTypeTool import eu.darken.sdmse.common.files.APathLookup import eu.darken.sdmse.common.files.FileType import eu.darken.sdmse.common.files.GatewaySwitch -import okio.buffer import javax.inject.Inject class BitmapFetcher @Inject constructor( @@ -30,19 +28,15 @@ class BitmapFetcher @Inject constructor( if (target.fileType != FileType.FILE) throw IllegalArgumentException("Not a file: $data") if (target.size == 0L) throw IllegalArgumentException("Empty file: $data") - val mimeType = mimeTypeTool.determineMimeType(data.lookup) val isValid = mimeType.startsWith("image") if (!isValid) throw UnsupportedOperationException("Unsupported mimetype: $mimeType") - val buffer = gatewaySwitch.read(target.lookedUp).buffer() + val handle = gatewaySwitch.file(target.lookedUp, readWrite = false) return SourceResult( - ImageSource( - buffer, - coilTempFiles.getBaseCachePath(), - ), + ImageSource(buffer, context), mimeType, dataSource = DataSource.DISK ) diff --git a/app/src/main/java/eu/darken/sdmse/common/coil/FileHandleImageSource.kt b/app/src/main/java/eu/darken/sdmse/common/coil/FileHandleImageSource.kt new file mode 100644 index 000000000..d435bc132 --- /dev/null +++ b/app/src/main/java/eu/darken/sdmse/common/coil/FileHandleImageSource.kt @@ -0,0 +1,36 @@ +package eu.darken.sdmse.common.coil + +import android.media.MediaDataSource +import coil.annotation.ExperimentalCoilApi +import coil.decode.ImageSource +import coil.fetch.MediaDataSourceFetcher.MediaSourceMetadata +import coil.request.Options +import okio.FileHandle +import okio.buffer + +@OptIn(ExperimentalCoilApi::class) +internal fun FileHandle.toImageSource( + options: Options, +): ImageSource { + val handle = this + val sourceBuffer = this.source().buffer() + val mediaDataSource = object : MediaDataSource() { + override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { + return handle.read(position, buffer, offset, size) + } + + override fun getSize(): Long { + return handle.size() + } + + override fun close() { + sourceBuffer.close() + handle.close() + } + } + return ImageSource( + source = sourceBuffer, + context = options.context, + metadata = MediaSourceMetadata(mediaDataSource), + ) +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/sdmse/common/coil/PathPreviewFetcher.kt b/app/src/main/java/eu/darken/sdmse/common/coil/PathPreviewFetcher.kt index 26089d1cb..1604ce312 100644 --- a/app/src/main/java/eu/darken/sdmse/common/coil/PathPreviewFetcher.kt +++ b/app/src/main/java/eu/darken/sdmse/common/coil/PathPreviewFetcher.kt @@ -5,7 +5,6 @@ import android.content.pm.PackageManager import androidx.core.content.ContextCompat import coil.ImageLoader import coil.decode.DataSource -import coil.decode.ImageSource import coil.fetch.DrawableResult import coil.fetch.FetchResult import coil.fetch.Fetcher @@ -22,7 +21,6 @@ import eu.darken.sdmse.common.files.extension import eu.darken.sdmse.common.files.iconRes import eu.darken.sdmse.common.files.local.LocalPath import eu.darken.sdmse.main.core.GeneralSettings -import okio.buffer import javax.inject.Inject class PathPreviewFetcher @Inject constructor( @@ -55,12 +53,10 @@ class PathPreviewFetcher @Inject constructor( return when { mimeType.startsWith("image") || mimeType.startsWith("video") -> { - val buffer = gatewaySwitch.read(data.lookedUp).buffer() + val handle = gatewaySwitch.file(data.lookedUp, readWrite = false) + SourceResult( - ImageSource( - buffer, - coilTempFiles.getBaseCachePath(), - ), + handle.toImageSource(options), mimeType, dataSource = DataSource.DISK ) diff --git a/app/src/main/java/eu/darken/sdmse/deduplicator/core/scanner/checksum/ChecksumSleuth.kt b/app/src/main/java/eu/darken/sdmse/deduplicator/core/scanner/checksum/ChecksumSleuth.kt index eeb1ea2bd..8c5f2b9e4 100644 --- a/app/src/main/java/eu/darken/sdmse/deduplicator/core/scanner/checksum/ChecksumSleuth.kt +++ b/app/src/main/java/eu/darken/sdmse/deduplicator/core/scanner/checksum/ChecksumSleuth.kt @@ -171,7 +171,7 @@ class ChecksumSleuth @Inject constructor( val start = System.currentTimeMillis() val hash = try { - gatewaySwitch.read(item.lookedUp).hash(Hasher.Type.SHA256) + gatewaySwitch.file(item.lookedUp, readWrite = false).source().hash(Hasher.Type.SHA256) } catch (e: Exception) { log(TAG, ERROR) { "Failed to read $item: ${e.asLog()}" } return@flow diff --git a/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/SuperfluousApksFilter.kt b/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/SuperfluousApksFilter.kt index 1d0c3a6f6..01e60a828 100644 --- a/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/SuperfluousApksFilter.kt +++ b/app/src/main/java/eu/darken/sdmse/systemcleaner/core/filter/stock/SuperfluousApksFilter.kt @@ -23,9 +23,9 @@ import eu.darken.sdmse.common.files.APathLookup import eu.darken.sdmse.common.files.GatewaySwitch import eu.darken.sdmse.common.files.copyToAutoClose import eu.darken.sdmse.common.files.core.local.deleteAll +import eu.darken.sdmse.common.files.file import eu.darken.sdmse.common.files.inputStream import eu.darken.sdmse.common.files.local.toLocalPath -import eu.darken.sdmse.common.files.read import eu.darken.sdmse.common.files.segs import eu.darken.sdmse.common.hashing.Hasher import eu.darken.sdmse.common.pkgs.PkgRepo @@ -102,7 +102,9 @@ class SuperfluousApksFilter @Inject constructor( } item.name.endsWith(".apks") -> withContext(NonCancellable) { - val checksum = item.read(gatewaySwitch).use { Hasher(Hasher.Type.MD5).calc(it) }.format() + val checksum = item.file(gatewaySwitch, readWrite = false).source().use { + Hasher(Hasher.Type.MD5).calc(it) + }.format() log(TAG, VERBOSE) { "Checksum is $checksum for ${item.path}" } val baseNames = setOf("base.apk") @@ -114,7 +116,7 @@ class SuperfluousApksFilter @Inject constructor( if (extractedBase.exists()) { log(TAG, WARN) { "Why do we already have extracted this?: $extractedBase" } } else if (cacheRepo.canSpare(item.size)) { - item.read(gatewaySwitch).use { apksSource -> + item.file(gatewaySwitch, readWrite = false).source().use { apksSource -> ZipInputStream(apksSource.inputStream()).use { zis -> zis.entries .find { (_, entry) ->