PM-20123: Add flight recorder metadata (#5024)

This commit is contained in:
David Perez authored and GitHub committed 2025-04-10 20:57:24 +00:00
1 parent dd0f08d759
commit d7f771990e
5 files changed
+191

No files matched your search

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
@@ -84,6 +85,16 @@ interface SettingsDiskSource {
*/
val hasUserLoggedInOrCreatedAccountFlow: Flow<Boolean?>
/**
* The current status of whether the flight recorder is enabled.
*/
var flightRecorderData: FlightRecorderDataSet?
/**
* Emits updates that track [flightRecorderData].
*/
val flightRecorderDataFlow: Flow<FlightRecorderDataSet?>
/**
* Clears all the settings data for the given user.
*/
@@ -5,6 +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.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
@@ -45,6 +46,7 @@ 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"
/**
* Primary implementation of [SettingsDiskSource].
@@ -84,6 +86,8 @@ class SettingsDiskSourceImpl(
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableFlightRecorderDataFlow = bufferedMutableSharedFlow<FlightRecorderDataSet?>()
private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
@@ -189,6 +193,20 @@ 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 fun clearData(userId: String) {
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
storeVaultTimeoutAction(userId = userId, vaultTimeoutAction = null)
@@ -0,0 +1,46 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Persistable class containing all required data for a all instances of the flight recorder.
*/
@Serializable
data class FlightRecorderDataSet(
@SerialName("data")
val data: Set<FlightRecorderData>,
) {
/**
* Returns `true` is there is an active [FlightRecorderDataSet.FlightRecorderData].
*/
val hasActiveFlightRecorderData: Boolean
get() = this.activeFlightRecorderData != null
/**
* Retrieves tha active log if available.
*/
val activeFlightRecorderData: FlightRecorderData?
get() = this.data.find { it.isActive }
/**
* Persistable class containing all required data for a single instance of the flight recorder.
*/
@Serializable
data class FlightRecorderData(
@SerialName("id")
val id: String,
@SerialName("fileName")
val fileName: String,
@SerialName("startTime")
val startTimeMs: Long,
@SerialName("duration")
val durationMs: Long,
@SerialName("isActive")
val isActive: Boolean,
)
}
@@ -5,10 +5,12 @@ import app.cash.turbine.test
import com.bitwarden.core.data.util.decodeFromStringOrNull
import com.bitwarden.core.di.CoreModule
import com.bitwarden.data.datasource.disk.base.FakeSharedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
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.data.util.assertJsonEquals
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import kotlinx.coroutines.test.runTest
@@ -83,6 +85,104 @@ 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"
@@ -3,6 +3,7 @@ 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.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import com.x8bit.bitwarden.data.platform.manager.model.AppResumeScreenData
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
@@ -50,6 +51,9 @@ 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?>()
@@ -80,6 +84,7 @@ class FakeSettingsDiskSource : SettingsDiskSource {
private var createSendActionCount: Int? = null
private var hasSeenAddLoginCoachMark: Boolean? = null
private var hasSeenGeneratorCoachMark: Boolean? = null
private var storedFlightRecorderData: FlightRecorderDataSet? = null
private val mutableShowAutoFillSettingBadgeFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
@@ -173,6 +178,17 @@ 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 fun getAccountBiometricIntegrityValidity(
userId: String,
systemBioIntegrityState: String,