PM-41429: Chore: Decouple dependencies (#7238)

This commit is contained in:
David Perez
2026-08-04 14:21:05 +00:00
committed by GitHub
parent d029ebb0bf
commit 33a8429c3e
11 changed files with 221 additions and 82 deletions
@@ -3,7 +3,9 @@ package com.bitwarden.data.manager.di
import android.app.Application
import android.content.Context
import com.bitwarden.core.data.manager.BuildInfoManager
import com.bitwarden.core.data.manager.UuidManager
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.data.datasource.disk.ConfigDiskSource
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.data.manager.BitwardenPackageManager
import com.bitwarden.data.manager.BitwardenPackageManagerImpl
@@ -17,8 +19,6 @@ import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManagerImpl
import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriter
import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriterImpl
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.network.service.DownloadService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -50,11 +50,11 @@ object DataManagerModule {
@Singleton
fun provideFileManager(
@ApplicationContext context: Context,
downloadService: DownloadService,
uuidManager: UuidManager,
dispatcherManager: DispatcherManager,
): FileManager = FileManagerImpl(
context = context,
downloadService = downloadService,
uuidManager = uuidManager,
dispatcherManager = dispatcherManager,
)
@@ -81,13 +81,13 @@ object DataManagerModule {
fileManager: FileManager,
dispatcherManager: DispatcherManager,
buildInfoManager: BuildInfoManager,
serverConfigRepository: ServerConfigRepository,
configDiskSource: ConfigDiskSource,
): FlightRecorderWriter = FlightRecorderWriterImpl(
clock = clock,
fileManager = fileManager,
dispatcherManager = dispatcherManager,
buildInfoManager = buildInfoManager,
serverConfigRepository = serverConfigRepository,
configDiskSource = configDiskSource,
)
@Provides
@@ -2,9 +2,9 @@ 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
import java.io.InputStream
/**
* Manages reading files.
@@ -28,10 +28,10 @@ interface FileManager {
suspend fun delete(vararg files: File)
/**
* Downloads a file temporarily to cache from [url]. A successful [DownloadResult] will contain
* Opens the provided [stream] and writes it to cache. A successful [Result] will contain
* the final file path.
*/
suspend fun downloadFileToCache(url: String): DownloadResult
suspend fun streamFileToCache(stream: InputStream): Result<File>
/**
* Writes an existing [file] to a [fileUri]. `true` will be returned if the file was
@@ -5,11 +5,12 @@ 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.UuidManager
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.core.data.util.asFailure
import com.bitwarden.core.data.util.asSuccess
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
@@ -18,7 +19,7 @@ import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.UUID
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
@@ -32,7 +33,7 @@ private const val BUFFER_SIZE: Int = 1024
*/
internal class FileManagerImpl(
private val context: Context,
private val downloadService: DownloadService,
private val uuidManager: UuidManager,
private val dispatcherManager: DispatcherManager,
) : FileManager {
@@ -48,20 +49,10 @@ internal class FileManagerImpl(
}
}
@Suppress("NestedBlockDepth")
override suspend fun downloadFileToCache(url: String): DownloadResult {
val response = downloadService
.getDataStream(url)
.fold(
onSuccess = { it },
onFailure = { return DownloadResult.Failure(error = it) },
)
override suspend fun streamFileToCache(stream: InputStream): Result<File> {
// Create a temporary file in cache to write to
val file = File(context.cacheDir, UUID.randomUUID().toString())
withContext(dispatcherManager.io) {
val stream = response.byteStream()
val file = File(context.cacheDir, uuidManager.generateUuid())
return withContext(dispatcherManager.io) {
stream.use {
val buffer = ByteArray(BUFFER_SIZE)
var progress = 0
@@ -76,13 +67,12 @@ internal class FileManagerImpl(
}
fos.flush()
} catch (e: RuntimeException) {
return@withContext DownloadResult.Failure(error = e)
return@withContext e.asFailure()
}
}
}
file.asSuccess()
}
return DownloadResult.Success(file)
}
@Suppress("NestedBlockDepth")
@@ -8,9 +8,9 @@ import com.bitwarden.core.data.manager.BuildInfoManager
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.core.data.manager.util.deviceData
import com.bitwarden.core.data.util.toFormattedPattern
import com.bitwarden.data.datasource.disk.ConfigDiskSource
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
import com.bitwarden.data.manager.file.FileManager
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.network.util.redactHostnamesInMessage
import kotlinx.coroutines.withContext
import timber.log.Timber
@@ -34,12 +34,15 @@ internal class FlightRecorderWriterImpl(
private val fileManager: FileManager,
private val dispatcherManager: DispatcherManager,
private val buildInfoManager: BuildInfoManager,
private val serverConfigRepository: ServerConfigRepository,
private val configDiskSource: ConfigDiskSource,
) : FlightRecorderWriter {
private val configuredHosts: Set<String>
get() {
val environment = serverConfigRepository.serverConfigStateFlow.value
?.serverData?.environment ?: return emptySet()
val environment = configDiskSource
.serverConfig
?.serverData
?.environment
?: return emptySet()
return listOfNotNull(
environment.vaultUrl,
environment.apiUrl,
@@ -75,7 +78,7 @@ internal class FlightRecorderWriterImpl(
logFile.createNewFile()
val ciInfo = buildInfoManager.ciBuildInfo?.takeIf { it.isNotBlank() }
val serverData = serverConfigRepository.serverConfigStateFlow.value?.serverData
val serverData = configDiskSource.serverConfig?.serverData
val serverInfo = StringBuilder()
.append(serverData?.server?.name ?: "Bitwarden Cloud")
.apply {
@@ -1,20 +0,0 @@
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()
}
@@ -3,23 +3,27 @@ package com.bitwarden.data.manager.file
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import com.bitwarden.core.data.manager.UuidManager
import com.bitwarden.core.data.manager.dispatcher.FakeDispatcherManager
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.network.service.DownloadService
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertInstanceOf
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
/**
* Test class for [FileManagerImpl].
@@ -28,18 +32,25 @@ class FileManagerTest {
private val fakeDispatcherManager = FakeDispatcherManager()
private val mockContentResolver = mockk<ContentResolver>()
private val downloadService = mockk<DownloadService>()
private val cacheDirectory: File = Files.createTempDirectory("cache").toFile()
private val mockContext = mockk<Context> {
every { contentResolver } returns mockContentResolver
every { cacheDir } returns cacheDirectory
}
private val uuidManager: UuidManager = mockk()
private val mockUri = mockk<Uri>()
private val fileManager = FileManagerImpl(
context = mockContext,
uuidManager = uuidManager,
dispatcherManager = fakeDispatcherManager,
downloadService = downloadService,
)
@AfterEach
fun tearDown() {
cacheDirectory.deleteRecursively()
}
//region stringToUri Tests
@Test
@@ -258,6 +269,62 @@ class FileManagerTest {
//endregion
//region streamFileToCache Tests
@Test
fun `streamFileToCache with valid stream should return Success with the cached file`() =
runTest {
val testData = "Test content".toByteArray()
val mockInputStream = createMockInputStream(testData)
every { uuidManager.generateUuid() } returns "mockUuid"
val result = fileManager.streamFileToCache(stream = mockInputStream)
val file = result.getOrThrow()
assertEquals(File(cacheDirectory, "mockUuid"), file)
assertArrayEquals(testData, file.readBytes())
verify(exactly = 1) { mockInputStream.close() }
}
@Test
fun `streamFileToCache with empty stream should return Success with an empty file`() = runTest {
val mockInputStream = createMockInputStream(testData = ByteArray(0))
every { uuidManager.generateUuid() } returns "mockUuid"
val result = fileManager.streamFileToCache(stream = mockInputStream)
val file = result.getOrThrow()
assertEquals(0, file.length())
}
@Test
fun `streamFileToCache with large stream should write the stream completely`() = runTest {
val testData = "L".repeat(5000).toByteArray()
val mockInputStream = createMockInputStream(testData)
every { uuidManager.generateUuid() } returns "mockUuid"
val result = fileManager.streamFileToCache(stream = mockInputStream)
assertArrayEquals(testData, result.getOrThrow().readBytes())
}
@Test
fun `streamFileToCache with read failure should return Failure`() = runTest {
val error = RuntimeException("Read failed")
val mockInputStream = mockk<InputStream> {
every { read(any<ByteArray>()) } throws error
every { close() } just runs
}
every { uuidManager.generateUuid() } returns "mockUuid"
val result = fileManager.streamFileToCache(stream = mockInputStream)
assertEquals(error, result.exceptionOrNull())
verify(exactly = 1) { mockInputStream.close() }
}
//endregion
//region Helper Methods
/**