PM-20133: Add initial logging logic (#5029)

This commit is contained in:
David Perez
2025-04-11 12:33:06 -05:00
committed by GitHub
parent b72da1ba69
commit 12ce1e5229
13 changed files with 730 additions and 2 deletions

View File

@@ -55,6 +55,10 @@ import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardMan
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManagerImpl
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderManager
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderManagerImpl
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderWriter
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderWriterImpl
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManager
import com.x8bit.bitwarden.data.platform.manager.garbage.GarbageCollectionManagerImpl
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
@@ -70,6 +74,7 @@ import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.data.vault.manager.VaultLockManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import dagger.Module
@@ -94,6 +99,34 @@ object PlatformManagerModule {
application: Application,
): AppStateManager = AppStateManagerImpl(application = application)
@Provides
@Singleton
fun provideFlightRecorderWriter(
clock: Clock,
fileManager: FileManager,
dispatcherManager: DispatcherManager,
): FlightRecorderWriter = FlightRecorderWriterImpl(
clock = clock,
fileManager = fileManager,
dispatcherManager = dispatcherManager,
)
@Provides
@Singleton
fun provideFlightRecorderManager(
@ApplicationContext context: Context,
clock: Clock,
dispatcherManager: DispatcherManager,
settingsDiskSource: SettingsDiskSource,
flightRecorderWriter: FlightRecorderWriter,
): FlightRecorderManager = FlightRecorderManagerImpl(
context = context,
clock = clock,
dispatcherManager = dispatcherManager,
settingsDiskSource = settingsDiskSource,
flightRecorderWriter = flightRecorderWriter,
)
@Provides
@Singleton
fun provideAuthenticatorBridgeProcessor(

View File

@@ -0,0 +1,40 @@
package com.x8bit.bitwarden.data.platform.manager.flightrecorder
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration
import kotlinx.coroutines.flow.StateFlow
/**
* Manager class that handles recording logs for the flight recorder.
*/
interface FlightRecorderManager {
/**
* Returns a set of all flight recorder data currently stored on the device.
*/
val flightRecorderData: FlightRecorderDataSet
/**
* Tracks changes to [FlightRecorderDataSet].
*/
val flightRecorderDataFlow: StateFlow<FlightRecorderDataSet>
/**
* Starts the flight recorder for the given [duration].
*/
fun startFlightRecorder(duration: FlightRecorderDuration)
/**
* Cancels the active flight recorder if one is currently active.
*/
fun endFlightRecorder()
/**
* Deletes the raw log file and metadata associated with the [data].
*/
fun deleteLog(data: FlightRecorderDataSet.FlightRecorderData)
/**
* Deletes the raw log files and metadata.
*/
fun deleteAllLogs()
}

View File

@@ -0,0 +1,193 @@
package com.x8bit.bitwarden.data.platform.manager.flightrecorder
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.bitwarden.data.manager.DispatcherManager
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.repository.model.FlightRecorderDuration
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.Clock
import java.util.UUID
/**
* The default implementation of the [FlightRecorderManager].
*/
internal class FlightRecorderManagerImpl(
private val context: Context,
private val clock: Clock,
private val settingsDiskSource: SettingsDiskSource,
private val flightRecorderWriter: FlightRecorderWriter,
dispatcherManager: DispatcherManager,
) : FlightRecorderManager {
private val unconfinedScope = CoroutineScope(context = dispatcherManager.unconfined)
private val ioScope = CoroutineScope(context = dispatcherManager.io)
private var cancellationJob: Job = Job().apply { complete() }
private val flightRecorderTree = FlightRecorderTree()
override val flightRecorderData: FlightRecorderDataSet
get() = settingsDiskSource.flightRecorderData ?: FlightRecorderDataSet(data = emptySet())
override val flightRecorderDataFlow: StateFlow<FlightRecorderDataSet>
get() = settingsDiskSource
.flightRecorderDataFlow
.map { it ?: FlightRecorderDataSet(data = emptySet()) }
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = flightRecorderData,
)
init {
// Always plant the tree, it will not log anything if the flight recorder is off.
Timber.plant(flightRecorderTree)
flightRecorderDataFlow
.onEach { it.configureFlightRecorder() }
.launchIn(scope = unconfinedScope)
context.registerReceiver(
ScreenStateBroadcastReceiver(),
IntentFilter(Intent.ACTION_SCREEN_ON),
)
}
override fun startFlightRecorder(duration: FlightRecorderDuration) {
val startTime = clock.instant()
val originalData = flightRecorderData
settingsDiskSource.flightRecorderData = originalData.copy(
data = originalData
.data
.map { it.copy(isActive = false) }
.toMutableSet()
.apply {
val formattedTime = startTime.toFormattedPattern(
pattern = "yyyy-MM-dd_HH-mm-ss",
clock = clock,
)
add(
element = FlightRecorderDataSet.FlightRecorderData(
id = UUID.randomUUID().toString(),
fileName = "flight_recorder_$formattedTime",
startTimeMs = startTime.toEpochMilli(),
durationMs = duration.milliseconds,
isActive = true,
),
)
},
)
}
override fun endFlightRecorder() {
val originalData = flightRecorderData
settingsDiskSource.flightRecorderData = originalData.copy(
data = originalData.data.map { it.copy(isActive = false) }.toSet(),
)
}
override fun deleteAllLogs() {
val activeLog = flightRecorderData.activeFlightRecorderData
val inactiveDataset = flightRecorderData.copy(
data = flightRecorderData.data.filterNot { it.isActive }.toSet(),
)
// Clear everything but the active log.
settingsDiskSource.flightRecorderData = activeLog?.let {
FlightRecorderDataSet(data = setOf(it))
}
// Clear all logs but the active one.
ioScope.launch { flightRecorderWriter.deleteLogs(dataset = inactiveDataset) }
}
override fun deleteLog(data: FlightRecorderDataSet.FlightRecorderData) {
if (data.isActive) return
val originalData = flightRecorderData
settingsDiskSource.flightRecorderData = originalData.copy(
data = originalData.data.filterNot { it == data }.toSet(),
)
ioScope.launch { flightRecorderWriter.deleteLog(data = data) }
}
private fun cancelCancellationJob() {
cancellationJob.cancel()
}
private fun startCancellationJob(data: FlightRecorderDataSet.FlightRecorderData) {
cancelCancellationJob()
cancellationJob = unconfinedScope.launch {
val endTimeMs = data.startTimeMs + data.durationMs
val remainingTimeMs = endTimeMs - clock.instant().toEpochMilli()
delay(timeMillis = remainingTimeMs)
endFlightRecorder()
}
}
/**
* Configures the the flight recorder based on the [FlightRecorderDataSet].
*
* This sets the log to ensure that the log file is being updated properly as well as starting
* the cancellation job.
*/
private fun FlightRecorderDataSet.configureFlightRecorder() {
this
.activeFlightRecorderData
?.let {
// Set the file data to be recorded too.
flightRecorderTree.flightRecorderData = it
startCancellationJob(data = it)
}
?: run {
// Clear the file data to stop recording.
flightRecorderTree.flightRecorderData = null
cancelCancellationJob()
}
}
private inner class FlightRecorderTree : Timber.Tree() {
var flightRecorderData: FlightRecorderDataSet.FlightRecorderData? = null
set(value) {
value?.let {
ioScope.launch { flightRecorderWriter.getOrCreateLogFile(data = it) }
}
field = value
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
flightRecorderData?.let {
ioScope.launch {
flightRecorderWriter.writeToLog(
data = it,
priority = priority,
tag = tag,
message = message,
throwable = t,
)
}
}
}
}
/**
* A custom [BroadcastReceiver] that listens for when the screen is powered on and restarts the
* cancellation job to ensure they complete at the correct time.
*
* This is necessary because the [delay] function in a coroutine will not keep accurate time
* when the screen is off. We do not cancel the job when the screen is off, this allows the
* job to complete as-soon-as possible if the screen is powered off for an extended period.
*/
private inner class ScreenStateBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
flightRecorderData.configureFlightRecorder()
}
}
}

View File

@@ -0,0 +1,36 @@
package com.x8bit.bitwarden.data.platform.manager.flightrecorder
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import java.io.File
/**
* Helper for creating and writing log files.
*/
interface FlightRecorderWriter {
/**
* Deletes the file associated with the [data].
*/
suspend fun deleteLog(data: FlightRecorderDataSet.FlightRecorderData)
/**
* Deletes all files associated with the [dataset].
*/
suspend fun deleteLogs(dataset: FlightRecorderDataSet)
/**
* Creates or retrieves already created log files. If a new file is created, it will be
* pre-populated with metadata.
*/
suspend fun getOrCreateLogFile(data: FlightRecorderDataSet.FlightRecorderData): Result<File>
/**
* Formats the data and writes it to the log file.
*/
suspend fun writeToLog(
data: FlightRecorderDataSet.FlightRecorderData,
priority: Int,
tag: String?,
message: String,
throwable: Throwable?,
)
}

View File

@@ -0,0 +1,141 @@
package com.x8bit.bitwarden.data.platform.manager.flightrecorder
import android.os.Build
import android.util.Log
import com.bitwarden.core.annotation.OmitFromCoverage
import com.bitwarden.data.manager.DispatcherManager
import com.x8bit.bitwarden.BuildConfig
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import com.x8bit.bitwarden.data.vault.manager.FileManager
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter
import java.io.StringWriter
import java.time.Clock
import java.time.Instant
import kotlin.time.Duration.Companion.milliseconds
private const val LOG_TIME_PATTERN: String = "yyyy-MM-dd HH:mm:ss:SSS"
/**
* The default implementation of the [FlightRecorderWriter].
*/
@OmitFromCoverage
class FlightRecorderWriterImpl(
private val clock: Clock,
private val fileManager: FileManager,
private val dispatcherManager: DispatcherManager,
) : FlightRecorderWriter {
override suspend fun deleteLog(data: FlightRecorderDataSet.FlightRecorderData) {
fileManager.delete(File(File(fileManager.logsDirectory), data.fileName))
}
override suspend fun deleteLogs(dataset: FlightRecorderDataSet) {
fileManager.delete(
files = dataset
.data
.map { File(File(fileManager.logsDirectory), it.fileName) }
.toTypedArray(),
)
}
override suspend fun getOrCreateLogFile(
data: FlightRecorderDataSet.FlightRecorderData,
): Result<File> = withContext(dispatcherManager.io) {
runCatching {
val logFolder = File(fileManager.logsDirectory)
if (!logFolder.exists()) logFolder.mkdirs()
val logFile = File(logFolder, data.fileName)
if (!logFile.exists()) {
logFile.createNewFile()
val startTime = Instant
.ofEpochMilli(data.startTimeMs)
.toFormattedPattern(pattern = LOG_TIME_PATTERN, clock = clock)
val appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
val operatingSystem = "${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT})"
// Upon creating the new file, we pre-populate it with basic data
BufferedWriter(FileWriter(logFile, true)).use { bw ->
bw.append("Bitwarden Android")
bw.newLine()
bw.append("Log Start Time: $startTime")
bw.newLine()
bw.append("Log Duration: ${data.durationMs.milliseconds}")
bw.newLine()
bw.append("App Version: $appVersion")
bw.newLine()
bw.append("Build: ${BuildConfig.BUILD_TYPE}/${BuildConfig.FLAVOR}")
bw.newLine()
bw.append("Operating System: $operatingSystem")
bw.newLine()
bw.append("Device: ${Build.BRAND} ${Build.MODEL}")
bw.newLine()
}
}
logFile
}
}
override suspend fun writeToLog(
data: FlightRecorderDataSet.FlightRecorderData,
priority: Int,
tag: String?,
message: String,
throwable: Throwable?,
) {
val logFile = getOrCreateLogFile(data = data).getOrNull() ?: return
val formattedTime = clock
.instant()
.toFormattedPattern(pattern = LOG_TIME_PATTERN, clock = clock)
withContext(context = dispatcherManager.io) {
runCatching {
BufferedWriter(FileWriter(logFile, true)).use { bw ->
bw.append(formattedTime)
bw.append(" ")
bw.append(priority.logLevel)
tag?.let {
bw.append(" ")
bw.append(it)
}
bw.append(" ")
bw.append(message)
throwable?.let {
bw.append(" ")
bw.append(it.getStackTraceString())
}
bw.newLine()
}
}
}
}
}
/**
* Helper function modifier from the [Timber] library.
*/
@Suppress("MagicNumber")
private fun Throwable.getStackTraceString(): String {
// Don't replace this with Log.getStackTraceString() - it hides
// UnknownHostException, which is not what we want.
return StringWriter(256).use { sw ->
PrintWriter(sw, false).use { pw ->
this.printStackTrace(pw)
pw.flush()
}
sw.toString()
}
}
private val Int.logLevel: String
get() = when (this) {
Log.VERBOSE -> "VERBOSE"
Log.DEBUG -> "DEBUG"
Log.INFO -> "INFO"
Log.WARN -> "WARNING"
Log.ERROR -> "ERROR"
Log.ASSERT -> "ASSERT"
else -> "UNKNOWN"
}

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.repository
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderManager
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
@@ -17,7 +18,7 @@ import javax.crypto.Cipher
* Provides an API for observing and modifying settings state.
*/
@Suppress("TooManyFunctions")
interface SettingsRepository {
interface SettingsRepository : FlightRecorderManager {
/**
* The [AppLanguage] for the current user.
*/

View File

@@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderManager
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
@@ -49,10 +50,12 @@ class SettingsRepositoryImpl(
private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val vaultSdkSource: VaultSdkSource,
flightRecorderManager: FlightRecorderManager,
accessibilityEnabledManager: AccessibilityEnabledManager,
policyManager: PolicyManager,
dispatcherManager: DispatcherManager,
) : SettingsRepository {
) : SettingsRepository,
FlightRecorderManager by flightRecorderManager {
private val activeUserId: String? get() = authDiskSource.userState?.activeUserId
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)

View File

@@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.EnvironmentDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.FeatureFlagOverrideDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderManager
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepository
import com.x8bit.bitwarden.data.platform.repository.AuthenticatorBridgeRepositoryImpl
import com.x8bit.bitwarden.data.platform.repository.DebugMenuRepository
@@ -75,6 +76,7 @@ object PlatformRepositoryModule {
accessibilityEnabledManager: AccessibilityEnabledManager,
dispatcherManager: DispatcherManager,
policyManager: PolicyManager,
flightRecorderManager: FlightRecorderManager,
): SettingsRepository =
SettingsRepositoryImpl(
autofillManager = autofillManager,
@@ -85,6 +87,7 @@ object PlatformRepositoryModule {
accessibilityEnabledManager = accessibilityEnabledManager,
dispatcherManager = dispatcherManager,
policyManager = policyManager,
flightRecorderManager = flightRecorderManager,
)
@Provides

View File

@@ -16,6 +16,11 @@ interface FileManager {
*/
val filesDirectory: String
/**
* Absolute path to the private logs storage directory.
*/
val logsDirectory: String
/**
* Deletes [files] from disk.
*/

View File

@@ -32,6 +32,9 @@ class FileManagerImpl(
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() }

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
import kotlinx.serialization.json.Json
import org.junit.jupiter.api.Assertions.assertEquals
import java.time.Instant
/**
@@ -446,6 +447,13 @@ 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)
}
//region Private helper functions
private fun getMutableLastSyncTimeFlow(
userId: String,

View File

@@ -0,0 +1,259 @@
package com.x8bit.bitwarden.data.platform.manager.flightrecorder
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import app.cash.turbine.test
import com.bitwarden.core.data.util.asSuccess
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.datasource.disk.model.FlightRecorderDataSet
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.runs
import io.mockk.slot
import io.mockk.unmockkStatic
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.io.File
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
import java.util.UUID
class FlightRecorderManagerTest {
private val broadcastReceiver = slot<BroadcastReceiver>()
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private val context: Context = mockk {
every { registerReceiver(capture(broadcastReceiver), any()) } returns null
}
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
private val flightRecorderWriter = mockk<FlightRecorderWriter> {
coEvery { getOrCreateLogFile(data = any()) } returns mockk<File>().asSuccess()
coEvery { deleteLogs(dataset = any()) } just runs
coEvery { deleteLog(data = any()) } just runs
coEvery {
writeToLog(
data = any(),
priority = any(),
tag = any(),
message = any(),
throwable = any(),
)
} just runs
}
private val fakeDispatcherManager = FakeDispatcherManager()
private val flightRecorder = FlightRecorderManagerImpl(
context = context,
clock = FIXED_CLOCK,
settingsDiskSource = fakeSettingsDiskSource,
flightRecorderWriter = flightRecorderWriter,
dispatcherManager = fakeDispatcherManager,
)
@BeforeEach
fun setup() {
mockkStatic(UUID::class)
every { UUID.randomUUID().toString() } returns "mockUUID"
}
@AfterEach
fun tearDown() {
unmockkStatic(UUID::class)
}
@Test
fun `flightRecorderData should pull from and update SettingsDiskSource`() {
fakeSettingsDiskSource.flightRecorderData = null
assertEquals(FlightRecorderDataSet(data = emptySet()), flightRecorder.flightRecorderData)
val expected = mockk<FlightRecorderDataSet> {
every { activeFlightRecorderData } returns null
}
fakeSettingsDiskSource.flightRecorderData = expected
assertEquals(expected, flightRecorder.flightRecorderData)
}
@Test
fun `flightRecorderDataFlow should react to changes in SettingsDiskSource`() = runTest {
fakeSettingsDiskSource.flightRecorderData = null
flightRecorder
.flightRecorderDataFlow
.test {
assertEquals(FlightRecorderDataSet(data = emptySet()), awaitItem())
val expected = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "mockUUID",
fileName = "flight_recorder_2023-10-27_12-00-00",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = FlightRecorderDuration.ONE_HOUR.milliseconds,
isActive = false,
),
),
)
fakeSettingsDiskSource.flightRecorderData = expected
assertEquals(expected, awaitItem())
fakeSettingsDiskSource.flightRecorderData = null
assertEquals(FlightRecorderDataSet(data = emptySet()), awaitItem())
}
}
@Test
fun `startFlightRecorder should properly update SettingsDiskSource`() {
fakeSettingsDiskSource.flightRecorderData = null
flightRecorder.startFlightRecorder(duration = FlightRecorderDuration.ONE_HOUR)
fakeSettingsDiskSource.assertFlightRecorderData(
expected = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "mockUUID",
fileName = "flight_recorder_2023-10-27_12-00-00",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = FlightRecorderDuration.ONE_HOUR.milliseconds,
isActive = true,
),
),
),
)
}
@Suppress("MaxLineLength")
@Test
fun `endFlightRecorder should set the active log to inactive and update the SettingsDiskSource`() {
val data = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "40",
fileName = "fileName1",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = 60L,
isActive = true,
),
FlightRecorderDataSet.FlightRecorderData(
id = "50",
fileName = "fileName2",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = 60L,
isActive = false,
),
),
)
fakeSettingsDiskSource.flightRecorderData = data
flightRecorder.endFlightRecorder()
fakeSettingsDiskSource.assertFlightRecorderData(
expected = FlightRecorderDataSet(
data = setOf(
FlightRecorderDataSet.FlightRecorderData(
id = "40",
fileName = "fileName1",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = 60L,
isActive = false,
),
FlightRecorderDataSet.FlightRecorderData(
id = "50",
fileName = "fileName2",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = 60L,
isActive = false,
),
),
),
)
}
@Test
fun `deleteAllLogs should clear the metadata for all non-active logs and call deleteLogs`() {
val inactiveData = FlightRecorderDataSet.FlightRecorderData(
id = "50",
fileName = "fileName1",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = 60L,
isActive = false,
)
val activeData = FlightRecorderDataSet.FlightRecorderData(
id = "50",
fileName = "fileName2",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = 60L,
isActive = true,
)
val activeDataset = FlightRecorderDataSet(data = setOf(activeData))
val inactiveDataset = FlightRecorderDataSet(data = setOf(inactiveData))
fakeSettingsDiskSource.flightRecorderData = FlightRecorderDataSet(
data = setOf(activeData, inactiveData),
)
flightRecorder.deleteAllLogs()
coVerify(exactly = 1) {
flightRecorderWriter.deleteLogs(dataset = inactiveDataset)
}
fakeSettingsDiskSource.assertFlightRecorderData(expected = activeDataset)
}
@Test
fun `deleteLog with active log should do nothing`() {
val data = FlightRecorderDataSet.FlightRecorderData(
id = "50",
fileName = "fileName1",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = 60L,
isActive = true,
)
val dataset = FlightRecorderDataSet(data = setOf(data))
fakeSettingsDiskSource.flightRecorderData = dataset
flightRecorder.deleteLog(data = data)
coVerify(exactly = 0) {
flightRecorderWriter.deleteLog(data = data)
}
fakeSettingsDiskSource.assertFlightRecorderData(expected = dataset)
}
@Test
fun `deleteLog with inactive log should clear the metadata for the log and call deleteLog`() {
val data = FlightRecorderDataSet.FlightRecorderData(
id = "50",
fileName = "fileName1",
startTimeMs = FIXED_CLOCK_TIME,
durationMs = 60L,
isActive = false,
)
fakeSettingsDiskSource.flightRecorderData = FlightRecorderDataSet(data = setOf(data))
flightRecorder.deleteLog(data = data)
coVerify(exactly = 1) {
flightRecorderWriter.deleteLog(data = data)
}
fakeSettingsDiskSource.assertFlightRecorderData(
expected = FlightRecorderDataSet(data = emptySet()),
)
}
}
private const val FIXED_CLOCK_TIME: Long = 1_698_408_000_000L
private val FIXED_CLOCK: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)

View File

@@ -24,6 +24,7 @@ import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManagerImpl
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
import com.x8bit.bitwarden.data.platform.error.NoActiveUserException
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.platform.manager.flightrecorder.FlightRecorderManager
import com.x8bit.bitwarden.data.platform.repository.model.BiometricsKeyResult
import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency
import com.x8bit.bitwarden.data.platform.repository.model.UriMatchType
@@ -70,6 +71,7 @@ class SettingsRepositoryTest {
getActivePoliciesFlow(type = PolicyTypeJson.MAXIMUM_VAULT_TIMEOUT)
} returns mutableActivePolicyFlow
}
private val flightRecorderManager = mockk<FlightRecorderManager>()
private val settingsRepository = SettingsRepositoryImpl(
autofillManager = autofillManager,
@@ -80,6 +82,7 @@ class SettingsRepositoryTest {
accessibilityEnabledManager = fakeAccessibilityEnabledManager,
dispatcherManager = FakeDispatcherManager(),
policyManager = policyManager,
flightRecorderManager = flightRecorderManager,
)
@BeforeEach