Add concrete FlightRecorderDiskSource (#6281)

This commit is contained in:
David Perez 2026-01-07 13:30:53 -06:00 committed by GitHub
parent 5245a7a0c7
commit c4a94cf5d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 241 additions and 194 deletions

View File

@ -5,7 +5,7 @@ import androidx.core.content.edit
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.datasource.disk.BaseDiskSource
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
@ -47,7 +47,6 @@ private const val CREATE_ACTION_COUNT = "createActionCount"
private const val SHOULD_SHOW_ADD_LOGIN_COACH_MARK = "shouldShowAddLoginCoachMark"
private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMark"
private const val RESUME_SCREEN = "resumeScreen"
private const val FLIGHT_RECORDER_KEY = "flightRecorderData"
private const val IS_DYNAMIC_COLORS_ENABLED = "isDynamicColorsEnabled"
private const val BROWSER_AUTOFILL_DIALOG_RESHOW_TIME = "browserAutofillDialogReshowTime"
@ -58,8 +57,10 @@ private const val BROWSER_AUTOFILL_DIALOG_RESHOW_TIME = "browserAutofillDialogRe
class SettingsDiskSourceImpl(
private val sharedPreferences: SharedPreferences,
private val json: Json,
flightRecorderDiskSource: FlightRecorderDiskSource,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
SettingsDiskSource {
SettingsDiskSource,
FlightRecorderDiskSource by flightRecorderDiskSource {
private val mutableAppLanguageFlow = bufferedMutableSharedFlow<AppLanguage?>(replay = 1)
private val mutableAppThemeFlow = bufferedMutableSharedFlow<AppTheme>(replay = 1)
@ -92,8 +93,6 @@ class SettingsDiskSourceImpl(
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableFlightRecorderDataFlow = bufferedMutableSharedFlow<FlightRecorderDataSet?>()
private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
@ -214,20 +213,6 @@ class SettingsDiskSourceImpl(
get() = mutableHasUserLoggedInOrCreatedAccountFlow
.onSubscription { emit(getBoolean(HAS_USER_LOGGED_IN_OR_CREATED_AN_ACCOUNT_KEY)) }
override var flightRecorderData: FlightRecorderDataSet?
get() = getString(key = FLIGHT_RECORDER_KEY)
?.let { json.decodeFromStringOrNull<FlightRecorderDataSet>(it) }
set(value) {
putString(
key = FLIGHT_RECORDER_KEY,
value = value?.let { json.encodeToString(it) },
)
mutableFlightRecorderDataFlow.tryEmit(value)
}
override val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
get() = mutableFlightRecorderDataFlow.onSubscription { emit(flightRecorderData) }
override var browserAutofillDialogReshowTime: Instant?
get() = getLong(key = BROWSER_AUTOFILL_DIALOG_RESHOW_TIME)?.let { Instant.ofEpochMilli(it) }
set(value) {

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.data.datasource.disk.di.EncryptedPreferences
import com.bitwarden.data.datasource.disk.di.UnencryptedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
@ -139,10 +140,12 @@ object PlatformDiskModule {
fun provideSettingsDiskSource(
@UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json,
flightRecorderDiskSource: FlightRecorderDiskSource,
): SettingsDiskSource =
SettingsDiskSourceImpl(
sharedPreferences = sharedPreferences,
json = json,
flightRecorderDiskSource = flightRecorderDiskSource,
)
@Provides

View File

@ -12,8 +12,6 @@ import com.bitwarden.core.data.manager.toast.ToastManagerImpl
import com.bitwarden.cxf.registry.CredentialExchangeRegistry
import com.bitwarden.cxf.registry.dsl.credentialExchangeRegistry
import com.bitwarden.data.manager.NativeLibraryManager
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
import com.bitwarden.data.manager.flightrecorder.FlightRecorderWriter
import com.bitwarden.data.repository.ServerConfigRepository
import com.bitwarden.network.BitwardenServiceClient
import com.bitwarden.network.service.EventService
@ -106,22 +104,6 @@ object PlatformManagerModule {
application: Application,
): AppStateManager = AppStateManagerImpl(application = application)
@Provides
@Singleton
fun provideFlightRecorderManager(
@ApplicationContext context: Context,
clock: Clock,
dispatcherManager: DispatcherManager,
settingsDiskSource: SettingsDiskSource,
flightRecorderWriter: FlightRecorderWriter,
): FlightRecorderManager = FlightRecorderManager.create(
context = context,
clock = clock,
dispatcherManager = dispatcherManager,
flightRecorderDiskSource = settingsDiskSource,
flightRecorderWriter = flightRecorderWriter,
)
@Provides
@Singleton
fun provideAuthenticatorBridgeProcessor(

View File

@ -2,17 +2,16 @@ package com.x8bit.bitwarden.data.platform.datasource.disk
import androidx.core.content.edit
import app.cash.turbine.test
import com.bitwarden.core.data.util.assertJsonEquals
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.core.di.CoreModule
import com.bitwarden.data.datasource.disk.base.FakeSharedPreferences
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Assertions.assertEquals
@ -27,9 +26,10 @@ class SettingsDiskSourceTest {
private val fakeSharedPreferences = FakeSharedPreferences()
private val json = CoreModule.providesJson()
private val settingsDiskSource = SettingsDiskSourceImpl(
private val settingsDiskSource: SettingsDiskSource = SettingsDiskSourceImpl(
sharedPreferences = fakeSharedPreferences,
json = json,
flightRecorderDiskSource = mockk(),
)
@Test
@ -85,104 +85,6 @@ class SettingsDiskSourceTest {
)
}
@Test
fun `flightRecorderData should pull from SharedPreferences`() {
val flightRecorderKey = "bwPreferencesStorage:flightRecorderData"
val encodedData = """
{
"data": [
{
"id": "51"
"fileName": "flight_recorder_2025-04-03_14-22-40",
"startTime": 1744059882,
"duration": 3600,
"isActive": false
}
]
}
"""
.trimIndent()
val expected = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "51",
fileName = "flight_recorder_2025-04-03_14-22-40",
startTimeMs = 1_744_059_882L,
durationMs = 3_600L,
isActive = false,
),
),
)
// Verify initial value is null and disk source matches shared preferences.
assertNull(fakeSharedPreferences.getString(flightRecorderKey, null))
assertNull(settingsDiskSource.flightRecorderData)
// Updating the shared preferences should update disk source.
fakeSharedPreferences.edit { putString(flightRecorderKey, encodedData) }
val actual = settingsDiskSource.flightRecorderData
assertEquals(expected, actual)
}
@Test
fun `flightRecorderDataFlow should react to changes in isFLightRecorderEnabled`() = runTest {
val expected = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "52",
fileName = "flight_recorder_2025-04-03_14-22-40",
startTimeMs = 1_744_059_882L,
durationMs = 3_600L,
isActive = true,
),
),
)
settingsDiskSource.flightRecorderDataFlow.test {
// The initial values of the Flow and the property are in sync
assertNull(settingsDiskSource.flightRecorderData)
assertNull(awaitItem())
settingsDiskSource.flightRecorderData = expected
assertEquals(expected, awaitItem())
settingsDiskSource.flightRecorderData = null
assertNull(awaitItem())
}
}
@Test
fun `setting flightRecorderData should update SharedPreferences`() {
val flightRecorderKey = "bwPreferencesStorage:flightRecorderData"
val data = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "53",
fileName = "flight_recorder_2025-04-03_14-22-40",
startTimeMs = 1_744_059_882L,
durationMs = 3_600L,
isActive = true,
),
),
)
val expected = """
{
"data": [
{
"id": "53",
"fileName": "flight_recorder_2025-04-03_14-22-40",
"startTime": 1744059882,
"duration": 3600,
"isActive": true
}
]
}
"""
.trimIndent()
settingsDiskSource.flightRecorderData = data
val actual = fakeSharedPreferences.getString(flightRecorderKey, null)
assertJsonEquals(expected, actual!!)
}
@Test
fun `systemBiometricIntegritySource should pull from SharedPreferences`() {
val biometricIntegritySource = "bwPreferencesStorage:biometricIntegritySource"

View File

@ -2,7 +2,8 @@ package com.x8bit.bitwarden.data.platform.datasource.disk.util
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.data.datasource.disk.util.FakeFlightRecorderDiskSource
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
@ -19,7 +20,10 @@ import java.time.Instant
/**
* Fake, memory-based implementation of [SettingsDiskSource].
*/
class FakeSettingsDiskSource : SettingsDiskSource {
class FakeSettingsDiskSource(
flightRecorderDiskSource: FakeFlightRecorderDiskSource = FakeFlightRecorderDiskSource(),
) : SettingsDiskSource,
FlightRecorderDiskSource by flightRecorderDiskSource {
private val mutableAppLanguageFlow = bufferedMutableSharedFlow<AppLanguage?>(replay = 1)
@ -52,9 +56,6 @@ class FakeSettingsDiskSource : SettingsDiskSource {
private val mutableShouldShowGeneratorCoachMarkFlow =
bufferedMutableSharedFlow<Boolean?>()
private val mutableFlightRecorderDataFlow =
bufferedMutableSharedFlow<FlightRecorderDataSet?>(replay = 1)
private var storedAppLanguage: AppLanguage? = null
private var storedAppTheme: AppTheme = AppTheme.DEFAULT
private val storedLastSyncTime = mutableMapOf<String, Instant?>()
@ -86,7 +87,6 @@ class FakeSettingsDiskSource : SettingsDiskSource {
private var createSendActionCount: Int? = null
private var hasSeenAddLoginCoachMark: Boolean? = null
private var hasSeenGeneratorCoachMark: Boolean? = null
private var storedFlightRecorderData: FlightRecorderDataSet? = null
private var storedIsDynamicColorsEnabled: Boolean? = null
private var storedBrowserAutofillDialogReshowTime: Instant? = null
@ -200,17 +200,6 @@ class FakeSettingsDiskSource : SettingsDiskSource {
emit(hasUserLoggedInOrCreatedAccount)
}
override var flightRecorderData: FlightRecorderDataSet?
get() = storedFlightRecorderData
set(value) {
storedFlightRecorderData = value
mutableFlightRecorderDataFlow.tryEmit(value)
}
override val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
get() = mutableFlightRecorderDataFlow
.onSubscription { emit(storedFlightRecorderData) }
override var browserAutofillDialogReshowTime: Instant?
get() = storedBrowserAutofillDialogReshowTime
set(value) {
@ -486,13 +475,6 @@ class FakeSettingsDiskSource : SettingsDiskSource {
return storedAppResumeScreenData[userId]?.let { Json.decodeFromStringOrNull(it) }
}
/**
* Asserts that the stored [FlightRecorderDataSet] matches the [expected] one.
*/
fun assertFlightRecorderData(expected: FlightRecorderDataSet) {
assertEquals(expected, storedFlightRecorderData)
}
/**
* Asserts that the stored last sync time matches the [expected] one.
*/

View File

@ -2,13 +2,14 @@ package com.bitwarden.authenticator.data.platform.datasource.disk
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import kotlinx.coroutines.flow.Flow
/**
* Primary access point for general settings-related disk information.
*/
interface SettingsDiskSource {
interface SettingsDiskSource : FlightRecorderDiskSource {
/**
* The currently persisted app language (or `null` if not set).

View File

@ -5,6 +5,7 @@ import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.datasource.disk.BaseDiskSource
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
@ -32,8 +33,10 @@ private const val DEFAULT_ALERT_THRESHOLD_SECONDS = 7
*/
class SettingsDiskSourceImpl(
sharedPreferences: SharedPreferences,
flightRecorderDiskSource: FlightRecorderDiskSource,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
SettingsDiskSource {
SettingsDiskSource,
FlightRecorderDiskSource by flightRecorderDiskSource {
private val mutableAppThemeFlow =
bufferedMutableSharedFlow<AppTheme>(replay = 1)

View File

@ -5,6 +5,7 @@ import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagOver
import com.bitwarden.authenticator.data.platform.datasource.disk.FeatureFlagOverrideDiskSourceImpl
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSource
import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSourceImpl
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.data.datasource.disk.di.UnencryptedPreferences
import dagger.Module
import dagger.Provides
@ -23,8 +24,12 @@ object PlatformDiskModule {
@Singleton
fun provideSettingsDiskSource(
@UnencryptedPreferences sharedPreferences: SharedPreferences,
flightRecorderDiskSource: FlightRecorderDiskSource,
): SettingsDiskSource =
SettingsDiskSourceImpl(sharedPreferences = sharedPreferences)
SettingsDiskSourceImpl(
sharedPreferences = sharedPreferences,
flightRecorderDiskSource = flightRecorderDiskSource,
)
@Provides
@Singleton

View File

@ -12,6 +12,7 @@ import com.bitwarden.network.interceptor.BaseUrlsProvider
import com.bitwarden.network.model.AuthTokenData
import com.bitwarden.network.model.BitwardenServiceClientConfig
import com.bitwarden.network.service.ConfigService
import com.bitwarden.network.service.DownloadService
import com.bitwarden.network.ssl.CertificateProvider
import dagger.Module
import dagger.Provides
@ -71,4 +72,10 @@ object PlatformNetworkModule {
},
),
)
@Provides
@Singleton
fun provideDownloadService(
bitwardenServiceClient: BitwardenServiceClient,
): DownloadService = bitwardenServiceClient.downloadService
}

View File

@ -2,6 +2,7 @@ package com.bitwarden.authenticator.data.platform.repository
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@ -9,7 +10,7 @@ import kotlinx.coroutines.flow.StateFlow
/**
* Provides an API for observing and modifying settings state.
*/
interface SettingsRepository {
interface SettingsRepository : FlightRecorderManager {
/**
* The [AppLanguage] for the current user.

View File

@ -5,6 +5,7 @@ import com.bitwarden.authenticator.data.platform.datasource.disk.SettingsDiskSou
import com.bitwarden.authenticator.ui.platform.feature.settings.appearance.model.AppLanguage
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
import com.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@ -20,8 +21,10 @@ private val DEFAULT_IS_SCREEN_CAPTURE_ALLOWED = BuildConfig.DEBUG
*/
class SettingsRepositoryImpl(
private val settingsDiskSource: SettingsDiskSource,
flightRecorderManager: FlightRecorderManager,
dispatcherManager: DispatcherManager,
) : SettingsRepository {
) : SettingsRepository,
FlightRecorderManager by flightRecorderManager {
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)

View File

@ -7,6 +7,7 @@ import com.bitwarden.authenticator.data.platform.repository.DebugMenuRepositoryI
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.bitwarden.authenticator.data.platform.repository.SettingsRepositoryImpl
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
import com.bitwarden.data.repository.ServerConfigRepository
import dagger.Module
import dagger.Provides
@ -25,10 +26,12 @@ object PlatformRepositoryModule {
@Singleton
fun provideSettingsRepository(
settingsDiskSource: SettingsDiskSource,
flightRecorderManager: FlightRecorderManager,
dispatcherManager: DispatcherManager,
): SettingsRepository =
SettingsRepositoryImpl(
settingsDiskSource = settingsDiskSource,
flightRecorderManager = flightRecorderManager,
dispatcherManager = dispatcherManager,
)

View File

@ -1,6 +1,8 @@
package com.bitwarden.authenticator.ui.platform.manager.di
import com.bitwarden.authenticator.ui.platform.manager.AuthenticatorBuildInfoManagerImpl
import com.bitwarden.authenticator.ui.platform.model.SnackbarRelay
import com.bitwarden.core.data.manager.BuildInfoManager
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
import com.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManagerImpl
@ -16,6 +18,10 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class PlatformUiManagerModule {
@Provides
@Singleton
fun provideBuildInfoManager(): BuildInfoManager = AuthenticatorBuildInfoManagerImpl()
@Provides
@Singleton
fun provideSnackbarRelayManager(

View File

@ -4,6 +4,7 @@ import androidx.core.content.edit
import app.cash.turbine.test
import com.bitwarden.authenticator.ui.platform.feature.settings.data.model.DefaultSaveOption
import com.bitwarden.data.datasource.disk.base.FakeSharedPreferences
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
@ -15,8 +16,9 @@ class SettingDiskSourceTest {
private val sharedPreferences: FakeSharedPreferences = FakeSharedPreferences()
private val settingDiskSource = SettingsDiskSourceImpl(
sharedPreferences,
private val settingDiskSource: SettingsDiskSource = SettingsDiskSourceImpl(
sharedPreferences = sharedPreferences,
flightRecorderDiskSource = mockk(),
)
@Test

View File

@ -23,8 +23,9 @@ class SettingsRepositoryTest {
every { getAlertThresholdSeconds() } returns 7
}
private val settingsRepository = SettingsRepositoryImpl(
private val settingsRepository: SettingsRepository = SettingsRepositoryImpl(
settingsDiskSource = settingsDiskSource,
flightRecorderManager = mockk(),
dispatcherManager = FakeDispatcherManager(),
)

View File

@ -0,0 +1,33 @@
package com.bitwarden.data.datasource.disk
import android.content.SharedPreferences
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.json.Json
private const val FLIGHT_RECORDER_KEY = "flightRecorderData"
/**
* Primary implementation of [FlightRecorderDiskSource].
*/
internal class FlightRecorderDiskSourceImpl(
private val json: Json,
sharedPreferences: SharedPreferences,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
FlightRecorderDiskSource {
private val mutableFlightRecorderDataFlow = bufferedMutableSharedFlow<FlightRecorderDataSet?>()
override var flightRecorderData: FlightRecorderDataSet?
get() = getString(key = FLIGHT_RECORDER_KEY)
?.let { json.decodeFromStringOrNull<FlightRecorderDataSet>(it) }
set(value) {
putString(key = FLIGHT_RECORDER_KEY, value = value?.let { json.encodeToString(it) })
mutableFlightRecorderDataFlow.tryEmit(value)
}
override val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
get() = mutableFlightRecorderDataFlow.onSubscription { emit(flightRecorderData) }
}

View File

@ -1,8 +1,10 @@
package com.bitwarden.data.datasource.disk.di
import android.content.SharedPreferences
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.data.datasource.disk.ConfigDiskSource
import com.bitwarden.data.datasource.disk.ConfigDiskSourceImpl
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSourceImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -26,4 +28,15 @@ object DiskModule {
sharedPreferences = sharedPreferences,
json = json,
)
@Provides
@Singleton
fun provideFlightRecorderDiskSource(
@UnencryptedPreferences sharedPreferences: SharedPreferences,
json: Json,
): FlightRecorderDiskSource =
FlightRecorderDiskSourceImpl(
sharedPreferences = sharedPreferences,
json = json,
)
}

View File

@ -3,12 +3,15 @@ package com.bitwarden.data.manager.di
import android.content.Context
import com.bitwarden.core.data.manager.BuildInfoManager
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
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.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.network.service.DownloadService
@ -45,6 +48,22 @@ object DataManagerModule {
dispatcherManager = dispatcherManager,
)
@Provides
@Singleton
fun provideFlightRecorderManager(
@ApplicationContext context: Context,
clock: Clock,
dispatcherManager: DispatcherManager,
flightRecorderDiskSource: FlightRecorderDiskSource,
flightRecorderWriter: FlightRecorderWriter,
): FlightRecorderManager = FlightRecorderManagerImpl(
context = context,
clock = clock,
dispatcherManager = dispatcherManager,
flightRecorderDiskSource = flightRecorderDiskSource,
flightRecorderWriter = flightRecorderWriter,
)
@Provides
@Singleton
fun provideFlightRecorderWriter(

View File

@ -1,12 +1,8 @@
package com.bitwarden.data.manager.flightrecorder
import android.content.Context
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
import com.bitwarden.data.manager.model.FlightRecorderDuration
import kotlinx.coroutines.flow.StateFlow
import java.time.Clock
/**
* Manager class that handles recording logs for the flight recorder.
@ -46,24 +42,4 @@ interface FlightRecorderManager {
* Deletes the raw log files and metadata.
*/
fun deleteAllLogs()
@Suppress("UndocumentedPublicClass")
companion object {
/**
* Creates a new instance of the [FlightRecorderManager].
*/
fun create(
context: Context,
clock: Clock,
flightRecorderDiskSource: FlightRecorderDiskSource,
flightRecorderWriter: FlightRecorderWriter,
dispatcherManager: DispatcherManager,
): FlightRecorderManager = FlightRecorderManagerImpl(
context = context,
clock = clock,
flightRecorderDiskSource = flightRecorderDiskSource,
flightRecorderWriter = flightRecorderWriter,
dispatcherManager = dispatcherManager,
)
}
}

View File

@ -0,0 +1,120 @@
package com.bitwarden.data.datasource.disk
import androidx.core.content.edit
import app.cash.turbine.test
import com.bitwarden.core.data.util.assertJsonEquals
import com.bitwarden.core.di.CoreModule
import com.bitwarden.data.datasource.disk.base.FakeSharedPreferences
import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
class FlightRecorderDiskSourceTest {
private val fakeSharedPreferences = FakeSharedPreferences()
private val json = CoreModule.providesJson()
private val flightRecorderDiskSource = FlightRecorderDiskSourceImpl(
sharedPreferences = fakeSharedPreferences,
json = json,
)
@Test
fun `flightRecorderData should pull from SharedPreferences`() {
val flightRecorderKey = "bwPreferencesStorage:flightRecorderData"
val encodedData = """
{
"data": [
{
"id": "51",
"fileName": "flight_recorder_2025-04-03_14-22-40",
"startTime": 1744059882,
"duration": 3600,
"isActive": false
}
]
}
"""
.trimIndent()
val expected = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "51",
fileName = "flight_recorder_2025-04-03_14-22-40",
startTimeMs = 1_744_059_882L,
durationMs = 3_600L,
isActive = false,
),
),
)
// Verify initial value is null and disk source matches shared preferences.
assertNull(fakeSharedPreferences.getString(flightRecorderKey, null))
assertNull(flightRecorderDiskSource.flightRecorderData)
// Updating the shared preferences should update disk source.
fakeSharedPreferences.edit { putString(flightRecorderKey, encodedData) }
val actual = flightRecorderDiskSource.flightRecorderData
assertEquals(expected, actual)
}
@Test
fun `flightRecorderDataFlow should react to changes in isFlightRecorderEnabled`() = runTest {
val expected = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "52",
fileName = "flight_recorder_2025-04-03_14-22-40",
startTimeMs = 1_744_059_882L,
durationMs = 3_600L,
isActive = true,
),
),
)
flightRecorderDiskSource.flightRecorderDataFlow.test {
// The initial values of the Flow and the property are in sync
assertNull(flightRecorderDiskSource.flightRecorderData)
assertNull(awaitItem())
flightRecorderDiskSource.flightRecorderData = expected
assertEquals(expected, awaitItem())
flightRecorderDiskSource.flightRecorderData = null
assertNull(awaitItem())
}
}
@Test
fun `setting flightRecorderData should update SharedPreferences`() {
val flightRecorderKey = "bwPreferencesStorage:flightRecorderData"
val data = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "53",
fileName = "flight_recorder_2025-04-03_14-22-40",
startTimeMs = 1_744_059_882L,
durationMs = 3_600L,
isActive = true,
),
),
)
val expected = """
{
"data": [
{
"id": "53",
"fileName": "flight_recorder_2025-04-03_14-22-40",
"startTime": 1744059882,
"duration": 3600,
"isActive": true
}
]
}
"""
.trimIndent()
flightRecorderDiskSource.flightRecorderData = data
val actual = fakeSharedPreferences.getString(flightRecorderKey, null)
assertJsonEquals(expected, actual!!)
}
}