From 83f6dc6d8cc7c29ba71b4296bcce8f9f69f676c7 Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 17 Dec 2025 13:27:56 -0600 Subject: [PATCH] Add concrete FlightRecorderDiskSource --- .../datasource/disk/SettingsDiskSourceImpl.kt | 23 +--- .../datasource/disk/di/PlatformDiskModule.kt | 3 + .../manager/di/PlatformManagerModule.kt | 18 --- .../datasource/disk/SettingsDiskSourceTest.kt | 104 +-------------- .../disk/util/FakeSettingsDiskSource.kt | 25 ++-- .../datasource/disk/SettingsDiskSource.kt | 3 +- .../datasource/disk/SettingsDiskSourceImpl.kt | 5 +- .../datasource/disk/di/PlatformDiskModule.kt | 7 +- .../network/di/PlatformNetworkModule.kt | 7 + .../platform/repository/SettingsRepository.kt | 3 +- .../repository/SettingsRepositoryImpl.kt | 5 +- .../repository/di/PlatformRepositoryModule.kt | 3 + .../manager/di/PlatformUiManagerModule.kt | 6 + .../datasource/disk/SettingDiskSourceTest.kt | 6 +- .../repository/SettingsRepositoryTest.kt | 3 +- .../disk/FlightRecorderDiskSourceImpl.kt | 33 +++++ .../data/datasource/disk/di/DiskModule.kt | 13 ++ .../data/manager/di/DataManagerModule.kt | 19 +++ .../flightrecorder/FlightRecorderManager.kt | 24 ---- .../disk/FlightRecorderDiskSourceTest.kt | 120 ++++++++++++++++++ 20 files changed, 243 insertions(+), 187 deletions(-) create mode 100644 data/src/main/kotlin/com/bitwarden/data/datasource/disk/FlightRecorderDiskSourceImpl.kt create mode 100644 data/src/test/kotlin/com/bitwarden/data/datasource/disk/FlightRecorderDiskSourceTest.kt diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt index 369a551dd36..bc511f51aa2 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceImpl.kt @@ -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(replay = 1) private val mutableAppThemeFlow = bufferedMutableSharedFlow(replay = 1) @@ -92,8 +93,6 @@ class SettingsDiskSourceImpl( private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow() - private val mutableFlightRecorderDataFlow = bufferedMutableSharedFlow() - private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow() private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow() @@ -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(it) } - set(value) { - putString( - key = FLIGHT_RECORDER_KEY, - value = value?.let { json.encodeToString(it) }, - ) - mutableFlightRecorderDataFlow.tryEmit(value) - } - - override val flightRecorderDataFlow: Flow - get() = mutableFlightRecorderDataFlow.onSubscription { emit(flightRecorderData) } - override var browserAutofillDialogReshowTime: Instant? get() = getLong(key = BROWSER_AUTOFILL_DIALOG_RESHOW_TIME)?.let { Instant.ofEpochMilli(it) } set(value) { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt index b9aca699905..a06daf2f57a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/di/PlatformDiskModule.kt @@ -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 diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt index d456cc84fa2..1dff3bae484 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/platform/manager/di/PlatformManagerModule.kt @@ -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( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt index 7e171a067f9..a3966ac5f87 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/SettingsDiskSourceTest.kt @@ -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" diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt index e730a53f4cc..80255175f53 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/platform/datasource/disk/util/FakeSettingsDiskSource.kt @@ -2,7 +2,9 @@ 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.FlightRecorderDiskSource import com.bitwarden.data.datasource.disk.model.FlightRecorderDataSet +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 +21,11 @@ import java.time.Instant /** * Fake, memory-based implementation of [SettingsDiskSource]. */ -class FakeSettingsDiskSource : SettingsDiskSource { +class FakeSettingsDiskSource( + private val flightRecorderDiskSource: FakeFlightRecorderDiskSource = + FakeFlightRecorderDiskSource(), +) : SettingsDiskSource, + FlightRecorderDiskSource by flightRecorderDiskSource { private val mutableAppLanguageFlow = bufferedMutableSharedFlow(replay = 1) @@ -52,9 +58,6 @@ class FakeSettingsDiskSource : SettingsDiskSource { private val mutableShouldShowGeneratorCoachMarkFlow = bufferedMutableSharedFlow() - private val mutableFlightRecorderDataFlow = - bufferedMutableSharedFlow(replay = 1) - private var storedAppLanguage: AppLanguage? = null private var storedAppTheme: AppTheme = AppTheme.DEFAULT private val storedLastSyncTime = mutableMapOf() @@ -86,7 +89,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 +202,6 @@ class FakeSettingsDiskSource : SettingsDiskSource { emit(hasUserLoggedInOrCreatedAccount) } - override var flightRecorderData: FlightRecorderDataSet? - get() = storedFlightRecorderData - set(value) { - storedFlightRecorderData = value - mutableFlightRecorderDataFlow.tryEmit(value) - } - - override val flightRecorderDataFlow: Flow - get() = mutableFlightRecorderDataFlow - .onSubscription { emit(storedFlightRecorderData) } - override var browserAutofillDialogReshowTime: Instant? get() = storedBrowserAutofillDialogReshowTime set(value) { @@ -490,7 +481,7 @@ class FakeSettingsDiskSource : SettingsDiskSource { * Asserts that the stored [FlightRecorderDataSet] matches the [expected] one. */ fun assertFlightRecorderData(expected: FlightRecorderDataSet) { - assertEquals(expected, storedFlightRecorderData) + flightRecorderDiskSource.assertFlightRecorderData(expected) } /** diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSource.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSource.kt index 5c6c8faaec7..50ce0f95e5d 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSource.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSource.kt @@ -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). diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSourceImpl.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSourceImpl.kt index 1c41016b90d..7635c97b09a 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSourceImpl.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingsDiskSourceImpl.kt @@ -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(replay = 1) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/di/PlatformDiskModule.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/di/PlatformDiskModule.kt index fab3efe20bd..ac5453c491a 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/di/PlatformDiskModule.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/di/PlatformDiskModule.kt @@ -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 diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/di/PlatformNetworkModule.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/di/PlatformNetworkModule.kt index b1ea2c4d127..8364ed94fbc 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/di/PlatformNetworkModule.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/datasource/network/di/PlatformNetworkModule.kt @@ -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 } diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepository.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepository.kt index 0b8cb26b61e..0b13f955a48 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepository.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepository.kt @@ -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. diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryImpl.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryImpl.kt index 65594f8a0bb..16c2c6539b5 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryImpl.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryImpl.kt @@ -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) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/di/PlatformRepositoryModule.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/di/PlatformRepositoryModule.kt index 31004a605bf..79800f62ee7 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/di/PlatformRepositoryModule.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/data/platform/repository/di/PlatformRepositoryModule.kt @@ -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, ) diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/manager/di/PlatformUiManagerModule.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/manager/di/PlatformUiManagerModule.kt index 508b8a6b24b..3d5887cc2b3 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/manager/di/PlatformUiManagerModule.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/platform/manager/di/PlatformUiManagerModule.kt @@ -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( diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingDiskSourceTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingDiskSourceTest.kt index d0816078ccb..dc5c5d78f0d 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingDiskSourceTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/datasource/disk/SettingDiskSourceTest.kt @@ -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 diff --git a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryTest.kt b/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryTest.kt index 59dd3c75a17..cae6dcd624f 100644 --- a/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryTest.kt +++ b/authenticator/src/test/kotlin/com/bitwarden/authenticator/data/platform/repository/SettingsRepositoryTest.kt @@ -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(), ) diff --git a/data/src/main/kotlin/com/bitwarden/data/datasource/disk/FlightRecorderDiskSourceImpl.kt b/data/src/main/kotlin/com/bitwarden/data/datasource/disk/FlightRecorderDiskSourceImpl.kt new file mode 100644 index 00000000000..03d49e3cedf --- /dev/null +++ b/data/src/main/kotlin/com/bitwarden/data/datasource/disk/FlightRecorderDiskSourceImpl.kt @@ -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() + + override var flightRecorderData: FlightRecorderDataSet? + get() = getString(key = FLIGHT_RECORDER_KEY) + ?.let { json.decodeFromStringOrNull(it) } + set(value) { + putString(key = FLIGHT_RECORDER_KEY, value = value?.let { json.encodeToString(it) }) + mutableFlightRecorderDataFlow.tryEmit(value) + } + + override val flightRecorderDataFlow: Flow + get() = mutableFlightRecorderDataFlow.onSubscription { emit(flightRecorderData) } +} diff --git a/data/src/main/kotlin/com/bitwarden/data/datasource/disk/di/DiskModule.kt b/data/src/main/kotlin/com/bitwarden/data/datasource/disk/di/DiskModule.kt index 9af18332b71..e562abec84a 100644 --- a/data/src/main/kotlin/com/bitwarden/data/datasource/disk/di/DiskModule.kt +++ b/data/src/main/kotlin/com/bitwarden/data/datasource/disk/di/DiskModule.kt @@ -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, + ) } diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt b/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt index 02ab624573c..b70228247d7 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/di/DataManagerModule.kt @@ -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( diff --git a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderManager.kt b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderManager.kt index 40131f468c2..dfc588131f6 100644 --- a/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderManager.kt +++ b/data/src/main/kotlin/com/bitwarden/data/manager/flightrecorder/FlightRecorderManager.kt @@ -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, - ) - } } diff --git a/data/src/test/kotlin/com/bitwarden/data/datasource/disk/FlightRecorderDiskSourceTest.kt b/data/src/test/kotlin/com/bitwarden/data/datasource/disk/FlightRecorderDiskSourceTest.kt new file mode 100644 index 00000000000..6c76ec1ff58 --- /dev/null +++ b/data/src/test/kotlin/com/bitwarden/data/datasource/disk/FlightRecorderDiskSourceTest.kt @@ -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!!) + } +}