mirror of
https://github.com/bitwarden/android.git
synced 2026-03-09 03:33:36 -05:00
PM-29795: Move FileManager to data module (#6268)
This commit is contained in:
@@ -54,6 +54,7 @@ dependencies {
|
||||
implementation(libs.google.hilt.android)
|
||||
ksp(libs.google.hilt.compiler)
|
||||
implementation(libs.kotlinx.serialization)
|
||||
implementation(libs.square.okhttp)
|
||||
implementation(libs.timber)
|
||||
|
||||
// Pull in test fixtures from other modules
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.bitwarden.data.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.data.manager.BitwardenPackageManager
|
||||
import com.bitwarden.data.manager.BitwardenPackageManagerImpl
|
||||
import com.bitwarden.data.manager.NativeLibraryManager
|
||||
import com.bitwarden.data.manager.NativeLibraryManagerImpl
|
||||
import com.bitwarden.data.manager.file.FileManager
|
||||
import com.bitwarden.data.manager.file.FileManagerImpl
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -25,6 +29,18 @@ object DataManagerModule {
|
||||
@ApplicationContext context: Context,
|
||||
): BitwardenPackageManager = BitwardenPackageManagerImpl(context = context)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFileManager(
|
||||
@ApplicationContext context: Context,
|
||||
downloadService: DownloadService,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): FileManager = FileManagerImpl(
|
||||
context = context,
|
||||
downloadService = downloadService,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNativeLibraryManager(): NativeLibraryManager = NativeLibraryManagerImpl()
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.bitwarden.data.manager.file
|
||||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.data.manager.model.DownloadResult
|
||||
import com.bitwarden.data.manager.model.ZipFileResult
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Manages reading files.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
interface FileManager {
|
||||
|
||||
/**
|
||||
* Absolute path to the private file storage directory.
|
||||
*/
|
||||
val filesDirectory: String
|
||||
|
||||
/**
|
||||
* Absolute path to the private logs storage directory.
|
||||
*/
|
||||
val logsDirectory: String
|
||||
|
||||
/**
|
||||
* Deletes [files] from disk.
|
||||
*/
|
||||
suspend fun delete(vararg files: File)
|
||||
|
||||
/**
|
||||
* Downloads a file temporarily to cache from [url]. A successful [DownloadResult] will contain
|
||||
* the final file path.
|
||||
*/
|
||||
suspend fun downloadFileToCache(url: String): DownloadResult
|
||||
|
||||
/**
|
||||
* Writes an existing [file] to a [fileUri]. `true` will be returned if the file was
|
||||
* successfully saved.
|
||||
*/
|
||||
suspend fun fileToUri(fileUri: Uri, file: File): Boolean
|
||||
|
||||
/**
|
||||
* Writes an [dataString] to a [fileUri]. `true` will be returned if the file was
|
||||
* successfully saved.
|
||||
*/
|
||||
suspend fun stringToUri(fileUri: Uri, dataString: String): Boolean
|
||||
|
||||
/**
|
||||
* Reads the [fileUri] into memory. A successful result will contain the raw [ByteArray].
|
||||
*/
|
||||
suspend fun uriToByteArray(fileUri: Uri): Result<ByteArray>
|
||||
|
||||
/**
|
||||
* Reads the file or folder on disk from the [uri] and creates a temporary zip file. A
|
||||
* successful result will contain the zip [File] reference.
|
||||
*/
|
||||
suspend fun zipUriToCache(uri: Uri): ZipFileResult
|
||||
|
||||
/**
|
||||
* Reads the [fileUri] into a file on disk. A successful result will contain the [File]
|
||||
* reference.
|
||||
*/
|
||||
suspend fun writeUriToCache(fileUri: Uri): Result<File>
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.bitwarden.data.manager.file
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.core.data.util.sdkAgnosticTransferTo
|
||||
import com.bitwarden.data.manager.model.DownloadResult
|
||||
import com.bitwarden.data.manager.model.ZipFileResult
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.UUID
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
/**
|
||||
* The buffer size to be used when reading from an input stream.
|
||||
*/
|
||||
private const val BUFFER_SIZE: Int = 1024
|
||||
|
||||
/**
|
||||
* The default implementation of the [FileManager] interface.
|
||||
*/
|
||||
internal class FileManagerImpl(
|
||||
private val context: Context,
|
||||
private val downloadService: DownloadService,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
) : FileManager {
|
||||
|
||||
override val filesDirectory: String
|
||||
get() = context.filesDir.absolutePath
|
||||
|
||||
override val logsDirectory: String
|
||||
get() = "${context.dataDir}/logs"
|
||||
|
||||
override suspend fun delete(vararg files: File) {
|
||||
withContext(dispatcherManager.io) {
|
||||
files.forEach { it.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
override suspend fun downloadFileToCache(url: String): DownloadResult {
|
||||
val response = downloadService
|
||||
.getDataStream(url)
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { return DownloadResult.Failure(error = it) },
|
||||
)
|
||||
|
||||
// Create a temporary file in cache to write to
|
||||
val file = File(context.cacheDir, UUID.randomUUID().toString())
|
||||
|
||||
withContext(dispatcherManager.io) {
|
||||
val stream = response.byteStream()
|
||||
stream.use {
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
var progress = 0
|
||||
FileOutputStream(file).use { fos ->
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
var read = stream.read(buffer)
|
||||
while (read > 0) {
|
||||
fos.write(buffer, 0, read)
|
||||
progress += read
|
||||
read = stream.read(buffer)
|
||||
}
|
||||
fos.flush()
|
||||
} catch (e: RuntimeException) {
|
||||
return@withContext DownloadResult.Failure(error = e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DownloadResult.Success(file)
|
||||
}
|
||||
|
||||
@Suppress("NestedBlockDepth")
|
||||
override suspend fun fileToUri(fileUri: Uri, file: File): Boolean {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
return try {
|
||||
withContext(dispatcherManager.io) {
|
||||
context
|
||||
.contentResolver
|
||||
.openOutputStream(fileUri)
|
||||
?.use { outputStream ->
|
||||
FileInputStream(file).use { inputStream ->
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
var length: Int
|
||||
while (inputStream.read(buffer).also { length = it } != -1) {
|
||||
outputStream.write(buffer, 0, length)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (_: RuntimeException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun stringToUri(fileUri: Uri, dataString: String): Boolean {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
return try {
|
||||
withContext(dispatcherManager.io) {
|
||||
context
|
||||
.contentResolver
|
||||
.openOutputStream(fileUri)
|
||||
?.use { outputStream ->
|
||||
outputStream.write(dataString.toByteArray())
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (_: RuntimeException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun uriToByteArray(fileUri: Uri): Result<ByteArray> =
|
||||
runCatching {
|
||||
withContext(dispatcherManager.io) {
|
||||
context
|
||||
.contentResolver
|
||||
.openInputStream(fileUri)
|
||||
?.use { inputStream ->
|
||||
ByteArrayOutputStream().use { outputStream ->
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
var length: Int
|
||||
while (inputStream.read(buffer).also { length = it } != -1) {
|
||||
outputStream.write(buffer, 0, length)
|
||||
}
|
||||
outputStream.toByteArray()
|
||||
}
|
||||
}
|
||||
?: throw IllegalStateException("Stream has crashed")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun zipUriToCache(
|
||||
uri: Uri,
|
||||
): ZipFileResult =
|
||||
runCatching {
|
||||
withContext(dispatcherManager.io) {
|
||||
val sourceFile = File(uri.toString())
|
||||
if (!sourceFile.exists()) {
|
||||
ZipFileResult.NothingToZip
|
||||
} else {
|
||||
val zipFile = File.createTempFile(
|
||||
"bitwarden_flight_recorder",
|
||||
".zip",
|
||||
context.cacheDir,
|
||||
)
|
||||
FileOutputStream(zipFile).use { fos ->
|
||||
BufferedOutputStream(fos).use { bos ->
|
||||
ZipOutputStream(bos).use { zos ->
|
||||
zos.zipFiles(sourceFile = sourceFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
ZipFileResult.Success(file = zipFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
onFailure = { ZipFileResult.Failure(error = it) },
|
||||
onSuccess = { it },
|
||||
)
|
||||
|
||||
override suspend fun writeUriToCache(fileUri: Uri): Result<File> =
|
||||
runCatching {
|
||||
withContext(dispatcherManager.io) {
|
||||
val tempFileName = "temp_send_file.bw"
|
||||
context
|
||||
.contentResolver
|
||||
.openInputStream(fileUri)
|
||||
?.use { inputStream ->
|
||||
context.openFileOutput(tempFileName, Context.MODE_PRIVATE)
|
||||
.use { outputStream ->
|
||||
inputStream.sdkAgnosticTransferTo(outputStream)
|
||||
}
|
||||
}
|
||||
?: throw IllegalStateException("Stream has crashed")
|
||||
|
||||
File(context.filesDir, tempFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to write the files to a zip file.
|
||||
*/
|
||||
@Suppress("NestedBlockDepth")
|
||||
private fun ZipOutputStream.zipFiles(
|
||||
sourceFile: File,
|
||||
parentDir: String? = null,
|
||||
) {
|
||||
if (sourceFile.isDirectory) {
|
||||
val parentDirName = sourceFile.name + File.separator
|
||||
val entry = ZipEntry(parentDirName).apply {
|
||||
this.time = sourceFile.lastModified()
|
||||
this.size = sourceFile.length()
|
||||
}
|
||||
this.putNextEntry(entry)
|
||||
sourceFile.listFiles().orEmpty().forEach { file ->
|
||||
this.zipFiles(sourceFile = file, parentDir = parentDirName)
|
||||
}
|
||||
} else {
|
||||
FileInputStream(sourceFile).use { fis ->
|
||||
BufferedInputStream(fis).use { bis ->
|
||||
val entry = ZipEntry("${parentDir.orEmpty()}/${sourceFile.name}").apply {
|
||||
this.time = sourceFile.lastModified()
|
||||
this.size = sourceFile.length()
|
||||
}
|
||||
this.putNextEntry(entry)
|
||||
val data = ByteArray(BUFFER_SIZE)
|
||||
while (true) {
|
||||
val readBytes = bis.read(data)
|
||||
if (readBytes == -1) break
|
||||
this.write(data, 0, readBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.bitwarden.data.manager.model
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Represents a result from downloading a raw file.
|
||||
*/
|
||||
sealed class DownloadResult {
|
||||
/**
|
||||
* The download was a success, and was saved to [file].
|
||||
*/
|
||||
data class Success(val file: File) : DownloadResult()
|
||||
|
||||
/**
|
||||
* The download failed.
|
||||
*/
|
||||
data class Failure(
|
||||
val error: Throwable,
|
||||
) : DownloadResult()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.bitwarden.data.manager.model
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Represents a result from zipping a raw file or folder.
|
||||
*/
|
||||
sealed class ZipFileResult {
|
||||
/**
|
||||
* The zip was a success, and was saved to the temporary [file].
|
||||
*/
|
||||
data class Success(val file: File) : ZipFileResult()
|
||||
|
||||
/**
|
||||
* There was no file or folder to zip.
|
||||
*/
|
||||
data object NothingToZip : ZipFileResult()
|
||||
|
||||
/**
|
||||
* The zip failed in some generic manner.
|
||||
*/
|
||||
data class Failure(val error: Throwable) : ZipFileResult()
|
||||
}
|
||||
Reference in New Issue
Block a user