[BWA-61] Fix pre-existing detekt issues (#179)

This commit is contained in:
Patrick Honkonen
2024-09-03 09:05:20 -04:00
committed by GitHub
parent 5758b34dcf
commit e90c41a3b7
83 changed files with 654 additions and 787 deletions
+1 -1
View File
@@ -151,8 +151,8 @@ dependencies {
}
detekt {
autoCorrect = true
config.from(files("$rootDir/detekt-config.yml"))
baseline = file("$rootDir/detekt-baseline.xml")
}
kover {
@@ -5,6 +5,9 @@ import com.bitwarden.authenticator.data.platform.manager.CrashLogsManager
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
/**
* Custom application class.
*/
@HiltAndroidApp
class AuthenticatorApplication : Application() {
// Inject classes here that must be triggered on startup but are not otherwise consumed by
@@ -17,6 +17,9 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
/**
* Primary entry point for the application.
*/
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@@ -33,8 +36,8 @@ class MainActivity : AppCompatActivity() {
if (savedInstanceState == null) {
mainViewModel.trySendAction(
MainAction.ReceiveFirstIntent(
intent = intent
)
intent = intent,
),
)
}
@@ -42,11 +45,11 @@ class MainActivity : AppCompatActivity() {
val state by mainViewModel.stateFlow.collectAsStateWithLifecycle()
AuthenticatorTheme(
theme = state.theme
theme = state.theme,
) {
RootNavScreen(
onSplashScreenRemoved = { shouldShowSplashScreen = false },
onExitApplication = { finishAffinity() }
onExitApplication = { finishAffinity() },
)
}
}
@@ -56,7 +59,7 @@ class MainActivity : AppCompatActivity() {
super.onNewIntent(intent)
sanitizeIntent()
mainViewModel.trySendAction(
MainAction.ReceiveNewIntent(intent = intent)
MainAction.ReceiveNewIntent(intent = intent),
)
}
@@ -21,8 +21,8 @@ class MainViewModel @Inject constructor(
settingsRepository: SettingsRepository,
) : BaseViewModel<MainState, MainEvent, MainAction>(
MainState(
theme = settingsRepository.appTheme
)
theme = settingsRepository.appTheme,
),
) {
init {
@@ -71,7 +71,6 @@ class MainViewModel @Inject constructor(
) {
// RFU
}
}
/**
@@ -4,6 +4,9 @@ import android.os.SystemClock
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
import javax.inject.Inject
/**
* Default implementation of [AuthRepository].
*/
class AuthRepositoryImpl @Inject constructor(
private val authDiskSource: AuthDiskSource,
private val elapsedRealtimeMillisProvider: () -> Long = { SystemClock.elapsedRealtime() },
@@ -19,6 +19,6 @@ object AuthRepositoryModule {
fun provideAuthRepository(
authDiskSource: AuthDiskSource,
): AuthRepository = AuthRepositoryImpl(
authDiskSource = authDiskSource
authDiskSource = authDiskSource,
)
}
@@ -22,5 +22,4 @@ interface AuthenticatorDiskSource {
* Deletes an authenticator item from the data source with the given [itemId].
*/
suspend fun deleteItem(itemId: String)
}
@@ -7,6 +7,9 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.merge
import javax.inject.Inject
/**
* Default implementation of [AuthenticatorDiskSource].
*/
class AuthenticatorDiskSourceImpl @Inject constructor(
private val itemDao: ItemDao,
) : AuthenticatorDiskSource {
@@ -19,7 +22,7 @@ class AuthenticatorDiskSourceImpl @Inject constructor(
override fun getItems(): Flow<List<AuthenticatorItemEntity>> = merge(
forceItemsFlow,
itemDao.getAllItems()
itemDao.getAllItems(),
)
override suspend fun deleteItem(itemId: String) {
@@ -14,7 +14,7 @@ import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.Aut
*/
@Database(
entities = [
AuthenticatorItemEntity::class
AuthenticatorItemEntity::class,
],
autoMigrations = [
AutoMigration(from = 1, to = 2),
@@ -32,5 +32,4 @@ abstract class AuthenticatorDatabase : RoomDatabase() {
* Provide the DAO for accessing authenticator item data.
*/
abstract fun itemDao(): ItemDao
}
@@ -28,7 +28,7 @@ object AuthenticatorDiskModule {
.databaseBuilder(
context = app,
klass = AuthenticatorDatabase::class.java,
name = "authenticator_database"
name = "authenticator_database",
)
.fallbackToDestructiveMigration()
.addTypeConverter(AuthenticatorItemTypeConverter())
@@ -22,6 +22,9 @@ enum class AuthenticatorItemAlgorithm {
;
companion object {
/**
* Returns a [AuthenticatorItemAlgorithm] with a name matching [value], or null.
*/
fun fromStringOrNull(value: String): AuthenticatorItemAlgorithm? =
entries.find { it.name.equals(value, ignoreCase = true) }
}
@@ -42,6 +42,10 @@ data class AuthenticatorItemEntity(
@ColumnInfo(name = "favorite", defaultValue = "0")
val favorite: Boolean,
) {
/**
* Returns the OTP data in a string formatted to match the Google Authenticator specification,
* https://github.com/google/google-authenticator/wiki/Key-Uri-Format
*/
fun toOtpAuthUriString(): String {
return when (type) {
AuthenticatorItemType.TOTP -> {
@@ -1,7 +1,7 @@
package com.bitwarden.authenticator.data.authenticator.datasource.disk.entity
/**
* Enum representing the main type options for authenticator items.
* Enum representing the supported "type" options for authenticator items.
*/
enum class AuthenticatorItemType {
@@ -17,6 +17,10 @@ enum class AuthenticatorItemType {
;
companion object {
/**
* Returns the [AuthenticatorItemType] matching [value], or null.
*/
fun fromStringOrNull(value: String): AuthenticatorItemType? =
entries.find { it.name.equals(value, ignoreCase = true) }
}
@@ -16,5 +16,8 @@ interface AuthenticatorSdkSource {
time: DateTime,
): Result<TotpResponse>
/**
* Generate a random key for seeding biometrics.
*/
suspend fun generateBiometricsKey(): Result<String>
}
@@ -7,6 +7,9 @@ import com.bitwarden.generators.PasswordGeneratorRequest
import com.bitwarden.sdk.Client
import javax.inject.Inject
/**
* Default implementation of [AuthenticatorSdkSource].
*/
class AuthenticatorSdkSourceImpl @Inject constructor(
private val sdkClientManager: SdkClientManager,
) : AuthenticatorSdkSource {
@@ -38,11 +41,10 @@ class AuthenticatorSdkSourceImpl @Inject constructor(
minLowercase = null,
minUppercase = null,
minNumber = null,
minSpecial = null
)
minSpecial = null,
),
)
}
private suspend fun getClient(): Client = sdkClientManager.getOrCreateClient()
}
@@ -21,5 +21,4 @@ object AuthenticatorSdkModule {
fun provideAuthenticatorSdkSource(
sdkClientManager: SdkClientManager,
): AuthenticatorSdkSource = AuthenticatorSdkSourceImpl(sdkClientManager)
}
@@ -11,6 +11,9 @@ import java.io.ByteArrayOutputStream
*/
private const val BUFFER_SIZE: Int = 1024
/**
* Manages reading and writing files.
*/
class FileManagerImpl(
private val context: Context,
private val dispatcherManager: DispatcherManager,
@@ -40,8 +40,11 @@ interface TotpCodeManager {
const val STEAM_CODE_PREFIX = "steam://"
const val GOOGLE_EXPORT_PREFIX = "otpauth-migration://"
const val TOTP_DIGITS_DEFAULT = 6
const val TOTP_DIGITS_MIN = 5
const val TOTP_DIGITS_MAX = 10
const val STEAM_DIGITS_DEFAULT = 5
const val PERIOD_SECONDS_DEFAULT = 30
val TOTP_DIGITS_RANGE = TOTP_DIGITS_MIN..TOTP_DIGITS_MAX
val ALGORITHM_DEFAULT = AuthenticatorItemAlgorithm.SHA1
}
}
@@ -27,7 +27,7 @@ object AuthenticatorManagerModule {
fun providerFileManager(
@ApplicationContext context: Context,
dispatcherManager: DispatcherManager,
) : FileManager = FileManagerImpl(
): FileManager = FileManagerImpl(
context = context,
dispatcherManager = dispatcherManager,
)
@@ -41,7 +41,6 @@ object AuthenticatorManagerModule {
): TotpCodeManager = TotpCodeManagerImpl(
authenticatorSdkSource = authenticatorSdkSource,
dispatcherManager = dispatcherManager,
clock = clock
clock = clock,
)
}
@@ -8,12 +8,10 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.CreateIte
import com.bitwarden.authenticator.data.authenticator.repository.model.DeleteItemResult
import com.bitwarden.authenticator.data.authenticator.repository.model.ExportDataResult
import com.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult
import com.bitwarden.authenticator.data.authenticator.repository.model.UpdateItemRequest
import com.bitwarden.authenticator.data.authenticator.repository.model.UpdateItemResult
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportFileFormat
import com.bitwarden.authenticator.data.platform.repository.model.DataState
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportFormat
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportVaultFormat
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -82,18 +80,10 @@ interface AuthenticatorRepository {
*/
suspend fun hardDeleteItem(itemId: String): DeleteItemResult
/**
* Attempt to update a cipher.
*/
suspend fun updateItem(
itemId: String,
updateItemRequest: UpdateItemRequest,
): UpdateItemResult
/**
* Attempt to get the user's data for export.
*/
suspend fun exportVaultData(format: ExportFormat, fileUri: Uri): ExportDataResult
suspend fun exportVaultData(format: ExportVaultFormat, fileUri: Uri): ExportDataResult
/**
* Attempt to read the user's data from a file
@@ -12,8 +12,6 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.CreateIte
import com.bitwarden.authenticator.data.authenticator.repository.model.DeleteItemResult
import com.bitwarden.authenticator.data.authenticator.repository.model.ExportDataResult
import com.bitwarden.authenticator.data.authenticator.repository.model.TotpCodeResult
import com.bitwarden.authenticator.data.authenticator.repository.model.UpdateItemRequest
import com.bitwarden.authenticator.data.authenticator.repository.model.UpdateItemResult
import com.bitwarden.authenticator.data.platform.manager.DispatcherManager
import com.bitwarden.authenticator.data.platform.manager.imports.ImportManager
import com.bitwarden.authenticator.data.platform.manager.imports.model.ImportDataResult
@@ -22,7 +20,7 @@ import com.bitwarden.authenticator.data.platform.repository.model.DataState
import com.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
import com.bitwarden.authenticator.data.platform.repository.util.combineDataStates
import com.bitwarden.authenticator.data.platform.repository.util.map
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportFormat
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportVaultFormat
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -50,6 +48,10 @@ import javax.inject.Inject
*/
private const val STOP_TIMEOUT_DELAY_MS: Long = 5_000L
/**
* Default implementation of [AuthenticatorRepository].
*/
@Suppress("TooManyFunctions")
class AuthenticatorRepositoryImpl @Inject constructor(
private val authenticatorDiskSource: AuthenticatorDiskSource,
private val totpCodeManager: TotpCodeManager,
@@ -188,6 +190,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
mutableTotpCodeResultFlow.tryEmit(totpCodeResult)
}
@Suppress("TooGenericExceptionCaught")
override suspend fun createItem(item: AuthenticatorItemEntity): CreateItemResult {
return try {
authenticatorDiskSource.saveItem(item)
@@ -197,6 +200,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
}
}
@Suppress("TooGenericExceptionCaught")
override suspend fun addItems(vararg items: AuthenticatorItemEntity): CreateItemResult {
return try {
authenticatorDiskSource.saveItem(*items)
@@ -206,6 +210,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
}
}
@Suppress("TooGenericExceptionCaught")
override suspend fun hardDeleteItem(itemId: String): DeleteItemResult {
return try {
authenticatorDiskSource.deleteItem(itemId)
@@ -215,34 +220,13 @@ class AuthenticatorRepositoryImpl @Inject constructor(
}
}
override suspend fun updateItem(
itemId: String,
updateItemRequest: UpdateItemRequest,
): UpdateItemResult {
return try {
authenticatorDiskSource.saveItem(
AuthenticatorItemEntity(
id = itemId,
key = updateItemRequest.key,
accountName = updateItemRequest.accountName,
type = updateItemRequest.type,
period = updateItemRequest.period,
digits = updateItemRequest.digits,
issuer = updateItemRequest.issuer,
userId = null,
favorite = updateItemRequest.favorite
),
)
UpdateItemResult.Success
} catch (e: Exception) {
UpdateItemResult.Error(e.message)
}
}
override suspend fun exportVaultData(format: ExportFormat, fileUri: Uri): ExportDataResult {
override suspend fun exportVaultData(
format: ExportVaultFormat,
fileUri: Uri,
): ExportDataResult {
return when (format) {
ExportFormat.JSON -> encodeVaultDataToJson(fileUri)
ExportFormat.CSV -> encodeVaultDataToCsv(fileUri)
ExportVaultFormat.JSON -> encodeVaultDataToJson(fileUri)
ExportVaultFormat.CSV -> encodeVaultDataToCsv(fileUri)
}
}
@@ -254,12 +238,12 @@ class AuthenticatorRepositoryImpl @Inject constructor(
importManager
.import(
importFileFormat = format,
byteArray = it
byteArray = it,
)
}
.fold(
onSuccess = { it },
onFailure = { ImportDataResult.Error() }
onFailure = { ImportDataResult.Error() },
)
private suspend fun encodeVaultDataToCsv(fileUri: Uri): ExportDataResult {
@@ -292,7 +276,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
.firstOrNull()
.orEmpty()
.map { it.toExportJsonItem() },
)
),
)
return if (
@@ -35,5 +35,4 @@ object AuthenticatorRepositoryModule {
importManager,
dispatcherManager,
)
}
@@ -5,8 +5,13 @@ package com.bitwarden.authenticator.data.authenticator.repository.model
*/
sealed class ExportDataResult {
/**
* Data has been successfully exported.
*/
data object Success : ExportDataResult()
/**
* Data could not be exported.
*/
data object Error : ExportDataResult()
}
@@ -16,6 +16,7 @@ private const val ALERT_THRESHOLD_SECONDS_KEY = "$BASE_KEY:alertThresholdSeconds
private const val FIRST_LAUNCH_KEY = "$BASE_KEY:hasSeenWelcomeTutorial"
private const val CRASH_LOGGING_ENABLED_KEY = "$BASE_KEY:crashLoggingEnabled"
private const val SCREEN_CAPTURE_ALLOW_KEY = "screenCaptureAllowed"
private const val DEFAULT_ALERT_THRESHOLD_SECONDS = 7
/**
* Primary implementation of [SettingsDiskSource].
@@ -99,14 +100,14 @@ class SettingsDiskSourceImpl(
override fun storeAlertThresholdSeconds(thresholdSeconds: Int) {
putInt(
ALERT_THRESHOLD_SECONDS_KEY,
thresholdSeconds
thresholdSeconds,
)
mutableAlertThresholdSecondsFlow.tryEmit(thresholdSeconds)
}
override fun getAlertThresholdSeconds(): Int {
return getInt(ALERT_THRESHOLD_SECONDS_KEY, default = 7) ?: 7
}
override fun getAlertThresholdSeconds() =
getInt(ALERT_THRESHOLD_SECONDS_KEY, default = DEFAULT_ALERT_THRESHOLD_SECONDS)
?: DEFAULT_ALERT_THRESHOLD_SECONDS
override fun getAlertThresholdSecondsFlow(): Flow<Int> = mutableAlertThresholdSecondsFlow
.onSubscription { emit(getAlertThresholdSeconds()) }
@@ -15,6 +15,9 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
/**
* Provides persistence-related dependencies in the platform package.
*/
object PlatformDiskModule {
@Provides
@@ -12,6 +12,10 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
/**
* This class provides network-related functionality for the application.
* It initializes and configures the networking components.
*/
object PlatformNetworkModule {
@Provides
@Singleton
@@ -15,5 +15,4 @@ interface ImportManager {
importFileFormat: ImportFileFormat,
byteArray: ByteArray,
): ImportDataResult
}
@@ -29,19 +29,15 @@ class ImportManagerImpl(
ImportFileFormat.AEGIS -> AegisExportParser()
}
return try {
when (val parseResult = parser.parse(byteArray)) {
is ExportParseResult.Error -> {
ImportDataResult.Error(parseResult.message)
}
return when (val parseResult = parser.parse(byteArray)) {
is ExportParseResult.Error -> {
ImportDataResult.Error(parseResult.message)
}
is ExportParseResult.Success -> {
authenticatorDiskSource.saveItem(*parseResult.items.toTypedArray())
ImportDataResult.Success
}
is ExportParseResult.Success -> {
authenticatorDiskSource.saveItem(*parseResult.items.toTypedArray())
ImportDataResult.Success
}
} catch (e: Throwable) {
ImportDataResult.Error()
}
}
}
@@ -2,12 +2,18 @@ package com.bitwarden.authenticator.data.platform.manager.imports.model
import kotlinx.serialization.Serializable
/**
* Models the Aegis JSON export file.
*/
@Serializable
data class AegisJsonExport(
val version: Int,
val db: Database,
) {
/**
* Models the Aegis database in JSON format.
*/
@Serializable
data class Database(
val version: Int,
@@ -15,6 +21,9 @@ data class AegisJsonExport(
val groups: List<Group>,
) {
/**
* Models an Aegis database entry.
*/
@Serializable
data class Entry(
val type: String,
@@ -27,6 +36,9 @@ data class AegisJsonExport(
val groups: List<String>,
) {
/**
* Models key information for an [Entry].
*/
@Serializable
data class Info(
val secret: String,
@@ -36,6 +48,9 @@ data class AegisJsonExport(
)
}
/**
* Models a collection that can be associated with an [Entry].
*/
@Serializable
data class Group(
val uuid: String,
@@ -6,7 +6,15 @@ import com.bitwarden.authenticator.ui.platform.base.util.Text
* Represents the result of a data import operation.
*/
sealed class ImportDataResult {
/**
* Indicates import was successful.
*/
data object Success : ImportDataResult()
/**
* Indicates import was not successful.
*
* @property message Optional [Text] indicating why the import failed.
*/
data class Error(val message: Text? = null) : ImportDataResult()
}
@@ -3,6 +3,9 @@ package com.bitwarden.authenticator.data.platform.manager.imports.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* Models the JSON export file from LastPass.
*/
@Serializable
data class LastPassJsonExport(
val deviceId: String,
@@ -13,6 +16,9 @@ data class LastPassJsonExport(
val accounts: List<Account>,
val folders: List<Folder>,
) {
/**
* Models an account contained within a [LastPassJsonExport].
*/
@Serializable
data class Account(
@SerialName("accountID")
@@ -31,12 +37,18 @@ data class LastPassJsonExport(
val folderData: FolderData?,
val backupInfo: BackupInfo?,
) {
/**
* Models metadata for a [Folder].
*/
@Serializable
data class FolderData(
val folderId: String,
val position: Int,
)
/**
* Models backup file information for an [Account].
*/
@Serializable
data class BackupInfo(
val creationDate: String,
@@ -45,6 +57,9 @@ data class LastPassJsonExport(
)
}
/**
* Models a collection of [Account] objects.
*/
@Serializable
data class Folder(
val id: Int,
@@ -2,6 +2,9 @@ package com.bitwarden.authenticator.data.platform.manager.imports.model
import kotlinx.serialization.Serializable
/**
* Models the JSON export file from 2FAS.
*/
@Serializable
data class TwoFasJsonExport(
val schemaVersion: Int?,
@@ -11,6 +14,9 @@ data class TwoFasJsonExport(
val servicesEncrypted: String?,
val groups: List<Group>?,
) {
/**
* Models a service account contained within a [TwoFasJsonExport].
*/
@Serializable
data class Service(
val otp: Otp,
@@ -22,6 +28,9 @@ data class TwoFasJsonExport(
val badge: Badge?,
val serviceTypeId: String?,
) {
/**
* Models OTP auth data for a 2fas [Service].
*/
@Serializable
data class Otp(
val counter: Int?,
@@ -35,22 +44,34 @@ data class TwoFasJsonExport(
val issuer: String?,
)
/**
* Models ordinal information for a 2fas [Service].
*/
@Serializable
data class Order(
val position: Int?,
)
/**
* Models the icon for a 2fas [Service].
*/
@Serializable
data class Icon(
val iconCollection: IconCollection?,
val label: Label?,
val selected: String?,
) {
/**
* Models a collection that can be associated to a 2fas [Icon].
*/
@Serializable
data class IconCollection(
val id: String?,
)
/**
* Models label data for a 2fas [Icon].
*/
@Serializable
data class Label(
val backgroundColor: String?,
@@ -58,12 +79,18 @@ data class TwoFasJsonExport(
)
}
/**
* Models badge data about a 2fas [Service].
*/
@Serializable
data class Badge(
val color: String,
)
}
/**
* Models a collection of 2fas [Service] objects.
*/
@Serializable
data class Group(
val id: String,
@@ -13,6 +13,9 @@ import java.io.ByteArrayInputStream
import java.io.IOException
import java.util.UUID
/**
* Implementation of [ExportParser] responsible for parsing exports from the Aegis application.
*/
class AegisExportParser : ExportParser {
@OptIn(ExperimentalSerializationApi::class)
override fun parse(byteArray: ByteArray): ExportParseResult {
@@ -43,6 +46,7 @@ class AegisExportParser : ExportParser {
private fun List<AegisJsonExport.Database.Entry>.toAuthenticatorItemEntities() =
map { it.toAuthenticatorItemEntity() }
@Suppress("MaxLineLength")
private fun AegisJsonExport.Database.Entry.toAuthenticatorItemEntity(): AuthenticatorItemEntity {
// Aegis only supports TOTP codes.
@@ -17,6 +17,9 @@ import kotlinx.serialization.json.decodeFromStream
import java.io.ByteArrayInputStream
import java.io.IOException
/**
* Implementation of [ExportParser] responsible for parsing exports from the Bitwarden application.
*/
class BitwardenExportParser(
private val fileFormat: ImportFileFormat,
) : ExportParser {
@@ -42,7 +45,7 @@ class BitwardenExportParser(
items = exportData
.items
.filter { it.login?.totp != null }
.toAuthenticatorItemEntities()
.toAuthenticatorItemEntities(),
)
} catch (e: SerializationException) {
ExportParseResult.Error()
@@ -56,6 +59,7 @@ class BitwardenExportParser(
private fun List<ExportJsonData.ExportItem>.toAuthenticatorItemEntities() =
map { it.toAuthenticatorItemEntity() }
@Suppress("MaxLineLength", "CyclomaticComplexMethod", "LongMethod")
private fun ExportJsonData.ExportItem.toAuthenticatorItemEntity(): AuthenticatorItemEntity {
val otpString = requireNotNull(login?.totp)
@@ -45,7 +45,7 @@ class TwoFasExportParser : ExportParser {
)
} else {
ExportParseResult.Success(
items = exportData.services.toAuthenticatorItemEntities()
items = exportData.services.toAuthenticatorItemEntities(),
)
}
} catch (e: SerializationException) {
@@ -60,6 +60,7 @@ class TwoFasExportParser : ExportParser {
private fun List<TwoFasJsonExport.Service>.toAuthenticatorItemEntities() =
mapNotNull { it.toAuthenticatorItemEntityOrNull() }
@Suppress("MaxLineLength")
private fun TwoFasJsonExport.Service.toAuthenticatorItemEntityOrNull(): AuthenticatorItemEntity {
val type = otp.tokenType
@@ -4,7 +4,8 @@ package com.bitwarden.authenticator.data.platform.manager.model
* Represents feature flags used by the application.
*
* @property name The string value of the given key.
* @property defaultValue The value to be used if the flags value cannot be determined or is not remotely configured.
* @property defaultValue The value to be used if the flags value cannot be determined or is not
* remotely configured.
* @property isRemotelyConfigured Indicates if the flag should respect the network value or not.
*/
sealed class FeatureFlag<out T : Any>(
@@ -37,4 +38,3 @@ sealed class RemoteFeatureFlag<out T : Any>(
name: String,
defaultValue: T,
) : FeatureFlag<T>(name, defaultValue, isRemotelyConfigured = true)
@@ -37,9 +37,9 @@ class FeatureFlagRepositoryImpl(
val config = FeatureFlagsConfiguration(
mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(
LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue
)
)
LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue,
),
),
)
featureFlagDiskSource.featureFlagsConfiguration = config
return config
@@ -39,6 +39,10 @@ import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaf
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager
import com.bitwarden.authenticator.ui.platform.theme.LocalBiometricsManager
/**
* Top level composable for the unlock screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun UnlockScreen(
@@ -66,7 +70,7 @@ fun UnlockScreen(
is UnlockState.Dialog.Error -> BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = dialog.message
message = dialog.message,
),
onDismissRequest = remember(viewModel) {
{
@@ -76,7 +80,7 @@ fun UnlockScreen(
)
UnlockState.Dialog.Loading -> BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(R.string.loading.asText())
visibilityState = LoadingDialogState.Shown(R.string.loading.asText()),
)
null -> Unit
@@ -118,7 +122,7 @@ fun UnlockScreen(
.padding(innerPadding)
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
modifier = Modifier
@@ -33,7 +33,7 @@ class UnlockViewModel @Inject constructor(
isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(),
dialog = null,
)
}
},
) {
init {
@@ -74,7 +74,7 @@ class UnlockViewModel @Inject constructor(
it.copy(
dialog = UnlockState.Dialog.Error(
message = R.string.too_many_failed_biometric_attempts.asText(),
)
),
)
}
}
@@ -125,7 +125,6 @@ sealed class UnlockEvent {
data class ShowToast(
val message: Text,
) : UnlockEvent()
}
/**
@@ -33,7 +33,7 @@ fun NavGraphBuilder.authenticatorGraph(
) {
navigation(
startDestination = AUTHENTICATOR_NAV_BAR_ROUTE,
route = AUTHENTICATOR_GRAPH_ROUTE
route = AUTHENTICATOR_GRAPH_ROUTE,
) {
authenticatorNavBarDestination(
onNavigateBack = onNavigateBack,
@@ -43,7 +43,7 @@ fun NavGraphBuilder.authenticatorGraph(
onNavigateToEditItem = { navController.navigateToEditItem(itemId = it) },
onNavigateToExport = { navController.navigateToExport() },
onNavigateToImport = { navController.navigateToImporting() },
onNavigateToTutorial = { navController.navigateToSettingsTutorial() }
onNavigateToTutorial = { navController.navigateToSettingsTutorial() },
)
itemListingGraph(
navController = navController,
@@ -62,7 +62,7 @@ fun NavGraphBuilder.authenticatorGraph(
},
navigateToExport = { navController.navigateToExport() },
navigateToImport = { navController.navigateToImporting() },
navigateToTutorial = { navController.navigateToSettingsTutorial() }
navigateToTutorial = { navController.navigateToSettingsTutorial() },
)
}
}
@@ -72,6 +72,7 @@ import kotlinx.collections.immutable.toImmutableList
/**
* Displays the edit authenticator item screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditItemScreen(
@@ -94,7 +95,7 @@ fun EditItemScreen(
.makeText(
context,
event.message(resources),
Toast.LENGTH_LONG
Toast.LENGTH_LONG,
)
.show()
}
@@ -103,7 +104,7 @@ fun EditItemScreen(
EditItemDialogs(
dialogState = state.dialog,
onDismissRequest = { viewModel.trySendAction(EditItemAction.DismissDialog) }
onDismissRequest = { viewModel.trySendAction(EditItemAction.DismissDialog) },
)
BitwardenScaffold(
@@ -131,7 +132,7 @@ fun EditItemScreen(
},
modifier = Modifier.semantics { testTag = "SaveButton" },
)
}
},
)
},
floatingActionButtonPosition = FabPosition.EndOverlay,
@@ -146,66 +147,66 @@ fun EditItemScreen(
onIssuerNameTextChange = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.IssuerNameTextChange(it)
EditItemAction.IssuerNameTextChange(it),
)
}
},
onUsernameTextChange = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.UsernameTextChange(it)
EditItemAction.UsernameTextChange(it),
)
}
},
onToggleFavorite = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.FavoriteToggleClick(it)
EditItemAction.FavoriteToggleClick(it),
)
}
},
onTypeOptionClicked = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.TypeOptionClick(it)
EditItemAction.TypeOptionClick(it),
)
}
},
onTotpCodeTextChange = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.TotpCodeTextChange(it)
EditItemAction.TotpCodeTextChange(it),
)
}
},
onAlgorithmOptionClicked = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.AlgorithmOptionClick(it)
EditItemAction.AlgorithmOptionClick(it),
)
}
},
onRefreshPeriodOptionClicked = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.RefreshPeriodOptionClick(it)
EditItemAction.RefreshPeriodOptionClick(it),
)
}
},
onNumberOfDigitsChanged = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.NumberOfDigitsOptionClick(it)
EditItemAction.NumberOfDigitsOptionClick(it),
)
}
},
onExpandAdvancedOptionsClicked = remember(viewModel) {
{
viewModel.trySendAction(
EditItemAction.ExpandAdvancedOptionsClick
EditItemAction.ExpandAdvancedOptionsClick,
)
}
}
},
)
}
@@ -218,6 +219,10 @@ fun EditItemScreen(
}
}
/**
* The top level content UI state for the [EditItemScreen].
*/
@Suppress("LongMethod")
@Composable
fun EditItemContent(
modifier: Modifier = Modifier,
@@ -307,11 +312,12 @@ fun EditItemContent(
onAlgorithmOptionClicked = onAlgorithmOptionClicked,
onTypeOptionClicked = onTypeOptionClicked,
onRefreshPeriodOptionClicked = onRefreshPeriodOptionClicked,
onNumberOfDigitsChanged = onNumberOfDigitsChanged
onNumberOfDigitsChanged = onNumberOfDigitsChanged,
)
}
}
@Suppress("LongMethod")
@Composable
private fun AdvancedOptions(
modifier: Modifier = Modifier,
@@ -332,7 +338,7 @@ private fun AdvancedOptions(
bounded = true,
color = MaterialTheme.colorScheme.primary,
),
interactionSource = remember { MutableInteractionSource() }
interactionSource = remember { MutableInteractionSource() },
) {
onExpandStateChange()
},
@@ -350,21 +356,21 @@ private fun AdvancedOptions(
R.drawable.ic_chevron_up
} else {
R.drawable.ic_chevron_down
}
},
),
contentDescription = stringResource(
id = R.string.collapse_advanced_options
id = R.string.collapse_advanced_options,
),
tint = MaterialTheme.colorScheme.primary
tint = MaterialTheme.colorScheme.primary,
)
}
AnimatedVisibility(
visible = viewState.isAdvancedOptionsExpanded,
enter = fadeIn(tween(DEFAULT_FADE_TRANSITION_TIME_MS))
+ expandVertically(tween(DEFAULT_STAY_TRANSITION_TIME_MS)),
exit = fadeOut(tween(DEFAULT_FADE_TRANSITION_TIME_MS))
+ shrinkVertically(tween(DEFAULT_STAY_TRANSITION_TIME_MS)),
enter = fadeIn(tween(DEFAULT_FADE_TRANSITION_TIME_MS)) +
expandVertically(tween(DEFAULT_STAY_TRANSITION_TIME_MS)),
exit = fadeOut(tween(DEFAULT_FADE_TRANSITION_TIME_MS)) +
shrinkVertically(tween(DEFAULT_STAY_TRANSITION_TIME_MS)),
) {
LazyColumn {
item {
@@ -418,7 +424,7 @@ private fun AdvancedOptions(
.associateWith {
stringResource(
id = R.string.refresh_period_seconds,
it.seconds
it.seconds,
)
}
Spacer(modifier = Modifier.height(8.dp))
@@ -440,7 +446,7 @@ private fun AdvancedOptions(
onRefreshPeriodOptionClicked(selectedOptionName)
}
}
},
)
}
@@ -468,7 +474,7 @@ private fun EditItemDialogs(
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialogState.title,
message = dialogState.message
message = dialogState.message,
),
onDismissRequest = onDismissRequest,
)
@@ -520,7 +526,7 @@ private fun EditItemContentExpandedOptionsPreview() {
),
minDigitsAllowed = 5,
maxDigitsAllowed = 10,
)
),
)
}
@@ -542,6 +548,6 @@ private fun EditItemContentCollapsedOptionsPreview() {
),
minDigitsAllowed = 5,
maxDigitsAllowed = 10,
)
),
)
}
@@ -35,6 +35,7 @@ private const val KEY_STATE = "state"
/**
* View model responsible for handling user interaction with the edit authenticator item screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class EditItemViewModel @Inject constructor(
private val authenticatorRepository: AuthenticatorRepository,
@@ -44,7 +45,7 @@ class EditItemViewModel @Inject constructor(
itemId = EditItemArgs(savedStateHandle).itemId,
viewState = EditItemState.ViewState.Loading,
dialog = null,
)
),
) {
init {
@@ -76,7 +77,7 @@ class EditItemViewModel @Inject constructor(
private fun handleExpandAdvancedOptionsClick() {
updateContent { currentContent ->
currentContent.copy(
isAdvancedOptionsExpanded = currentContent.isAdvancedOptionsExpanded.not()
isAdvancedOptionsExpanded = currentContent.isAdvancedOptionsExpanded.not(),
)
}
}
@@ -88,7 +89,7 @@ class EditItemViewModel @Inject constructor(
dialog = EditItemState.DialogState.Generic(
title = R.string.an_error_has_occurred.asText(),
message = R.string.validation_field_required.asText(R.string.name.asText()),
)
),
)
}
return@onContent
@@ -98,7 +99,7 @@ class EditItemViewModel @Inject constructor(
dialog = EditItemState.DialogState.Generic(
title = R.string.an_error_has_occurred.asText(),
message = R.string.validation_field_required.asText(R.string.key.asText()),
)
),
)
}
return@onContent
@@ -107,8 +108,8 @@ class EditItemViewModel @Inject constructor(
it.copy(
dialog = EditItemState.DialogState.Generic(
title = R.string.an_error_has_occurred.asText(),
message = R.string.key_is_invalid.asText()
)
message = R.string.key_is_invalid.asText(),
),
)
}
return@onContent
@@ -117,8 +118,8 @@ class EditItemViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
dialog = EditItemState.DialogState.Loading(
R.string.saving.asText()
)
R.string.saving.asText(),
),
)
}
viewModelScope.launch {
@@ -133,7 +134,7 @@ class EditItemViewModel @Inject constructor(
digits = content.itemData.digits,
issuer = content.itemData.issuer.trim(),
favorite = content.itemData.favorite,
)
),
)
trySendAction(EditItemAction.Internal.UpdateItemResult(result))
}
@@ -142,7 +143,7 @@ class EditItemViewModel @Inject constructor(
private fun handleNumberOfDigitsOptionChange(action: EditItemAction.NumberOfDigitsOptionClick) {
updateItemData { currentItemData ->
currentItemData.copy(
digits = action.digits
digits = action.digits,
)
}
}
@@ -150,7 +151,7 @@ class EditItemViewModel @Inject constructor(
private fun handleIssuerNameTextChange(action: EditItemAction.IssuerNameTextChange) {
updateItemData { currentItemData ->
currentItemData.copy(
issuer = action.issuerName
issuer = action.issuerName,
)
}
}
@@ -158,7 +159,7 @@ class EditItemViewModel @Inject constructor(
private fun handleIssuerTextChange(action: EditItemAction.UsernameTextChange) {
updateItemData { currentItemData ->
currentItemData.copy(
username = action.username
username = action.username,
)
}
}
@@ -166,7 +167,7 @@ class EditItemViewModel @Inject constructor(
private fun handleFavoriteToggleClick(action: EditItemAction.FavoriteToggleClick) {
updateItemData { currentItemData ->
currentItemData.copy(
favorite = action.favorite
favorite = action.favorite,
)
}
}
@@ -174,7 +175,7 @@ class EditItemViewModel @Inject constructor(
private fun handleTotpCodeTextChange(action: EditItemAction.TotpCodeTextChange) {
updateItemData { currentItemData ->
currentItemData.copy(
totpCode = action.totpCode
totpCode = action.totpCode,
)
}
}
@@ -182,7 +183,7 @@ class EditItemViewModel @Inject constructor(
private fun handlePeriodTextChange(action: EditItemAction.RefreshPeriodOptionClick) {
updateItemData { currentItemData ->
currentItemData.copy(
refreshPeriod = action.period
refreshPeriod = action.period,
)
}
}
@@ -190,7 +191,7 @@ class EditItemViewModel @Inject constructor(
private fun handleAlgorithmOptionClick(action: EditItemAction.AlgorithmOptionClick) {
updateItemData { currentItemData ->
currentItemData.copy(
algorithm = action.algorithmOption
algorithm = action.algorithmOption,
)
}
}
@@ -202,7 +203,7 @@ class EditItemViewModel @Inject constructor(
private fun handleTypeOptionClick(action: EditItemAction.TypeOptionClick) {
updateItemData { currentItemData ->
currentItemData.copy(
type = action.typeOption
type = action.typeOption,
)
}
}
@@ -236,6 +237,7 @@ class EditItemViewModel @Inject constructor(
}
}
@Suppress("LongMethod")
private fun handleItemDataReceive(action: EditItemAction.Internal.EditItemDataReceive) {
when (val itemState = action.itemDataState) {
is DataState.Error -> {
@@ -258,7 +260,7 @@ class EditItemViewModel @Inject constructor(
.data
?.toViewState(expandAdvancedOptions)
?: EditItemState.ViewState.Error(
message = R.string.generic_error_message.asText()
message = R.string.generic_error_message.asText(),
),
)
}
@@ -267,7 +269,7 @@ class EditItemViewModel @Inject constructor(
DataState.Loading -> {
mutableStateFlow.update {
it.copy(
viewState = EditItemState.ViewState.Loading
viewState = EditItemState.ViewState.Loading,
)
}
}
@@ -294,7 +296,7 @@ class EditItemViewModel @Inject constructor(
.data
?.toViewState(expandAdvancedOptions)
?: EditItemState.ViewState.Error(
message = R.string.generic_error_message.asText()
message = R.string.generic_error_message.asText(),
),
)
}
@@ -512,6 +514,9 @@ sealed class EditItemAction {
val digits: Int,
) : EditItemAction()
/**
* The user has clicked to expand advanced OTP options.
*/
data object ExpandAdvancedOptionsClick : EditItemAction()
/**
@@ -543,6 +548,9 @@ enum class AuthenticatorRefreshPeriodOption(val seconds: Int) {
;
companion object {
/**
* Returns a [AuthenticatorRefreshPeriodOption] with the provided [seconds], or null.
*/
fun fromSeconds(seconds: Int) = entries.find { it.seconds == seconds }
}
}
@@ -17,6 +17,7 @@ const val ITEM_LISTING_GRAPH_ROUTE = "item_listing_graph"
/**
* Add the item listing graph to the nav graph.
*/
@Suppress("LongParameterList")
fun NavGraphBuilder.itemListingGraph(
navController: NavController,
navigateBack: () -> Unit,
@@ -43,7 +44,7 @@ fun NavGraphBuilder.itemListingGraph(
onNavigateBack = { navController.popBackStack() },
)
itemSearchDestination(
onNavigateBack = { navController.popBackStack() }
onNavigateBack = { navController.popBackStack() },
)
qrCodeScanDestination(
onNavigateBack = { navController.popBackStack() },
@@ -57,13 +58,13 @@ fun NavGraphBuilder.itemListingGraph(
onNavigateToQrCodeScreen = {
navController.popBackStack()
navController.navigateToQrCodeScanScreen()
}
},
)
settingsGraph(
navController = navController,
onNavigateToExport = navigateToExport,
onNavigateToImport = navigateToImport,
onNavigateToTutorial = navigateToTutorial
onNavigateToTutorial = navigateToTutorial,
)
}
}
@@ -76,6 +77,6 @@ fun NavController.navigateToItemListGraph(
) {
navigate(
route = ITEM_LISTING_GRAPH_ROUTE,
navOptions = navOptions
navOptions = navOptions,
)
}
@@ -72,6 +72,7 @@ import com.bitwarden.authenticator.ui.platform.theme.Typography
/**
* Displays the item listing screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemListingScreen(
@@ -109,7 +110,7 @@ fun ItemListingScreen(
.makeText(
context,
event.message(context.resources),
Toast.LENGTH_LONG
Toast.LENGTH_LONG,
)
.show()
}
@@ -164,7 +165,7 @@ fun ItemListingScreen(
onNavigateToSearch = remember(viewModel) {
{
viewModel.trySendAction(
ItemListingAction.SearchClick
ItemListingAction.SearchClick,
)
}
},
@@ -181,24 +182,24 @@ fun ItemListingScreen(
onItemClick = remember(viewModel) {
{
viewModel.trySendAction(
ItemListingAction.ItemClick(it)
ItemListingAction.ItemClick(it),
)
}
},
onEditItemClick = remember(viewModel) {
{
viewModel.trySendAction(
ItemListingAction.EditItemClick(it)
ItemListingAction.EditItemClick(it),
)
}
},
onDeleteItemClick = remember(viewModel) {
{
viewModel.trySendAction(
ItemListingAction.DeleteItemClick(it)
ItemListingAction.DeleteItemClick(it),
)
}
}
},
)
}
@@ -229,7 +230,7 @@ fun ItemListingScreen(
{
viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick)
}
}
},
)
}
}
@@ -270,7 +271,7 @@ private fun ItemListingDialogs(
onConfirmDeleteClick(dialog.itemId)
},
onDismissClick = onDismissRequest,
onDismissRequest = onDismissRequest
onDismissRequest = onDismissRequest,
)
}
@@ -278,6 +279,7 @@ private fun ItemListingDialogs(
}
}
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ItemListingContent(
@@ -303,7 +305,7 @@ private fun ItemListingContent(
contentDescription = stringResource(id = R.string.search_codes),
onClick = onNavigateToSearch,
)
}
},
)
},
floatingActionButton = {
@@ -329,8 +331,8 @@ private fun ItemListingContent(
contentDescription = stringResource(id = R.string.enter_key_manually),
testTag = "EnterSetupKeyButton",
),
onEnterSetupKeyClick = onEnterSetupKeyClick
)
onEnterSetupKeyClick = onEnterSetupKeyClick,
),
),
expandableFabIcon = ExpandableFabIcon(
iconData = IconResource(
@@ -421,13 +423,14 @@ private fun ItemListingContent(
/**
* Displays the item listing screen with no existing items.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EmptyItemListingContent(
modifier: Modifier = Modifier,
appTheme: AppTheme,
scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
rememberTopAppBarState()
rememberTopAppBarState(),
),
onAddCodeClick: () -> Unit,
onScanQuCodeClick: () -> Unit,
@@ -442,7 +445,7 @@ fun EmptyItemListingContent(
title = stringResource(id = R.string.verification_codes),
scrollBehavior = scrollBehavior,
navigationIcon = null,
actions = { }
actions = { },
)
},
floatingActionButton = {
@@ -459,7 +462,7 @@ fun EmptyItemListingContent(
contentDescription = stringResource(id = R.string.scan_a_qr_code),
testTag = "ScanQRCodeButton",
),
onScanQrCodeClick = onScanQuCodeClick
onScanQrCodeClick = onScanQuCodeClick,
),
ItemListingExpandableFabAction.EnterSetupKey(
label = R.string.enter_key_manually.asText(),
@@ -469,7 +472,7 @@ fun EmptyItemListingContent(
testTag = "EnterSetupKeyButton",
),
onEnterSetupKeyClick = onEnterSetupKeyClick,
)
),
),
expandableFabIcon = ExpandableFabIcon(
iconData = IconResource(
@@ -497,7 +500,7 @@ fun EmptyItemListingContent(
AppTheme.DARK -> R.drawable.ic_empty_vault_dark
AppTheme.LIGHT -> R.drawable.ic_empty_vault_light
AppTheme.DEFAULT -> R.drawable.ic_empty_vault
}
},
),
contentDescription = stringResource(
id = R.string.empty_item_list,
@@ -530,7 +533,7 @@ fun EmptyItemListingContent(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Preview(showBackground = true)
fun EmptyListingContentPreview() {
private fun EmptyListingContentPreview() {
EmptyItemListingContent(
modifier = Modifier.padding(horizontal = 16.dp),
appTheme = AppTheme.DEFAULT,
@@ -38,6 +38,7 @@ import javax.inject.Inject
/**
* View model responsible for handling user interactions with the item listing screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class ItemListingViewModel @Inject constructor(
private val authenticatorRepository: AuthenticatorRepository,
@@ -49,8 +50,8 @@ class ItemListingViewModel @Inject constructor(
settingsRepository.appTheme,
settingsRepository.authenticatorAlertThresholdSeconds,
viewState = ItemListingState.ViewState.Loading,
dialog = null
)
dialog = null,
),
) {
init {
@@ -136,8 +137,8 @@ class ItemListingViewModel @Inject constructor(
clipboardManager.setText(action.authCode)
sendEvent(
ItemListingEvent.ShowToast(
message = R.string.value_has_been_copied.asText(action.authCode)
)
message = R.string.value_has_been_copied.asText(action.authCode),
),
)
}
@@ -151,7 +152,7 @@ class ItemListingViewModel @Inject constructor(
dialog = ItemListingState.DialogState.DeleteConfirmationPrompt(
message = R.string.do_you_really_want_to_permanently_delete_cipher.asText(),
itemId = action.itemId,
)
),
)
}
}
@@ -166,8 +167,8 @@ class ItemListingViewModel @Inject constructor(
viewModelScope.launch {
trySendAction(
ItemListingAction.Internal.DeleteItemReceive(
authenticatorRepository.hardDeleteItem(action.itemId)
)
authenticatorRepository.hardDeleteItem(action.itemId),
),
)
}
}
@@ -214,7 +215,7 @@ class ItemListingViewModel @Inject constructor(
dialog = ItemListingState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
)
),
)
}
}
@@ -244,7 +245,7 @@ class ItemListingViewModel @Inject constructor(
dialog = ItemListingState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.authenticator_key_read_error.asText(),
)
),
)
}
}
@@ -310,7 +311,7 @@ class ItemListingViewModel @Inject constructor(
.otpParametersList
.mapNotNull { otpParam ->
val secret = encodingManager.base32Encode(
byteArray = otpParam.secret.toByteArray()
byteArray = otpParam.secret.toByteArray(),
)
// Google Authenticator only supports TOTP and HOTP codes. We do not support HOTP
@@ -325,7 +326,7 @@ class ItemListingViewModel @Inject constructor(
// Google Authenticator does not always provide a valid digits value so we double
// check it and fallback to the default value if it is not within our valid range.
val digits = if (otpParam.digits in 5..10) {
val digits = if (otpParam.digits in TotpCodeManager.TOTP_DIGITS_RANGE) {
otpParam.digits
} else {
TotpCodeManager.TOTP_DIGITS_DEFAULT
@@ -411,11 +412,13 @@ class ItemListingViewModel @Inject constructor(
}
}
private fun authenticatorErrorReceive(authenticatorData: DataState.Error<List<VerificationCodeItem>>) {
private fun authenticatorErrorReceive(
authenticatorData: DataState.Error<List<VerificationCodeItem>>,
) {
if (authenticatorData.data != null) {
updateStateWithVerificationCodeItems(
authenticatorData = authenticatorData.data,
clearDialogState = true
clearDialogState = true,
)
} else {
mutableStateFlow.update {
@@ -434,14 +437,14 @@ class ItemListingViewModel @Inject constructor(
) {
updateStateWithVerificationCodeItems(
authenticatorData = authenticatorData.data,
clearDialogState = false
clearDialogState = false,
)
}
private fun authenticatorDataLoadingReceive() {
mutableStateFlow.update {
it.copy(
viewState = ItemListingState.ViewState.Loading
viewState = ItemListingState.ViewState.Loading,
)
}
}
@@ -452,7 +455,7 @@ class ItemListingViewModel @Inject constructor(
if (state.data != null) {
updateStateWithVerificationCodeItems(
authenticatorData = state.data,
clearDialogState = true
clearDialogState = true,
)
} else {
mutableStateFlow.update {
@@ -460,7 +463,7 @@ class ItemListingViewModel @Inject constructor(
viewState = ItemListingState.ViewState.Error(
message = R.string.internet_connection_required_title
.asText()
.concat(R.string.internet_connection_required_message.asText())
.concat(R.string.internet_connection_required_message.asText()),
),
dialog = null,
)
@@ -473,7 +476,7 @@ class ItemListingViewModel @Inject constructor(
) {
updateStateWithVerificationCodeItems(
authenticatorData = action.data,
clearDialogState = false
clearDialogState = false,
)
}
@@ -492,51 +495,50 @@ class ItemListingViewModel @Inject constructor(
}
private fun String.toAuthenticatorEntityOrNull(): AuthenticatorItemEntity? {
try {
val uri = Uri.parse(this)
val uri = Uri.parse(this)
val type = AuthenticatorItemType
.entries
.find { it.name.lowercase() == uri.host }
?: return null
val type = AuthenticatorItemType
.entries
.find { it.name.lowercase() == uri.host }
?: return null
val label = uri.pathSegments.firstOrNull() ?: return null
val accountName = if (label.contains(":")) {
label
.split(":")
.last()
} else {
label
}
val key = uri.getQueryParameter(SECRET) ?: return null
val algorithm = AuthenticatorItemAlgorithm
.entries
.find { it.name == uri.getQueryParameter(ALGORITHM) }
?: AuthenticatorItemAlgorithm.SHA1
val digits = uri.getQueryParameter(DIGITS)?.toInt() ?: 6
val issuer = uri.getQueryParameter(ISSUER) ?: label
val period = uri.getQueryParameter(PERIOD)?.toInt() ?: 30
return AuthenticatorItemEntity(
id = UUID.randomUUID().toString(),
key = key,
accountName = accountName,
type = type,
algorithm = algorithm,
period = period,
digits = digits,
issuer = issuer,
userId = null,
favorite = false,
)
} catch (e: Throwable) {
return null
val label = uri.pathSegments.firstOrNull() ?: return null
val accountName = if (label.contains(":")) {
label
.split(":")
.last()
} else {
label
}
val key = uri.getQueryParameter(SECRET) ?: return null
val algorithm = AuthenticatorItemAlgorithm
.entries
.find { it.name == uri.getQueryParameter(ALGORITHM) }
?: AuthenticatorItemAlgorithm.SHA1
val digits = uri.getQueryParameter(DIGITS)?.toIntOrNull()
?: TotpCodeManager.TOTP_DIGITS_DEFAULT
val issuer = uri.getQueryParameter(ISSUER)
?: label
val period = uri.getQueryParameter(PERIOD)?.toIntOrNull()
?: TotpCodeManager.PERIOD_SECONDS_DEFAULT
return AuthenticatorItemEntity(
id = UUID.randomUUID().toString(),
key = key,
accountName = accountName,
type = type,
algorithm = algorithm,
period = period,
digits = digits,
issuer = issuer,
userId = null,
favorite = false,
)
}
}
@@ -617,6 +619,9 @@ data class ItemListingState(
val message: Text,
) : DialogState()
/**
* Displays a prompt to confirm item deletion.
*/
@Parcelize
data class DeleteConfirmationPrompt(
val message: Text,
@@ -73,12 +73,12 @@ fun VaultVerificationCodeItem(
onClick = onItemClick,
onLongClick = {
shouldShowDropdownMenu = true
}
},
)
.defaultMinSize(minHeight = 72.dp)
.padding(
vertical = 8.dp,
horizontal = 16.dp
horizontal = 16.dp,
)
.then(modifier),
verticalAlignment = Alignment.CenterVertically,
@@ -120,7 +120,7 @@ fun VaultVerificationCodeItem(
BitwardenCircularCountdownIndicator(
timeLeftSeconds = timeLeftSeconds,
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds
alertThresholdSeconds = alertThresholdSeconds,
)
Text(
@@ -145,9 +145,9 @@ fun VaultVerificationCodeItem(
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_edit_item),
contentDescription = stringResource(R.string.edit_item)
contentDescription = stringResource(R.string.edit_item),
)
}
},
)
HorizontalDivider()
DropdownMenuItem(
@@ -163,7 +163,7 @@ fun VaultVerificationCodeItem(
painter = painterResource(id = R.drawable.ic_delete_item),
contentDescription = stringResource(id = R.string.delete_item),
)
}
},
)
}
}
@@ -1,15 +1,22 @@
package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model
import androidx.compose.material3.ExtendedFloatingActionButton
import com.bitwarden.authenticator.ui.platform.base.util.Text
import com.bitwarden.authenticator.ui.platform.components.fab.ExpandableFabOption
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
/**
* Models [ExpandableFabOption]s that can be triggered by the [ExtendedFloatingActionButton].
*/
sealed class ItemListingExpandableFabAction(
label: Text?,
icon: IconResource,
onFabOptionClick: () -> Unit,
) : ExpandableFabOption(label, icon, onFabOptionClick) {
/**
* Indicates the Scan QR code button was clicked.
*/
class ScanQrCode(
label: Text?,
icon: IconResource,
@@ -17,9 +24,12 @@ sealed class ItemListingExpandableFabAction(
) : ItemListingExpandableFabAction(
label,
icon,
onScanQrCodeClick
onScanQrCodeClick,
)
/**
* Indicates the Enter Key button was clicked.
*/
class EnterSetupKey(
label: Text?,
icon: IconResource,
@@ -27,6 +37,6 @@ sealed class ItemListingExpandableFabAction(
) : ItemListingExpandableFabAction(
label,
icon,
onEnterSetupKeyClick
onEnterSetupKeyClick,
)
}
@@ -4,6 +4,9 @@ import com.bitwarden.authenticator.data.authenticator.manager.model.Verification
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingState
import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VerificationCodeDisplayItem
/**
* Transform a list of [VerificationCodeItem] into [ItemListingState.ViewState].
*/
fun List<VerificationCodeItem>.toViewState(
alertThresholdSeconds: Int,
): ItemListingState.ViewState =
@@ -25,7 +28,7 @@ fun List<VerificationCodeItem>.toViewState(
)
}
fun VerificationCodeItem.toDisplayItem(alertThresholdSeconds: Int) =
private fun VerificationCodeItem.toDisplayItem(alertThresholdSeconds: Int) =
VerificationCodeDisplayItem(
id = id,
issuer = issuer,
@@ -120,19 +120,19 @@ fun ManualCodeEntryScreen(
BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialog.title,
message = dialog.message
message = dialog.message,
),
onDismissRequest = remember(state) {
{ viewModel.trySendAction(ManualCodeEntryAction.DismissDialog) }
}
},
)
}
is ManualCodeEntryState.DialogState.Loading -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(
dialog.message
)
dialog.message,
),
)
}
@@ -7,7 +7,6 @@ import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
import com.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
import com.bitwarden.authenticator.data.authenticator.repository.model.CreateItemResult
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel
import com.bitwarden.authenticator.ui.platform.base.util.Text
import com.bitwarden.authenticator.ui.platform.base.util.asText
@@ -41,10 +40,6 @@ class ManualCodeEntryViewModel @Inject constructor(
is ManualCodeEntryAction.CodeSubmit -> handleCodeSubmit()
is ManualCodeEntryAction.ScanQrCodeTextClick -> handleScanQrCodeTextClick()
is ManualCodeEntryAction.SettingsClick -> handleSettingsClick()
is ManualCodeEntryAction.Internal.CreateItemResultReceive -> {
handleCreateItemReceive(action)
}
ManualCodeEntryAction.DismissDialog -> {
handleDialogDismiss()
}
@@ -76,8 +71,8 @@ class ManualCodeEntryViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
dialog = ManualCodeEntryState.DialogState.Error(
message = R.string.key_is_required.asText()
)
message = R.string.key_is_required.asText(),
),
)
}
return
@@ -87,8 +82,8 @@ class ManualCodeEntryViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
dialog = ManualCodeEntryState.DialogState.Error(
message = R.string.key_is_invalid.asText()
)
message = R.string.key_is_invalid.asText(),
),
)
}
return
@@ -98,15 +93,15 @@ class ManualCodeEntryViewModel @Inject constructor(
mutableStateFlow.update {
it.copy(
dialog = ManualCodeEntryState.DialogState.Error(
message = R.string.name_is_required.asText()
)
message = R.string.name_is_required.asText(),
),
)
}
return
}
viewModelScope.launch {
val result = authenticatorRepository.createItem(
authenticatorRepository.createItem(
AuthenticatorItemEntity(
id = UUID.randomUUID().toString(),
key = state.code,
@@ -119,9 +114,16 @@ class ManualCodeEntryViewModel @Inject constructor(
AuthenticatorItemType.TOTP
},
favorite = false,
)
),
)
sendEvent(
event = ManualCodeEntryEvent.ShowToast(
message = R.string.verification_code_added.asText(),
),
)
sendEvent(
event = ManualCodeEntryEvent.NavigateBack,
)
sendAction(ManualCodeEntryAction.Internal.CreateItemResultReceive(result))
}
}
@@ -132,34 +134,6 @@ class ManualCodeEntryViewModel @Inject constructor(
private fun handleSettingsClick() {
sendEvent(ManualCodeEntryEvent.NavigateToAppSettings)
}
private fun handleCreateItemReceive(
action: ManualCodeEntryAction.Internal.CreateItemResultReceive,
) {
when (action.result) {
CreateItemResult.Error -> {
mutableStateFlow.update {
it.copy(
dialog = ManualCodeEntryState.DialogState.Error(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
)
)
}
}
CreateItemResult.Success -> {
sendEvent(
event = ManualCodeEntryEvent.ShowToast(
message = R.string.verification_code_added.asText(),
),
)
sendEvent(
event = ManualCodeEntryEvent.NavigateBack,
)
}
}
}
}
/**
@@ -172,15 +146,24 @@ data class ManualCodeEntryState(
val dialog: DialogState?,
) : Parcelable {
/**
* Models dialog states for [ManualCodeEntryViewModel].
*/
@Parcelize
sealed class DialogState : Parcelable {
/**
* Show an error dialog with an optional [title], and a [message].
*/
@Parcelize
data class Error(
val title: Text? = null,
val message: Text,
) : DialogState()
/**
* Show a loading dialog.
*/
@Parcelize
data class Loading(
val message: Text,
@@ -239,17 +222,6 @@ sealed class ManualCodeEntryAction {
*/
data class IssuerTextChange(val issuer: String) : ManualCodeEntryAction()
/**
* Models actions that the [ManualCodeEntryViewModel] itself might send.
*/
sealed class Internal : ManualCodeEntryAction() {
/**
* Indicates a result for creating an item has been received.
*/
data class CreateItemResultReceive(val result: CreateItemResult) : Internal()
}
/**
* The text to switch to QR code scanning is clicked.
*/
@@ -8,6 +8,7 @@ const val AUTHENTICATOR_NAV_BAR_ROUTE: String = "AuthenticatorNavBarRoute"
/**
* Add the authenticator nav bar to the nav graph.
*/
@Suppress("LongParameterList")
fun NavGraphBuilder.authenticatorNavBarDestination(
onNavigateBack: () -> Unit,
onNavigateToSearch: () -> Unit,
@@ -29,7 +30,7 @@ fun NavGraphBuilder.authenticatorNavBarDestination(
onNavigateToEditItem = onNavigateToEditItem,
onNavigateToExport = onNavigateToExport,
onNavigateToImport = onNavigateToImport,
onNavigateToTutorial = onNavigateToTutorial
onNavigateToTutorial = onNavigateToTutorial,
)
}
}
@@ -154,7 +154,7 @@ private fun AuthenticatorNavBarScaffold(
},
modifier = Modifier
.fillMaxWidth()
.height(appBarHeightPx.toDp())
.height(appBarHeightPx.toDp()),
)
}
},
@@ -186,6 +186,7 @@ private fun AuthenticatorNavBarScaffold(
}
}
@Suppress("LongMethod")
@Composable
private fun AuthenticatorBottomAppBar(
navController: NavController,
@@ -47,14 +47,13 @@ class QrCodeAnalyzerImpl : QrCodeAnalyzer {
/* hints = */
mapOf(
DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE),
DecodeHintType.ALSO_INVERTED to true
DecodeHintType.ALSO_INVERTED to true,
),
)
qrCodeRead = true
onQrCodeScanned(result.text)
} catch (ignored: NotFoundException) {
} finally {
image.close()
}
@@ -16,7 +16,7 @@ fun NavGraphBuilder.itemSearchDestination(
route = ITEM_SEARCH_ROUTE,
) {
ItemSearchScreen(
onNavigateBack = onNavigateBack
onNavigateBack = onNavigateBack,
)
}
}
@@ -37,6 +37,7 @@ import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaf
/**
* The search screen for authenticator items.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemSearchScreen(
@@ -84,8 +85,7 @@ fun ItemSearchScreen(
),
)
},
)
{ innerPadding ->
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
@@ -107,7 +107,7 @@ fun ItemSearchScreen(
is ItemSearchState.ViewState.Empty -> {
ItemSearchEmptyContent(
viewState = viewState,
modifier = innerModifier
modifier = innerModifier,
)
}
@@ -24,6 +24,7 @@ import javax.inject.Inject
/**
* View model for the item search screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class ItemSearchViewModel @Inject constructor(
private val clipboardManager: BitwardenClipboardManager,
@@ -34,7 +35,7 @@ class ItemSearchViewModel @Inject constructor(
searchTerm = "",
viewState = ItemSearchState.ViewState.Loading,
dialogState = null,
)
),
) {
init {
@@ -75,7 +76,9 @@ class ItemSearchViewModel @Inject constructor(
}
}
private fun handleAuthenticatorDataReceive(action: ItemSearchAction.Internal.AuthenticatorDataReceive) {
private fun handleAuthenticatorDataReceive(
action: ItemSearchAction.Internal.AuthenticatorDataReceive,
) {
when (val data = action.dataState) {
is DataState.Error -> authenticatorErrorReceive(authenticatorData = data)
is DataState.Loaded -> authenticatorLoadedReceive(authenticatorData = data)
@@ -85,7 +88,9 @@ class ItemSearchViewModel @Inject constructor(
}
}
private fun authenticatorErrorReceive(authenticatorData: DataState<List<VerificationCodeItem>>) {
private fun authenticatorErrorReceive(
authenticatorData: DataState<List<VerificationCodeItem>>,
) {
authenticatorData
.data
?.let {
@@ -157,7 +162,7 @@ class ItemSearchViewModel @Inject constructor(
authenticatorRepository.getAuthCodesFlow().value.data?.let { authenticatorData ->
updateStateWithAuthenticatorData(
authenticatorData = authenticatorData,
clearDialogState = false
clearDialogState = false,
)
}
}
@@ -172,7 +177,7 @@ class ItemSearchViewModel @Inject constructor(
viewState = authenticatorData
.filterAndOrganize(state.searchTerm)
.toViewState(searchTerm = state.searchTerm),
dialogState = currentState.dialogState.takeUnless { clearDialogState }
dialogState = currentState.dialogState.takeUnless { clearDialogState },
)
}
}
@@ -192,6 +197,7 @@ class ItemSearchViewModel @Inject constructor(
}
}
@Suppress("MagicNumber")
private fun VerificationCodeItem.matchedSearch(searchTerm: String): SortPriority? {
val term = searchTerm.removeDiacritics()
val itemName = label.removeDiacritics()
@@ -221,7 +227,7 @@ class ItemSearchViewModel @Inject constructor(
else -> {
ItemSearchState.ViewState.Empty(
message = R.string.there_are_no_items_that_match_the_search.asText()
message = R.string.there_are_no_items_that_match_the_search.asText(),
)
}
}
@@ -382,5 +388,5 @@ sealed class ItemSearchEvent {
private enum class SortPriority {
HIGH,
LOW
LOW,
}
@@ -104,7 +104,7 @@ fun VaultVerificationCodeItem(
BitwardenCircularCountdownIndicator(
timeLeftSeconds = timeLeftSeconds,
periodSeconds = periodSeconds,
alertThresholdSeconds = alertThresholdSeconds
alertThresholdSeconds = alertThresholdSeconds,
)
Text(
@@ -141,7 +141,7 @@ private fun VerificationCodeItem_preview() {
onCopyClick = {},
onItemClick = {},
modifier = Modifier.padding(horizontal = 16.dp),
alertThresholdSeconds = 7
alertThresholdSeconds = 7,
)
}
}
@@ -17,6 +17,10 @@ class SearchHandlers(
* [ItemSearchViewModel].
*/
companion object {
/**
* Creates an instance of [SearchHandlers] by binding actions to the provided
* [ItemSearchViewModel].
*/
fun create(viewModel: ItemSearchViewModel): SearchHandlers =
SearchHandlers(
onBackClick = { viewModel.trySendAction(ItemSearchAction.BackClick) },
@@ -24,7 +28,7 @@ class SearchHandlers(
onItemClick = { viewModel.trySendAction(ItemSearchAction.ItemClick(it)) },
onSearchTermChange = {
viewModel.trySendAction(ItemSearchAction.SearchTermChange(it))
}
},
)
}
}
@@ -38,6 +38,15 @@ import com.bitwarden.authenticator.ui.platform.base.util.Text
import com.bitwarden.authenticator.ui.platform.components.model.IconResource
import com.bitwarden.authenticator.ui.platform.theme.Typography
/**
* A FAB that expands, when clicked, to display a collection of options that can be clicked.
*
* @param label [Text] displayed when the FAB is expanded.
* @param items [ExpandableFabOption] buttons displayed when the FAB is expanded.
* @param expandableFabState [ExpandableFabIcon] displayed in the FAB.
* @param onStateChange Lambda invoked when the FAB expanded state changes.
*/
@Suppress("LongMethod")
@Composable
fun <T : ExpandableFabOption> ExpandableFloatingActionButton(
modifier: Modifier = Modifier,
@@ -78,7 +87,7 @@ fun <T : ExpandableFabOption> ExpandableFloatingActionButton(
onStateChange(expandableFabState.value)
expandableFabOption.onFabOptionClick()
},
expandableFabOption = expandableFabOption
expandableFabOption = expandableFabOption,
)
}
}
@@ -150,31 +159,52 @@ private fun <T : ExpandableFabOption> ExpandableFabOption(
}
@Composable
fun rememberExpandableFabState() =
private fun rememberExpandableFabState() =
remember { mutableStateOf<ExpandableFabState>(ExpandableFabState.Collapsed) }
/**
* Represents options displayed when the FAB is expanded.
*/
abstract class ExpandableFabOption(
val label: Text?,
val iconData: IconResource,
val onFabOptionClick: () -> Unit,
)
/**
* Models data for an expandable FAB icon.
*/
data class ExpandableFabIcon(
val iconData: IconResource,
val iconRotation: Float?,
)
/**
* Models the state of the expandable FAB.
*/
sealed class ExpandableFabState {
/**
* Indicates if the FAB is expanded.
*/
fun isExpanded() = this is Expanded
/**
* Invert the state of the FAB.
*/
fun toggleValue() = if (isExpanded()) {
Collapsed
} else {
Expanded
}
/**
* Indicates the FAB is collapsed.
*/
data object Collapsed : ExpandableFabState()
/**
* Indicates the FAB is expanded.
*/
data object Expanded : ExpandableFabState()
}
@@ -35,6 +35,7 @@ import java.util.concurrent.atomic.AtomicReference
/**
* Controls the root level [NavHost] for the app.
*/
@Suppress("LongMethod")
@Composable
fun RootNavScreen(
viewModel: RootNavViewModel = hiltViewModel(),
@@ -77,7 +78,7 @@ fun RootNavScreen(
unlockDestination(
onUnlocked = {
viewModel.trySendAction(RootNavAction.Internal.AppUnlocked)
}
},
)
authenticatorGraph(
navController = navController,
@@ -15,6 +15,9 @@ import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
/**
* Manages root level navigation state for the application.
*/
@HiltViewModel
class RootNavViewModel @Inject constructor(
private val authRepository: AuthRepository,
@@ -24,7 +27,7 @@ class RootNavViewModel @Inject constructor(
initialState = RootNavState(
hasSeenWelcomeGuide = settingsRepository.hasSeenWelcomeTutorial,
navState = RootNavState.NavState.Splash,
)
),
) {
init {
@@ -67,8 +70,8 @@ class RootNavViewModel @Inject constructor(
private fun handleHasSeenWelcomeTutorialChange(hasSeenWelcomeGuide: Boolean) {
settingsRepository.hasSeenWelcomeTutorial = hasSeenWelcomeGuide
if (hasSeenWelcomeGuide) {
if (settingsRepository.isUnlockWithBiometricsEnabled
&& biometricsEncryptionManager.isBiometricIntegrityValid()) {
if (settingsRepository.isUnlockWithBiometricsEnabled &&
biometricsEncryptionManager.isBiometricIntegrityValid()) {
mutableStateFlow.update { it.copy(navState = RootNavState.NavState.Locked) }
} else {
mutableStateFlow.update { it.copy(navState = RootNavState.NavState.Unlocked) }
@@ -99,7 +102,10 @@ class RootNavViewModel @Inject constructor(
}
/**
* Models root level destinations for the app.
* Models root level navigation state for the app.
*
* @property hasSeenWelcomeGuide Indicates if the user has seen the Welcome Guide screen.
* @property navState Current destination state of the app.
*/
@Parcelize
data class RootNavState(
@@ -107,6 +113,9 @@ data class RootNavState(
val navState: NavState,
) : Parcelable {
/**
* Models root level destinations for the app.
*/
@Parcelize
sealed class NavState : Parcelable {
/**
@@ -23,10 +23,10 @@ fun NavGraphBuilder.settingsGraph(
) {
navigation(
startDestination = SETTINGS_ROUTE,
route = SETTINGS_GRAPH_ROUTE
route = SETTINGS_GRAPH_ROUTE,
) {
composableWithRootPushTransitions(
route = SETTINGS_ROUTE
route = SETTINGS_ROUTE,
) {
SettingsScreen(
onNavigateToTutorial = onNavigateToTutorial,
@@ -35,13 +35,13 @@ fun NavGraphBuilder.settingsGraph(
)
}
tutorialSettingsDestination(
onTutorialFinished = { navController.popBackStack() }
onTutorialFinished = { navController.popBackStack() },
)
exportDestination(
onNavigateBack = { navController.popBackStack() }
onNavigateBack = { navController.popBackStack() },
)
importingDestination(
onNavigateBack = { navController.popBackStack() }
onNavigateBack = { navController.popBackStack() },
)
}
}
@@ -69,6 +69,7 @@ import com.bitwarden.authenticator.ui.platform.util.displayLabel
/**
* Display the settings screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
@@ -111,7 +112,7 @@ fun SettingsScreen(
topBar = {
BitwardenMediumTopAppBar(
title = stringResource(id = R.string.settings),
scrollBehavior = scrollBehavior
scrollBehavior = scrollBehavior,
)
},
) { innerPadding ->
@@ -119,7 +120,7 @@ fun SettingsScreen(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.verticalScroll(state = rememberScrollState())
.verticalScroll(state = rememberScrollState()),
) {
SecuritySettings(
state = state,
@@ -127,10 +128,10 @@ fun SettingsScreen(
onBiometricToggle = remember(viewModel) {
{
viewModel.trySendAction(
SettingsAction.SecurityClick.UnlockWithBiometricToggle(it)
SettingsAction.SecurityClick.UnlockWithBiometricToggle(it),
)
}
}
},
)
Spacer(modifier = Modifier.height(16.dp))
VaultSettings(
@@ -148,7 +149,7 @@ fun SettingsScreen(
{
viewModel.trySendAction(SettingsAction.DataClick.BackupClick)
}
}
},
)
Spacer(modifier = Modifier.height(16.dp))
AppearanceSettings(
@@ -208,7 +209,7 @@ fun SettingsScreen(
//region Security settings
@Composable
fun SecuritySettings(
private fun SecuritySettings(
state: SettingsState,
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
onBiometricToggle: (Boolean) -> Unit,
@@ -217,7 +218,7 @@ fun SecuritySettings(
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.security)
label = stringResource(id = R.string.security),
)
Spacer(modifier = Modifier.height(8.dp))
UnlockWithBiometricsRow(
@@ -236,7 +237,7 @@ fun SecuritySettings(
//region Data settings
@Composable
fun VaultSettings(
private fun VaultSettings(
modifier: Modifier = Modifier,
onExportClick: () -> Unit,
onImportClick: () -> Unit,
@@ -244,7 +245,7 @@ fun VaultSettings(
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.data)
label = stringResource(id = R.string.data),
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextRow(
@@ -261,7 +262,7 @@ fun VaultSettings(
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextRow(
@@ -278,7 +279,7 @@ fun VaultSettings(
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
)
}
},
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenExternalLinkRow(
@@ -337,7 +338,7 @@ private fun AppearanceSettings(
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.appearance)
label = stringResource(id = R.string.appearance),
)
ThemeSelectionRow(
currentSelection = state.appearance.theme,
@@ -367,7 +368,7 @@ private fun ThemeSelectionRow(
.mirrorIfRtl()
.size(24.dp),
painter = painterResource(
id = R.drawable.ic_navigate_next
id = R.drawable.ic_navigate_next,
),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface,
@@ -407,7 +408,7 @@ private fun HelpSettings(
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.help)
label = stringResource(id = R.string.help),
)
BitwardenTextRow(
text = stringResource(id = R.string.launch_tutorial),
@@ -439,7 +440,7 @@ private fun AboutSettings(
) {
BitwardenListHeaderText(
modifier = modifier,
label = stringResource(id = R.string.about)
label = stringResource(id = R.string.about),
)
BitwardenWideSwitch(
modifier = modifier,
@@ -29,6 +29,7 @@ private const val KEY_STATE = "state"
/**
* View model for the settings screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class SettingsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@@ -42,8 +43,8 @@ class SettingsViewModel @Inject constructor(
settingsRepository.appLanguage,
settingsRepository.appTheme,
settingsRepository.isUnlockWithBiometricsEnabled,
settingsRepository.isCrashLoggingEnabled
)
settingsRepository.isCrashLoggingEnabled,
),
) {
override fun handleAction(action: SettingsAction) {
when (action) {
@@ -160,7 +161,7 @@ class SettingsViewModel @Inject constructor(
private fun handleLanguageChange(language: AppLanguage) {
mutableStateFlow.update {
it.copy(
appearance = it.appearance.copy(language = language)
appearance = it.appearance.copy(language = language),
)
}
settingsRepository.appLanguage = language
@@ -173,7 +174,7 @@ class SettingsViewModel @Inject constructor(
private fun handleThemeChange(theme: AppTheme) {
mutableStateFlow.update {
it.copy(
appearance = it.appearance.copy(theme = theme)
appearance = it.appearance.copy(theme = theme),
)
}
settingsRepository.appTheme = theme
@@ -226,7 +227,7 @@ class SettingsViewModel @Inject constructor(
}
companion object {
fun createInitialState(
private fun createInitialState(
clock: Clock,
appLanguage: AppLanguage,
appTheme: AppTheme,
@@ -246,7 +247,7 @@ class SettingsViewModel @Inject constructor(
version = R.string.version
.asText()
.concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()),
copyrightInfo = copyrightInfo
copyrightInfo = copyrightInfo,
)
}
}
@@ -265,9 +266,15 @@ data class SettingsState(
val copyrightInfo: Text,
) : Parcelable {
/**
* Models the dialog state for [SettingsViewModel].
*/
@Parcelize
sealed class Dialog : Parcelable {
/**
* Displays a loading dialog with a [message].
*/
data class Loading(
val message: Text,
) : Dialog()
@@ -343,6 +350,9 @@ sealed class SettingsAction(
* Indicates the user clicked the Unlock with biometrics button.
*/
sealed class SecurityClick : SettingsAction() {
/**
* Indicates the user clicked unlock with biometrics toggle.
*/
data class UnlockWithBiometricToggle(val enabled: Boolean) : SecurityClick()
}
@@ -5,8 +5,14 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.bitwarden.authenticator.ui.platform.base.util.composableWithSlideTransitions
/**
* Route for the export data screen.
*/
const val EXPORT_ROUTE = "export"
/**
* Add the export data destination to the nav graph.
*/
fun NavGraphBuilder.exportDestination(
onNavigateBack: () -> Unit,
) {
@@ -17,6 +23,9 @@ fun NavGraphBuilder.exportDestination(
}
}
/**
* Navigate to the export data screen.
*/
fun NavController.navigateToExport(navOptions: NavOptions? = null) {
navigate(EXPORT_ROUTE, navOptions)
}
@@ -40,12 +40,16 @@ import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenTwoBut
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState
import com.bitwarden.authenticator.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportFormat
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportVaultFormat
import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManager
import com.bitwarden.authenticator.ui.platform.theme.LocalIntentManager
import com.bitwarden.authenticator.ui.platform.util.displayLabel
import kotlinx.collections.immutable.toImmutableList
/**
* Top level composable for the export data screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ExportScreen(
@@ -117,15 +121,15 @@ fun ExportScreen(
{
viewModel.trySendAction(ExportAction.DialogDismiss)
}
}
},
)
}
is ExportState.DialogState.Loading -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(
text = dialog.message
)
text = dialog.message,
),
)
}
@@ -161,7 +165,7 @@ fun ExportScreen(
viewModel.trySendAction(ExportAction.ExportFormatOptionSelect(it))
}
},
onExportClick = { shouldShowConfirmationPrompt = true }
onExportClick = { shouldShowConfirmationPrompt = true },
)
}
}
@@ -170,21 +174,21 @@ fun ExportScreen(
private fun ExportScreenContent(
modifier: Modifier = Modifier,
state: ExportState,
onExportFormatOptionSelected: (ExportFormat) -> Unit,
onExportFormatOptionSelected: (ExportVaultFormat) -> Unit,
onExportClick: () -> Unit,
) {
Column(
modifier = modifier
.imePadding()
.verticalScroll(rememberScrollState())
.verticalScroll(rememberScrollState()),
) {
val resources = LocalContext.current.resources
BitwardenMultiSelectButton(
label = stringResource(id = R.string.file_format),
options = ExportFormat.entries.map { it.displayLabel() }.toImmutableList(),
selectedOption = state.exportFormat.displayLabel(),
options = ExportVaultFormat.entries.map { it.displayLabel() }.toImmutableList(),
selectedOption = state.exportVaultFormat.displayLabel(),
onOptionSelected = { selectedOptionLabel ->
val selectedOption = ExportFormat
val selectedOption = ExportVaultFormat
.entries
.first { it.displayLabel(resources) == selectedOptionLabel }
onExportFormatOptionSelected(selectedOption)
@@ -8,7 +8,7 @@ import com.bitwarden.authenticator.data.authenticator.repository.model.ExportDat
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel
import com.bitwarden.authenticator.ui.platform.base.util.Text
import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportFormat
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportVaultFormat
import com.bitwarden.authenticator.ui.platform.util.fileExtension
import com.bitwarden.authenticator.ui.platform.util.toFormattedPattern
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -18,13 +18,16 @@ import kotlinx.parcelize.IgnoredOnParcel
import java.time.Clock
import javax.inject.Inject
/**
* Manages state for the [ExportScreen].
*/
@HiltViewModel
class ExportViewModel @Inject constructor(
private val authenticatorRepository: AuthenticatorRepository,
private val clock: Clock,
) :
BaseViewModel<ExportState, ExportEvent, ExportAction>(
initialState = ExportState(dialogState = null, exportFormat = ExportFormat.JSON)
initialState = ExportState(dialogState = null, exportVaultFormat = ExportVaultFormat.JSON),
) {
override fun handleAction(action: ExportAction) {
@@ -61,7 +64,7 @@ class ExportViewModel @Inject constructor(
private fun handleExportFormatOptionSelect(action: ExportAction.ExportFormatOptionSelect) {
mutableStateFlow.update {
it.copy(exportFormat = action.option)
it.copy(exportVaultFormat = action.option)
}
}
@@ -71,7 +74,7 @@ class ExportViewModel @Inject constructor(
pattern = "yyyyMMddHHmmss",
clock = clock,
)
val extension = state.exportFormat.fileExtension
val extension = state.exportVaultFormat.fileExtension
val fileName = "authenticator_export_$date.$extension"
sendEvent(
@@ -92,14 +95,14 @@ class ExportViewModel @Inject constructor(
viewModelScope.launch {
val result = authenticatorRepository.exportVaultData(
format = state.exportFormat,
format = state.exportVaultFormat,
fileUri = action.fileUri,
)
sendAction(
ExportAction.Internal.SaveExportDataToUriResultReceive(
result = result
)
result = result,
),
)
}
}
@@ -133,17 +136,29 @@ class ExportViewModel @Inject constructor(
}
}
/**
* Represents the state of the [ExportViewModel].
*/
data class ExportState(
@IgnoredOnParcel
val exportData: String? = null,
val dialogState: DialogState? = null,
val exportFormat: ExportFormat,
val exportVaultFormat: ExportVaultFormat,
) {
/**
* Represents state of dialogs for the [ExportViewModel].
*/
sealed class DialogState {
/**
* Displays a loading dialog with an optional [message].
*/
data class Loading(
val message: Text = R.string.loading.asText(),
) : DialogState()
/**
* Displays an error dialog with an optional [title], and a [message].
*/
data class Error(
val title: Text? = null,
val message: Text,
@@ -151,27 +166,64 @@ data class ExportState(
}
}
/**
* Represents events for the [ExportViewModel].
*/
sealed class ExportEvent {
/**
* Navigate back.
*/
data object NavigateBack : ExportEvent()
/**
* Display a toast with the provided [message].
*/
data class ShowToast(val message: Text) : ExportEvent()
/**
* Navigate to the select export destination screen.
*/
data class NavigateToSelectExportDestination(val fileName: String) : ExportEvent()
}
/**
* Represents actions for the [ExportViewModel].
*/
sealed class ExportAction {
/**
* Indicates the user has clicked the close button.
*/
data object CloseButtonClick : ExportAction()
/**
* Indicates the user has clicked the export confirmation button.
*/
data object ConfirmExportClick : ExportAction()
/**
* Indicates the user has dismissed the dialog.
*/
data object DialogDismiss : ExportAction()
data class ExportFormatOptionSelect(val option: ExportFormat) : ExportAction()
/**
* Indicates the user has selected an export format.
*/
data class ExportFormatOptionSelect(val option: ExportVaultFormat) : ExportAction()
/**
* Indicates the user has selected a location for the exported data.
*/
data class ExportLocationReceive(val fileUri: Uri) : ExportAction()
/**
* Represents actions the [ExportViewModel] itself may trigger.
*/
sealed class Internal : ExportAction() {
/**
* Indicates the result for saving exported data to a URI has been received.
*/
data class SaveExportDataToUriResultReceive(
val result: ExportDataResult,
) : Internal()
@@ -3,7 +3,7 @@ package com.bitwarden.authenticator.ui.platform.feature.settings.export.model
/**
* Represents the file formats a user can select to export the vault.
*/
enum class ExportFormat {
enum class ExportVaultFormat {
JSON,
CSV,
}
@@ -42,6 +42,10 @@ import com.bitwarden.authenticator.ui.platform.theme.LocalIntentManager
import com.bitwarden.authenticator.ui.platform.util.displayLabel
import kotlinx.collections.immutable.toImmutableList
/**
* Top level composable for the importing screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ImportingScreen(
@@ -66,7 +70,9 @@ fun ImportingScreen(
when (event) {
ImportEvent.NavigateBack -> onNavigateBack()
is ImportEvent.NavigateToSelectImportFile -> {
launcher.launch(intentManager.createFileChooserIntent(event.importFileFormat.mimeType))
launcher.launch(
intentManager.createFileChooserIntent(event.importFileFormat.mimeType),
)
}
is ImportEvent.ShowToast -> {
@@ -88,15 +94,15 @@ fun ImportingScreen(
{
viewModel.trySendAction(ImportAction.DialogDismiss)
}
}
},
)
}
is ImportState.DialogState.Loading -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(
text = dialog.message
)
text = dialog.message,
),
)
}
@@ -151,7 +157,7 @@ private fun ImportScreenContent(
Column(
modifier = modifier
.imePadding()
.verticalScroll(rememberScrollState())
.verticalScroll(rememberScrollState()),
) {
val resources = LocalContext.current.resources
BitwardenMultiSelectButton(
@@ -24,7 +24,7 @@ class ImportingViewModel @Inject constructor(
private val authenticatorRepository: AuthenticatorRepository,
) :
BaseViewModel<ImportState, ImportEvent, ImportAction>(
initialState = ImportState(importFileFormat = ImportFileFormat.BITWARDEN_JSON)
initialState = ImportState(importFileFormat = ImportFileFormat.BITWARDEN_JSON),
) {
override fun handleAction(action: ImportAction) {
@@ -97,7 +97,7 @@ class ImportingViewModel @Inject constructor(
title = R.string.an_error_has_occurred.asText(),
message = result.message
?: R.string.import_vault_failure.asText(),
)
),
)
}
}
@@ -107,7 +107,7 @@ class ImportingViewModel @Inject constructor(
sendEvent(
ImportEvent.ShowToast(
message = R.string.import_success.asText(),
)
),
)
sendEvent(ImportEvent.NavigateBack)
}
@@ -8,6 +8,9 @@ import androidx.navigation.compose.composable
const val TUTORIAL_ROUTE = "tutorial"
const val SETTINGS_TUTORIAL_ROUTE = "settings/tutorial"
/**
* Add the top level Tutorial screen to the nav graph.
*/
fun NavGraphBuilder.tutorialDestination(onTutorialFinished: () -> Unit) {
composable(TUTORIAL_ROUTE) {
TutorialScreen(
@@ -16,6 +19,9 @@ fun NavGraphBuilder.tutorialDestination(onTutorialFinished: () -> Unit) {
}
}
/**
* Add the Settings Tutorial screen to the nav graph.
*/
fun NavGraphBuilder.tutorialSettingsDestination(onTutorialFinished: () -> Unit) {
composable(SETTINGS_TUTORIAL_ROUTE) {
TutorialScreen(
@@ -24,10 +30,16 @@ fun NavGraphBuilder.tutorialSettingsDestination(onTutorialFinished: () -> Unit)
}
}
/**
* Navigate to the top level Tutorial screen.
*/
fun NavController.navigateToTutorial(navOptions: NavOptions? = null) {
navigate(route = TUTORIAL_ROUTE, navOptions = navOptions)
}
/**
* Navigate to the Tutorial screen within Settings.
*/
fun NavController.navigateToSettingsTutorial(navOptions: NavOptions? = null) {
navigate(route = SETTINGS_TUTORIAL_ROUTE, navOptions)
}
@@ -44,6 +44,10 @@ private const val QR_SCANNER_PAGE = 1
private const val UNIQUE_CODES_PAGE = 2
private const val PAGE_COUNT = 3
/**
* Top level composable for the tutorial screen.
*/
@Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun TutorialScreen(
@@ -75,7 +79,7 @@ fun TutorialScreen(
Column(
modifier = Modifier
.padding(innerPadding),
verticalArrangement = Arrangement.Center
verticalArrangement = Arrangement.Center,
) {
HorizontalPager(
modifier = Modifier
@@ -86,7 +90,7 @@ fun TutorialScreen(
userScrollEnabled = true,
) { page ->
viewModel.trySendAction(
TutorialAction.TutorialPageChange(pagerState.targetPage)
TutorialAction.TutorialPageChange(pagerState.targetPage),
)
Column(
modifier = Modifier.fillMaxWidth(),
@@ -112,7 +116,7 @@ fun TutorialScreen(
modifier = Modifier
.height(50.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
horizontalArrangement = Arrangement.Center,
) {
repeat(PAGE_COUNT) {
val color = if (pagerState.currentPage == it) {
@@ -124,7 +128,7 @@ fun TutorialScreen(
modifier = Modifier
.padding(8.dp)
.background(color, CircleShape)
.size(10.dp)
.size(10.dp),
)
}
}
@@ -137,7 +141,7 @@ fun TutorialScreen(
onClick = remember(viewModel) {
{
viewModel.trySendAction(
TutorialAction.ContinueClick
TutorialAction.ContinueClick,
)
}
},
@@ -203,7 +207,7 @@ private fun TutorialQrScannerScreen() {
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
text = stringResource(
R.string.use_your_device_camera_to_scan_codes
R.string.use_your_device_camera_to_scan_codes,
),
)
Spacer(Modifier.height(8.dp))
@@ -211,16 +215,17 @@ private fun TutorialQrScannerScreen() {
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
text = stringResource(
R.string.scan_the_qr_code_in_your_2_step_verification_settings_for_any_account
R.string.scan_the_qr_code_in_your_2_step_verification_settings_for_any_account,
),
)
}
@Suppress("MaxLineLength")
@Composable
private fun UniqueCodesContent() {
Image(
painter = painterResource(id = R.drawable.ic_tutorial_2fa),
contentDescription = stringResource(id = R.string.unique_codes)
contentDescription = stringResource(id = R.string.unique_codes),
)
Spacer(Modifier.height(24.dp))
Text(
@@ -233,17 +238,17 @@ private fun UniqueCodesContent() {
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
text = stringResource(
R.string.when_using_2_step_verification_youll_enter_your_username_and_password_and_a_code_generated_in_this_app
R.string.when_using_2_step_verification_youll_enter_your_username_and_password_and_a_code_generated_in_this_app,
),
)
}
@Preview
@Composable
fun TutorialScreenPreview() {
private fun TutorialScreenPreview() {
Box {
TutorialScreen(
onTutorialFinished = {}
onTutorialFinished = {},
)
}
}
@@ -16,7 +16,7 @@ import javax.inject.Inject
@HiltViewModel
class TutorialViewModel @Inject constructor() :
BaseViewModel<TutorialState, TutorialEvent, TutorialAction>(
initialState = TutorialState.IntroSlide
initialState = TutorialState.IntroSlide,
) {
override fun handleAction(action: TutorialAction) {
@@ -84,7 +84,7 @@ sealed class TutorialState(
@Parcelize
data object QrScannerSlide : TutorialState(
continueButtonText = R.string.continue_button.asText(),
isLastPage = false
isLastPage = false,
)
/**
@@ -93,7 +93,7 @@ sealed class TutorialState(
@Parcelize
data object UniqueCodesSlide : TutorialState(
continueButtonText = R.string.get_started.asText(),
isLastPage = true
isLastPage = true,
)
}
@@ -30,6 +30,9 @@ import com.bitwarden.authenticator.ui.platform.manager.intent.IntentManagerImpl
import com.bitwarden.authenticator.ui.platform.manager.permissions.PermissionsManager
import com.bitwarden.authenticator.ui.platform.manager.permissions.PermissionsManagerImpl
/**
* The overall application theme. This can be configured to support a [theme] and [dynamicColor].
*/
@Composable
fun AuthenticatorTheme(
theme: AppTheme = AppTheme.DEFAULT,
@@ -3,22 +3,22 @@ package com.bitwarden.authenticator.ui.platform.util
import com.bitwarden.authenticator.R
import com.bitwarden.authenticator.ui.platform.base.util.Text
import com.bitwarden.authenticator.ui.platform.base.util.asText
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportFormat
import com.bitwarden.authenticator.ui.platform.feature.settings.export.model.ExportVaultFormat
/**
* Provides a human-readable label for the export format.
*/
val ExportFormat.displayLabel: Text
val ExportVaultFormat.displayLabel: Text
get() = when (this) {
ExportFormat.JSON -> R.string.export_format_label_json.asText()
ExportFormat.CSV -> R.string.export_format_label_csv.asText()
ExportVaultFormat.JSON -> R.string.export_format_label_json.asText()
ExportVaultFormat.CSV -> R.string.export_format_label_csv.asText()
}
/**
* Provides the file extension associated with the export format.
*/
val ExportFormat.fileExtension: String
val ExportVaultFormat.fileExtension: String
get() = when (this) {
ExportFormat.JSON -> "json"
ExportFormat.CSV -> "csv"
ExportVaultFormat.JSON -> "json"
ExportVaultFormat.CSV -> "csv"
}
@@ -7,7 +7,6 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
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
@@ -28,10 +27,6 @@ class MainViewModelTest : BaseViewModelTest() {
mainViewModel = MainViewModel(settingsRepository)
}
@AfterEach
fun tearDown() {
}
@Test
fun `on AppThemeChanged should update state`() {
assertEquals(
@@ -32,10 +32,10 @@ class FeatureFlagDiskSourceTest {
featureFlagDiskSource.featureFlagsConfiguration = FEATURE_FLAGS_CONFIGURATION
assertEquals(
json.parseToJsonElement(
FEATURE_FLAGS_CONFIGURATION_JSON
FEATURE_FLAGS_CONFIGURATION_JSON,
),
json.parseToJsonElement(
fakeSharedPreferences.getString(featureFlagsConfigKey, null)!!
fakeSharedPreferences.getString(featureFlagsConfigKey, null)!!,
),
)
@@ -53,7 +53,6 @@ class FeatureFlagDiskSourceTest {
assertEquals(FEATURE_FLAGS_CONFIGURATION, awaitItem())
}
}
}
private const val FEATURE_FLAGS_CONFIGURATION_JSON = """
@@ -67,6 +66,6 @@ private const val FEATURE_FLAGS_CONFIGURATION_JSON = """
private val FEATURE_FLAGS_CONFIGURATION = FeatureFlagsConfiguration(
featureFlags = mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(true)
)
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(true),
),
)
@@ -97,4 +97,3 @@ private data class ZonedDateTimeData(
@SerialName("dataAsZonedDateTime")
val dataAsZonedDateTime: ZonedDateTime,
)
@@ -49,7 +49,7 @@ class FeatureFlagManagerTest {
@Test
fun `getFeatureFlag should return default value if flag doesn't exist`() {
fakeFeatureFlagRepository.featureFlagsConfiguration = FEATURE_FLAGS_CONFIG.copy(
featureFlags = emptyMap()
featureFlags = emptyMap(),
)
val flagValue = featureFlagManager.getFeatureFlag(
@@ -65,5 +65,5 @@ private val FEATURE_FLAGS_CONFIG =
mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to
JsonPrimitive(true),
)
),
)
@@ -29,7 +29,7 @@ class FeatureFlagRepositoryTest {
assertEquals(
FEATURE_FLAGS_CONFIG,
fakeFeatureFlagDiskSource.featureFlagsConfiguration
fakeFeatureFlagDiskSource.featureFlagsConfiguration,
)
}
@@ -49,5 +49,5 @@ private val FEATURE_FLAGS_CONFIG =
featureFlags = mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to
JsonPrimitive(LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue),
)
),
)
@@ -33,6 +33,6 @@ private val FEATURE_FLAGS_CONFIG =
FeatureFlagsConfiguration(
featureFlags = mapOf(
LocalFeatureFlag.BitwardenAuthenticationEnabled.name to
JsonPrimitive(LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue)
)
JsonPrimitive(LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue),
),
)
@@ -26,4 +26,3 @@ abstract class BaseViewModelTest {
}
}
}
-373
View File
@@ -1,373 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>ChainWrapping:EditItemScreen.kt$+</ID>
<ID>ChainWrapping:RootNavViewModel.kt$RootNavViewModel$&amp;&amp;</ID>
<ID>CyclomaticComplexMethod:BitwardenExportParser.kt$BitwardenExportParser$private fun ExportJsonData.ExportItem.toAuthenticatorItemEntity(): AuthenticatorItemEntity</ID>
<ID>EmptyFunctionBlock:MainViewModelTest.kt$MainViewModelTest${ }</ID>
<ID>LongMethod:AuthenticatorNavBarScreen.kt$@Composable private fun AuthenticatorBottomAppBar( navController: NavController, verificationCodesTabClickedAction: () -&gt; Unit, settingsTabClickedAction: () -&gt; Unit, modifier: Modifier = Modifier, )</ID>
<ID>LongMethod:BitwardenExportParser.kt$BitwardenExportParser$private fun ExportJsonData.ExportItem.toAuthenticatorItemEntity(): AuthenticatorItemEntity</ID>
<ID>LongMethod:EditItemScreen.kt$@Composable fun EditItemContent( modifier: Modifier = Modifier, viewState: EditItemState.ViewState.Content, onIssuerNameTextChange: (String) -&gt; Unit = {}, onUsernameTextChange: (String) -&gt; Unit = {}, onToggleFavorite: (Boolean) -&gt; Unit = {}, onTypeOptionClicked: (AuthenticatorItemType) -&gt; Unit = {}, onTotpCodeTextChange: (String) -&gt; Unit = {}, onAlgorithmOptionClicked: (AuthenticatorItemAlgorithm) -&gt; Unit = {}, onRefreshPeriodOptionClicked: (AuthenticatorRefreshPeriodOption) -&gt; Unit = {}, onNumberOfDigitsChanged: (Int) -&gt; Unit = {}, onExpandAdvancedOptionsClicked: () -&gt; Unit = {}, )</ID>
<ID>LongMethod:EditItemScreen.kt$@Composable private fun AdvancedOptions( modifier: Modifier = Modifier, viewState: EditItemState.ViewState.Content, onExpandStateChange: () -&gt; Unit, onAlgorithmOptionClicked: (AuthenticatorItemAlgorithm) -&gt; Unit, onTypeOptionClicked: (AuthenticatorItemType) -&gt; Unit, onRefreshPeriodOptionClicked: (AuthenticatorRefreshPeriodOption) -&gt; Unit, onNumberOfDigitsChanged: (Int) -&gt; Unit, )</ID>
<ID>LongMethod:EditItemScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun EditItemScreen( viewModel: EditItemViewModel = hiltViewModel(), onNavigateBack: () -&gt; Unit = { }, )</ID>
<ID>LongMethod:EditItemViewModel.kt$EditItemViewModel$private fun handleItemDataReceive(action: EditItemAction.Internal.EditItemDataReceive)</ID>
<ID>LongMethod:ExpandableFloatingActionButton.kt$@Composable fun &lt;T : ExpandableFabOption&gt; ExpandableFloatingActionButton( modifier: Modifier = Modifier, label: Text?, items: List&lt;T&gt;, expandableFabState: MutableState&lt;ExpandableFabState&gt; = rememberExpandableFabState(), expandableFabIcon: ExpandableFabIcon, onStateChange: (expandableFabState: ExpandableFabState) -&gt; Unit = { }, )</ID>
<ID>LongMethod:ExportScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun ExportScreen( viewModel: ExportViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, onNavigateBack: () -&gt; Unit, )</ID>
<ID>LongMethod:ImportingScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun ImportingScreen( viewModel: ImportingViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, onNavigateBack: () -&gt; Unit, )</ID>
<ID>LongMethod:ItemListingScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun EmptyItemListingContent( modifier: Modifier = Modifier, appTheme: AppTheme, scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior( rememberTopAppBarState() ), onAddCodeClick: () -&gt; Unit, onScanQuCodeClick: () -&gt; Unit, onEnterSetupKeyClick: () -&gt; Unit, )</ID>
<ID>LongMethod:ItemListingScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun ItemListingScreen( viewModel: ItemListingViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, permissionsManager: PermissionsManager = LocalPermissionsManager.current, onNavigateBack: () -&gt; Unit, onNavigateToSearch: () -&gt; Unit, onNavigateToQrCodeScanner: () -&gt; Unit, onNavigateToManualKeyEntry: () -&gt; Unit, onNavigateToEditItemScreen: (id: String) -&gt; Unit, )</ID>
<ID>LongMethod:ItemListingScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ItemListingContent( state: ItemListingState.ViewState.Content, scrollBehavior: TopAppBarScrollBehavior, onNavigateToSearch: () -&gt; Unit, onScanQrCodeClick: () -&gt; Unit, onEnterSetupKeyClick: () -&gt; Unit, onItemClick: (String) -&gt; Unit, onEditItemClick: (String) -&gt; Unit, onDeleteItemClick: (String) -&gt; Unit, )</ID>
<ID>LongMethod:ItemSearchScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun ItemSearchScreen( viewModel: ItemSearchViewModel = hiltViewModel(), onNavigateBack: () -&gt; Unit, )</ID>
<ID>LongMethod:RootNavScreen.kt$@Composable fun RootNavScreen( viewModel: RootNavViewModel = hiltViewModel(), navController: NavHostController = rememberNavController(), onSplashScreenRemoved: () -&gt; Unit = {}, onExitApplication: () -&gt; Unit, )</ID>
<ID>LongMethod:SettingsScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( viewModel: SettingsViewModel = hiltViewModel(), biometricsManager: BiometricsManager = LocalBiometricsManager.current, intentManager: IntentManager = LocalIntentManager.current, onNavigateToTutorial: () -&gt; Unit, onNavigateToExport: () -&gt; Unit, onNavigateToImport: () -&gt; Unit, )</ID>
<ID>LongMethod:TutorialScreen.kt$@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun TutorialScreen( viewModel: TutorialViewModel = hiltViewModel(), onTutorialFinished: () -&gt; Unit, )</ID>
<ID>LongMethod:UnlockScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun UnlockScreen( viewModel: UnlockViewModel = hiltViewModel(), biometricsManager: BiometricsManager = LocalBiometricsManager.current, onUnlocked: () -&gt; Unit, )</ID>
<ID>LongParameterList:AuthenticatorNavBarNavigation.kt$( onNavigateBack: () -&gt; Unit, onNavigateToSearch: () -&gt; Unit, onNavigateToQrCodeScanner: () -&gt; Unit, onNavigateToManualKeyEntry: () -&gt; Unit, onNavigateToEditItem: (itemId: String) -&gt; Unit, onNavigateToExport: () -&gt; Unit, onNavigateToImport: () -&gt; Unit, onNavigateToTutorial: () -&gt; Unit, )</ID>
<ID>LongParameterList:ItemListingGraphNavigation.kt$( navController: NavController, navigateBack: () -&gt; Unit, navigateToSearch: () -&gt; Unit, navigateToQrCodeScanner: () -&gt; Unit, navigateToManualKeyEntry: () -&gt; Unit, navigateToEditItem: (String) -&gt; Unit, navigateToExport: () -&gt; Unit, navigateToImport: () -&gt; Unit, navigateToTutorial: () -&gt; Unit, )</ID>
<ID>MagicNumber:ItemListingViewModel.kt$ItemListingViewModel$10</ID>
<ID>MagicNumber:ItemListingViewModel.kt$ItemListingViewModel$30</ID>
<ID>MagicNumber:ItemListingViewModel.kt$ItemListingViewModel$5</ID>
<ID>MagicNumber:ItemListingViewModel.kt$ItemListingViewModel$6</ID>
<ID>MagicNumber:ItemSearchViewModel.kt$ItemSearchViewModel$8</ID>
<ID>MagicNumber:SettingsDiskSourceImpl.kt$SettingsDiskSourceImpl$7</ID>
<ID>MatchingDeclarationName:ExportVaultFormat.kt$ExportFormat</ID>
<ID>MaxLineLength:AegisExportParser.kt$AegisExportParser$private</ID>
<ID>MaxLineLength:BitwardenExportParser.kt$BitwardenExportParser$"${TotpCodeManager.TOTP_CODE_PREFIX}/$name?${TotpCodeManager.SECRET_PARAM}=$otpString"</ID>
<ID>MaxLineLength:FeatureFlag.kt$FeatureFlag$*</ID>
<ID>MaxLineLength:ImportingScreen.kt$launcher.launch(intentManager.createFileChooserIntent(event.importFileFormat.mimeType))</ID>
<ID>MaxLineLength:ItemListingViewModel.kt$ItemListingViewModel$private</ID>
<ID>MaxLineLength:ItemSearchViewModel.kt$ItemSearchViewModel$private</ID>
<ID>MaxLineLength:TutorialScreen.kt$R.string.when_using_2_step_verification_youll_enter_your_username_and_password_and_a_code_generated_in_this_app</ID>
<ID>MaxLineLength:TwoFasExportParser.kt$TwoFasExportParser$private</ID>
<ID>MaximumLineLength:TutorialScreen.kt$ </ID>
<ID>NoBlankLineBeforeRbrace:AuthenticatorDatabase.kt$AuthenticatorDatabase$ </ID>
<ID>NoBlankLineBeforeRbrace:AuthenticatorDiskSource.kt$AuthenticatorDiskSource$ </ID>
<ID>NoBlankLineBeforeRbrace:AuthenticatorManagerModule.kt$AuthenticatorManagerModule$ </ID>
<ID>NoBlankLineBeforeRbrace:AuthenticatorRepositoryModule.kt$AuthenticatorRepositoryModule$ </ID>
<ID>NoBlankLineBeforeRbrace:AuthenticatorSdkModule.kt$AuthenticatorSdkModule$ </ID>
<ID>NoBlankLineBeforeRbrace:AuthenticatorSdkSourceImpl.kt$AuthenticatorSdkSourceImpl$ </ID>
<ID>NoBlankLineBeforeRbrace:ExportDataResult.kt$ExportDataResult$ </ID>
<ID>NoBlankLineBeforeRbrace:FeatureFlagDiskSourceTest.kt$FeatureFlagDiskSourceTest$ </ID>
<ID>NoBlankLineBeforeRbrace:ImportManager.kt$ImportManager$ </ID>
<ID>NoBlankLineBeforeRbrace:MainViewModel.kt$MainViewModel$ </ID>
<ID>NoBlankLineBeforeRbrace:QrCodeAnalyzerImpl.kt$QrCodeAnalyzerImpl$ </ID>
<ID>NoBlankLineBeforeRbrace:UnlockViewModel.kt$UnlockEvent$ </ID>
<ID>NoConsecutiveBlankLines:BaseViewModelTest.kt$ </ID>
<ID>NoConsecutiveBlankLines:FeatureFlag.kt$ </ID>
<ID>NoConsecutiveBlankLines:ZonedDateTimeSerializerTest.kt$ </ID>
<ID>SpacingAroundColon:AuthenticatorManagerModule.kt$AuthenticatorManagerModule$:</ID>
<ID>SpacingAroundCurly:ItemSearchScreen.kt${</ID>
<ID>TooGenericExceptionCaught:AuthenticatorRepositoryImpl.kt$AuthenticatorRepositoryImpl$e: Exception</ID>
<ID>TooGenericExceptionCaught:ImportManagerImpl.kt$ImportManagerImpl$e: Throwable</ID>
<ID>TooGenericExceptionCaught:ItemListingViewModel.kt$ItemListingViewModel$e: Throwable</ID>
<ID>TooManyFunctions:AuthenticatorRepositoryImpl.kt$AuthenticatorRepositoryImpl : AuthenticatorRepository</ID>
<ID>TooManyFunctions:EditItemViewModel.kt$EditItemViewModel : BaseViewModel</ID>
<ID>TooManyFunctions:ItemListingViewModel.kt$ItemListingViewModel : BaseViewModel</ID>
<ID>TooManyFunctions:ItemSearchViewModel.kt$ItemSearchViewModel : BaseViewModel</ID>
<ID>TooManyFunctions:SettingsViewModel.kt$SettingsViewModel : BaseViewModel</ID>
<ID>TrailingCommaOnCallSite:AuthRepositoryModule.kt$AuthRepositoryModule$( authDiskSource = authDiskSource )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorDatabase.kt$AuthenticatorDatabase$[ AuthenticatorItemEntity::class ]</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorDiskModule.kt$AuthenticatorDiskModule$( context = app, klass = AuthenticatorDatabase::class.java, name = "authenticator_database" )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorDiskSourceImpl.kt$AuthenticatorDiskSourceImpl$( forceItemsFlow, itemDao.getAllItems() )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorManagerModule.kt$AuthenticatorManagerModule$( authenticatorSdkSource = authenticatorSdkSource, dispatcherManager = dispatcherManager, clock = clock )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorNavBarNavigation.kt$( onNavigateBack = onNavigateBack, onNavigateToSearch = onNavigateToSearch, onNavigateToQrCodeScanner = onNavigateToQrCodeScanner, onNavigateToManualKeyEntry = onNavigateToManualKeyEntry, onNavigateToEditItem = onNavigateToEditItem, onNavigateToExport = onNavigateToExport, onNavigateToImport = onNavigateToImport, onNavigateToTutorial = onNavigateToTutorial )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorNavBarScreen.kt$( isVisible = false, onClick = { // Do nothing }, modifier = Modifier .fillMaxWidth() .height(appBarHeightPx.toDp()) )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorNavigation.kt$( navController = navController, navigateBack = onNavigateBack, navigateToSearch = { navController.navigateToSearch() }, navigateToQrCodeScanner = { navController.navigateToQrCodeScanScreen() }, navigateToManualKeyEntry = { navController.navigateToManualCodeEntryScreen() }, navigateToEditItem = { navController.navigateToEditItem(itemId = it) }, navigateToExport = { navController.navigateToExport() }, navigateToImport = { navController.navigateToImporting() }, navigateToTutorial = { navController.navigateToSettingsTutorial() } )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorNavigation.kt$( onNavigateBack = onNavigateBack, onNavigateToSearch = { navController.navigateToSearch() }, onNavigateToQrCodeScanner = { navController.navigateToQrCodeScanScreen() }, onNavigateToManualKeyEntry = { navController.navigateToManualCodeEntryScreen() }, onNavigateToEditItem = { navController.navigateToEditItem(itemId = it) }, onNavigateToExport = { navController.navigateToExport() }, onNavigateToImport = { navController.navigateToImporting() }, onNavigateToTutorial = { navController.navigateToSettingsTutorial() } )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorNavigation.kt$( startDestination = AUTHENTICATOR_NAV_BAR_ROUTE, route = AUTHENTICATOR_GRAPH_ROUTE )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorRepositoryImpl.kt$AuthenticatorRepositoryImpl$( ExportJsonData( encrypted = false, items = authenticatorDiskSource .getItems() .firstOrNull() .orEmpty() .map { it.toExportJsonItem() }, ) )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorRepositoryImpl.kt$AuthenticatorRepositoryImpl$( id = itemId, key = updateItemRequest.key, accountName = updateItemRequest.accountName, type = updateItemRequest.type, period = updateItemRequest.period, digits = updateItemRequest.digits, issuer = updateItemRequest.issuer, userId = null, favorite = updateItemRequest.favorite )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorRepositoryImpl.kt$AuthenticatorRepositoryImpl$( importFileFormat = format, byteArray = it )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorRepositoryImpl.kt$AuthenticatorRepositoryImpl$( onSuccess = { it }, onFailure = { ImportDataResult.Error() } )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorSdkSourceImpl.kt$AuthenticatorSdkSourceImpl$( PasswordGeneratorRequest( lowercase = true, uppercase = true, numbers = true, special = true, length = 7.toUByte(), avoidAmbiguous = true, minLowercase = null, minUppercase = null, minNumber = null, minSpecial = null ) )</ID>
<ID>TrailingCommaOnCallSite:AuthenticatorSdkSourceImpl.kt$AuthenticatorSdkSourceImpl$( lowercase = true, uppercase = true, numbers = true, special = true, length = 7.toUByte(), avoidAmbiguous = true, minLowercase = null, minUppercase = null, minNumber = null, minSpecial = null )</ID>
<ID>TrailingCommaOnCallSite:BitwardenExportParser.kt$BitwardenExportParser$( items = exportData .items .filter { it.login?.totp != null } .toAuthenticatorItemEntities() )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( EditItemAction.AlgorithmOptionClick(it) )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( EditItemAction.ExpandAdvancedOptionsClick )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( EditItemAction.FavoriteToggleClick(it) )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( EditItemAction.IssuerNameTextChange(it) )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( EditItemAction.NumberOfDigitsOptionClick(it) )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( EditItemAction.RefreshPeriodOptionClick(it) )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( EditItemAction.TotpCodeTextChange(it) )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( EditItemAction.TypeOptionClick(it) )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( EditItemAction.UsernameTextChange(it) )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( context, event.message(resources), Toast.LENGTH_LONG )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( dialogState = state.dialog, onDismissRequest = { viewModel.trySendAction(EditItemAction.DismissDialog) } )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( iconData = IconData.Local( iconRes = if (viewState.isAdvancedOptionsExpanded) { R.drawable.ic_chevron_up } else { R.drawable.ic_chevron_down } ), contentDescription = stringResource( id = R.string.collapse_advanced_options ), tint = MaterialTheme.colorScheme.primary )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( iconRes = if (viewState.isAdvancedOptionsExpanded) { R.drawable.ic_chevron_up } else { R.drawable.ic_chevron_down } )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( id = R.string.collapse_advanced_options )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( id = R.string.refresh_period_seconds, it.seconds )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( indication = rememberRipple( bounded = true, color = MaterialTheme.colorScheme.primary, ), interactionSource = remember { MutableInteractionSource() } )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), viewState = viewState, onExpandStateChange = onExpandAdvancedOptionsClicked, onAlgorithmOptionClicked = onAlgorithmOptionClicked, onTypeOptionClicked = onTypeOptionClicked, onRefreshPeriodOptionClicked = onRefreshPeriodOptionClicked, onNumberOfDigitsChanged = onNumberOfDigitsChanged )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( modifier = Modifier .fillMaxWidth(), label = stringResource(id = R.string.refresh_period), options = refreshPeriodOptionsWithStrings.values.toImmutableList(), selectedOption = stringResource( id = R.string.refresh_period_seconds, viewState.itemData.refreshPeriod.seconds, ), onOptionSelected = remember(viewState) { { selectedOption -&gt; val selectedOptionName = refreshPeriodOptionsWithStrings .entries .first { it.value == selectedOption } .key onRefreshPeriodOptionClicked(selectedOptionName) } } )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( modifier = Modifier .imePadding() .padding(innerPadding), viewState = viewState, onIssuerNameTextChange = remember(viewModel) { { viewModel.trySendAction( EditItemAction.IssuerNameTextChange(it) ) } }, onUsernameTextChange = remember(viewModel) { { viewModel.trySendAction( EditItemAction.UsernameTextChange(it) ) } }, onToggleFavorite = remember(viewModel) { { viewModel.trySendAction( EditItemAction.FavoriteToggleClick(it) ) } }, onTypeOptionClicked = remember(viewModel) { { viewModel.trySendAction( EditItemAction.TypeOptionClick(it) ) } }, onTotpCodeTextChange = remember(viewModel) { { viewModel.trySendAction( EditItemAction.TotpCodeTextChange(it) ) } }, onAlgorithmOptionClicked = remember(viewModel) { { viewModel.trySendAction( EditItemAction.AlgorithmOptionClick(it) ) } }, onRefreshPeriodOptionClicked = remember(viewModel) { { viewModel.trySendAction( EditItemAction.RefreshPeriodOptionClick(it) ) } }, onNumberOfDigitsChanged = remember(viewModel) { { viewModel.trySendAction( EditItemAction.NumberOfDigitsOptionClick(it) ) } }, onExpandAdvancedOptionsClicked = remember(viewModel) { { viewModel.trySendAction( EditItemAction.ExpandAdvancedOptionsClick ) } } )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( title = dialogState.title, message = dialogState.message )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( title = stringResource( id = R.string.edit_item, ), scrollBehavior = scrollBehavior, navigationIcon = painterResource(id = R.drawable.ic_close), navigationIconContentDescription = stringResource(id = R.string.close), onNavigationIconClick = remember(viewModel) { { viewModel.trySendAction(EditItemAction.CancelClick) } }, actions = { BitwardenTextButton( label = stringResource(id = R.string.save), onClick = remember(viewModel) { { viewModel.trySendAction(EditItemAction.SaveClick) } }, modifier = Modifier.semantics { testTag = "SaveButton" }, ) } )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( viewState = EditItemState.ViewState.Content( isAdvancedOptionsExpanded = false, itemData = EditItemData( refreshPeriod = AuthenticatorRefreshPeriodOption.THIRTY, totpCode = "123456", type = AuthenticatorItemType.TOTP, username = "account name", issuer = "issuer", algorithm = AuthenticatorItemAlgorithm.SHA1, digits = 6, favorite = false, ), minDigitsAllowed = 5, maxDigitsAllowed = 10, ) )</ID>
<ID>TrailingCommaOnCallSite:EditItemScreen.kt$( viewState = EditItemState.ViewState.Content( isAdvancedOptionsExpanded = true, itemData = EditItemData( refreshPeriod = AuthenticatorRefreshPeriodOption.THIRTY, totpCode = "123456", type = AuthenticatorItemType.TOTP, username = "account name", issuer = "issuer", algorithm = AuthenticatorItemAlgorithm.SHA1, digits = 6, favorite = true, ), minDigitsAllowed = 5, maxDigitsAllowed = 10, ) )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( AuthenticatorItemEntity( id = state.itemId, key = content.itemData.totpCode.trim(), accountName = content.itemData.username?.trim(), type = content.itemData.type, algorithm = content.itemData.algorithm, period = content.itemData.refreshPeriod.seconds, digits = content.itemData.digits, issuer = content.itemData.issuer.trim(), favorite = content.itemData.favorite, ) )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( R.string.saving.asText() )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( algorithm = action.algorithmOption )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( dialog = EditItemState.DialogState.Generic( title = R.string.an_error_has_occurred.asText(), message = R.string.key_is_invalid.asText() ) )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( dialog = EditItemState.DialogState.Generic( title = R.string.an_error_has_occurred.asText(), message = R.string.validation_field_required.asText(R.string.key.asText()), ) )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( dialog = EditItemState.DialogState.Generic( title = R.string.an_error_has_occurred.asText(), message = R.string.validation_field_required.asText(R.string.name.asText()), ) )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( dialog = EditItemState.DialogState.Loading( R.string.saving.asText() ) )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( digits = action.digits )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( favorite = action.favorite )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( initialState = savedStateHandle[KEY_STATE] ?: EditItemState( itemId = EditItemArgs(savedStateHandle).itemId, viewState = EditItemState.ViewState.Loading, dialog = null, ) )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( isAdvancedOptionsExpanded = currentContent.isAdvancedOptionsExpanded.not() )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( issuer = action.issuerName )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( message = R.string.generic_error_message.asText() )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( refreshPeriod = action.period )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( title = R.string.an_error_has_occurred.asText(), message = R.string.key_is_invalid.asText() )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( totpCode = action.totpCode )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( type = action.typeOption )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( username = action.username )</ID>
<ID>TrailingCommaOnCallSite:EditItemViewModel.kt$EditItemViewModel$( viewState = EditItemState.ViewState.Loading )</ID>
<ID>TrailingCommaOnCallSite:ExpandableFloatingActionButton.kt$( onFabOptionClick = { expandableFabState.value = expandableFabState.value.toggleValue() onStateChange(expandableFabState.value) expandableFabOption.onFabOptionClick() }, expandableFabOption = expandableFabOption )</ID>
<ID>TrailingCommaOnCallSite:ExportScreen.kt$( modifier = Modifier .padding(paddingValues) .fillMaxSize(), state = state, onExportFormatOptionSelected = remember(viewModel) { { viewModel.trySendAction(ExportAction.ExportFormatOptionSelect(it)) } }, onExportClick = { shouldShowConfirmationPrompt = true } )</ID>
<ID>TrailingCommaOnCallSite:ExportScreen.kt$( modifier = modifier .imePadding() .verticalScroll(rememberScrollState()) )</ID>
<ID>TrailingCommaOnCallSite:ExportScreen.kt$( text = dialog.message )</ID>
<ID>TrailingCommaOnCallSite:ExportScreen.kt$( visibilityState = BasicDialogState.Shown( title = dialog.title, message = dialog.message, ), onDismissRequest = remember(viewModel) { { viewModel.trySendAction(ExportAction.DialogDismiss) } } )</ID>
<ID>TrailingCommaOnCallSite:ExportScreen.kt$( visibilityState = LoadingDialogState.Shown( text = dialog.message ) )</ID>
<ID>TrailingCommaOnCallSite:ExportViewModel.kt$ExportViewModel$( ExportAction.Internal.SaveExportDataToUriResultReceive( result = result ) )</ID>
<ID>TrailingCommaOnCallSite:ExportViewModel.kt$ExportViewModel$( initialState = ExportState(dialogState = null, exportFormat = ExportFormat.JSON) )</ID>
<ID>TrailingCommaOnCallSite:ExportViewModel.kt$ExportViewModel$( result = result )</ID>
<ID>TrailingCommaOnCallSite:FakeFeatureFlagRepository.kt$( LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue) )</ID>
<ID>TrailingCommaOnCallSite:FakeFeatureFlagRepository.kt$( featureFlags = mapOf( LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue) ) )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagDiskSourceTest.kt$( LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(true) )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagDiskSourceTest.kt$( featureFlags = mapOf( LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(true) ) )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagDiskSourceTest.kt$FeatureFlagDiskSourceTest$( FEATURE_FLAGS_CONFIGURATION_JSON )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagDiskSourceTest.kt$FeatureFlagDiskSourceTest$( fakeSharedPreferences.getString(featureFlagsConfigKey, null)!! )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagManagerTest.kt$( mapOf( LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(true), ) )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagManagerTest.kt$FeatureFlagManagerTest$( featureFlags = emptyMap() )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagRepositoryImpl.kt$FeatureFlagRepositoryImpl$( LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagRepositoryImpl.kt$FeatureFlagRepositoryImpl$( LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive( LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue ) )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagRepositoryImpl.kt$FeatureFlagRepositoryImpl$( mapOf( LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive( LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue ) ) )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagRepositoryTest.kt$( featureFlags = mapOf( LocalFeatureFlag.BitwardenAuthenticationEnabled.name to JsonPrimitive(LocalFeatureFlag.BitwardenAuthenticationEnabled.defaultValue), ) )</ID>
<ID>TrailingCommaOnCallSite:FeatureFlagRepositoryTest.kt$FeatureFlagRepositoryTest$( FEATURE_FLAGS_CONFIG, fakeFeatureFlagDiskSource.featureFlagsConfiguration )</ID>
<ID>TrailingCommaOnCallSite:ImportingScreen.kt$( modifier = modifier .imePadding() .verticalScroll(rememberScrollState()) )</ID>
<ID>TrailingCommaOnCallSite:ImportingScreen.kt$( text = dialog.message )</ID>
<ID>TrailingCommaOnCallSite:ImportingScreen.kt$( visibilityState = BasicDialogState.Shown( title = dialog.title, message = dialog.message, ), onDismissRequest = remember(viewModel) { { viewModel.trySendAction(ImportAction.DialogDismiss) } } )</ID>
<ID>TrailingCommaOnCallSite:ImportingScreen.kt$( visibilityState = LoadingDialogState.Shown( text = dialog.message ) )</ID>
<ID>TrailingCommaOnCallSite:ImportingViewModel.kt$ImportingViewModel$( ImportEvent.ShowToast( message = R.string.import_success.asText(), ) )</ID>
<ID>TrailingCommaOnCallSite:ImportingViewModel.kt$ImportingViewModel$( dialogState = ImportState.DialogState.Error( title = R.string.an_error_has_occurred.asText(), message = result.message ?: R.string.import_vault_failure.asText(), ) )</ID>
<ID>TrailingCommaOnCallSite:ImportingViewModel.kt$ImportingViewModel$( initialState = ImportState(importFileFormat = ImportFileFormat.BITWARDEN_JSON) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingExpandableFabAction.kt$ItemListingExpandableFabAction.EnterSetupKey$( label, icon, onEnterSetupKeyClick )</ID>
<ID>TrailingCommaOnCallSite:ItemListingExpandableFabAction.kt$ItemListingExpandableFabAction.ScanQrCode$( label, icon, onScanQrCodeClick )</ID>
<ID>TrailingCommaOnCallSite:ItemListingGraphNavigation.kt$( navController = navController, onNavigateToExport = navigateToExport, onNavigateToImport = navigateToImport, onNavigateToTutorial = navigateToTutorial )</ID>
<ID>TrailingCommaOnCallSite:ItemListingGraphNavigation.kt$( onNavigateBack = { navController.popBackStack() } )</ID>
<ID>TrailingCommaOnCallSite:ItemListingGraphNavigation.kt$( onNavigateBack = { navController.popBackStack() }, onNavigateToQrCodeScreen = { navController.popBackStack() navController.navigateToQrCodeScanScreen() } )</ID>
<ID>TrailingCommaOnCallSite:ItemListingGraphNavigation.kt$( route = ITEM_LISTING_GRAPH_ROUTE, navOptions = navOptions )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( ItemListingAction.DeleteItemClick(it) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( ItemListingAction.EditItemClick(it) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( ItemListingAction.ItemClick(it) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( ItemListingAction.SearchClick )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( ItemListingExpandableFabAction.ScanQrCode( label = R.string.scan_a_qr_code.asText(), icon = IconResource( iconPainter = painterResource(id = R.drawable.ic_camera), contentDescription = stringResource(id = R.string.scan_a_qr_code), testTag = "ScanQRCodeButton", ), onScanQrCodeClick = onScanQrCodeClick, ), ItemListingExpandableFabAction.EnterSetupKey( label = R.string.enter_key_manually.asText(), icon = IconResource( iconPainter = painterResource(id = R.drawable.ic_keyboard_24px), contentDescription = stringResource(id = R.string.enter_key_manually), testTag = "EnterSetupKeyButton", ), onEnterSetupKeyClick = onEnterSetupKeyClick ) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( ItemListingExpandableFabAction.ScanQrCode( label = R.string.scan_a_qr_code.asText(), icon = IconResource( iconPainter = painterResource(id = R.drawable.ic_camera), contentDescription = stringResource(id = R.string.scan_a_qr_code), testTag = "ScanQRCodeButton", ), onScanQrCodeClick = onScanQuCodeClick ), ItemListingExpandableFabAction.EnterSetupKey( label = R.string.enter_key_manually.asText(), icon = IconResource( iconPainter = painterResource(id = R.drawable.ic_keyboard_24px), contentDescription = stringResource(id = R.string.enter_key_manually), testTag = "EnterSetupKeyButton", ), onEnterSetupKeyClick = onEnterSetupKeyClick, ) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( appTheme = state.appTheme, scrollBehavior = scrollBehavior, onAddCodeClick = remember(viewModel) { { launcher.launch(Manifest.permission.CAMERA) } }, onScanQuCodeClick = remember(viewModel) { { launcher.launch(Manifest.permission.CAMERA) } }, onEnterSetupKeyClick = remember(viewModel) { { viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick) } } )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( context, event.message(context.resources), Toast.LENGTH_LONG )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( currentState, scrollBehavior, onNavigateToSearch = remember(viewModel) { { viewModel.trySendAction( ItemListingAction.SearchClick ) } }, onScanQrCodeClick = remember(viewModel) { { launcher.launch(Manifest.permission.CAMERA) } }, onEnterSetupKeyClick = remember(viewModel) { { viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick) } }, onItemClick = remember(viewModel) { { viewModel.trySendAction( ItemListingAction.ItemClick(it) ) } }, onEditItemClick = remember(viewModel) { { viewModel.trySendAction( ItemListingAction.EditItemClick(it) ) } }, onDeleteItemClick = remember(viewModel) { { viewModel.trySendAction( ItemListingAction.DeleteItemClick(it) ) } } )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( id = when (appTheme) { AppTheme.DARK -&gt; R.drawable.ic_empty_vault_dark AppTheme.LIGHT -&gt; R.drawable.ic_empty_vault_light AppTheme.DEFAULT -&gt; R.drawable.ic_empty_vault } )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( label = R.string.enter_key_manually.asText(), icon = IconResource( iconPainter = painterResource(id = R.drawable.ic_keyboard_24px), contentDescription = stringResource(id = R.string.enter_key_manually), testTag = "EnterSetupKeyButton", ), onEnterSetupKeyClick = onEnterSetupKeyClick )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( label = R.string.scan_a_qr_code.asText(), icon = IconResource( iconPainter = painterResource(id = R.drawable.ic_camera), contentDescription = stringResource(id = R.string.scan_a_qr_code), testTag = "ScanQRCodeButton", ), onScanQrCodeClick = onScanQuCodeClick )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( rememberTopAppBarState() )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( title = stringResource(id = R.string.delete), message = dialog.message(), confirmButtonText = stringResource(id = R.string.ok), dismissButtonText = stringResource(id = R.string.cancel), onConfirmClick = { onConfirmDeleteClick(dialog.itemId) }, onDismissClick = onDismissRequest, onDismissRequest = onDismissRequest )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( title = stringResource(id = R.string.verification_codes), scrollBehavior = scrollBehavior, actions = { BitwardenSearchActionItem( contentDescription = stringResource(id = R.string.search_codes), onClick = onNavigateToSearch, ) } )</ID>
<ID>TrailingCommaOnCallSite:ItemListingScreen.kt$( title = stringResource(id = R.string.verification_codes), scrollBehavior = scrollBehavior, navigationIcon = null, actions = { } )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( ItemListingAction.Internal.DeleteItemReceive( authenticatorRepository.hardDeleteItem(action.itemId) ) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( ItemListingEvent.ShowToast( message = R.string.value_has_been_copied.asText(action.authCode) ) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( authenticatorData = action.data, clearDialogState = false )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( authenticatorData = authenticatorData.data, clearDialogState = false )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( authenticatorData = authenticatorData.data, clearDialogState = true )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( authenticatorData = state.data, clearDialogState = true )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( authenticatorRepository.hardDeleteItem(action.itemId) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( byteArray = otpParam.secret.toByteArray() )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( dialog = ItemListingState.DialogState.DeleteConfirmationPrompt( message = R.string.do_you_really_want_to_permanently_delete_cipher.asText(), itemId = action.itemId, ) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( dialog = ItemListingState.DialogState.Error( title = R.string.an_error_has_occurred.asText(), message = R.string.authenticator_key_read_error.asText(), ) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( dialog = ItemListingState.DialogState.Error( title = R.string.an_error_has_occurred.asText(), message = R.string.generic_error_message.asText(), ) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( initialState = ItemListingState( settingsRepository.appTheme, settingsRepository.authenticatorAlertThresholdSeconds, viewState = ItemListingState.ViewState.Loading, dialog = null ) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( message = R.string.internet_connection_required_title .asText() .concat(R.string.internet_connection_required_message.asText()) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( message = R.string.value_has_been_copied.asText(action.authCode) )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( settingsRepository.appTheme, settingsRepository.authenticatorAlertThresholdSeconds, viewState = ItemListingState.ViewState.Loading, dialog = null )</ID>
<ID>TrailingCommaOnCallSite:ItemListingViewModel.kt$ItemListingViewModel$( viewState = ItemListingState.ViewState.Loading )</ID>
<ID>TrailingCommaOnCallSite:ItemSearchNavigation.kt$( onNavigateBack = onNavigateBack )</ID>
<ID>TrailingCommaOnCallSite:ItemSearchScreen.kt$( viewState = viewState, modifier = innerModifier )</ID>
<ID>TrailingCommaOnCallSite:ItemSearchViewModel.kt$ItemSearchViewModel$( authenticatorData = authenticatorData, clearDialogState = false )</ID>
<ID>TrailingCommaOnCallSite:ItemSearchViewModel.kt$ItemSearchViewModel$( initialState = ItemSearchState( searchTerm = "", viewState = ItemSearchState.ViewState.Loading, dialogState = null, ) )</ID>
<ID>TrailingCommaOnCallSite:ItemSearchViewModel.kt$ItemSearchViewModel$( message = R.string.there_are_no_items_that_match_the_search.asText() )</ID>
<ID>TrailingCommaOnCallSite:ItemSearchViewModel.kt$ItemSearchViewModel$( searchTerm = currentState.searchTerm, viewState = authenticatorData .filterAndOrganize(state.searchTerm) .toViewState(searchTerm = state.searchTerm), dialogState = currentState.dialogState.takeUnless { clearDialogState } )</ID>
<ID>TrailingCommaOnCallSite:MainActivity.kt$MainActivity$( MainAction.ReceiveFirstIntent( intent = intent ) )</ID>
<ID>TrailingCommaOnCallSite:MainActivity.kt$MainActivity$( MainAction.ReceiveNewIntent(intent = intent) )</ID>
<ID>TrailingCommaOnCallSite:MainActivity.kt$MainActivity$( intent = intent )</ID>
<ID>TrailingCommaOnCallSite:MainActivity.kt$MainActivity$( onSplashScreenRemoved = { shouldShowSplashScreen = false }, onExitApplication = { finishAffinity() } )</ID>
<ID>TrailingCommaOnCallSite:MainActivity.kt$MainActivity$( theme = state.theme )</ID>
<ID>TrailingCommaOnCallSite:MainViewModel.kt$MainViewModel$( MainState( theme = settingsRepository.appTheme ) )</ID>
<ID>TrailingCommaOnCallSite:MainViewModel.kt$MainViewModel$( theme = settingsRepository.appTheme )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryScreen.kt$( dialog.message )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryScreen.kt$( title = dialog.title, message = dialog.message )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryScreen.kt$( visibilityState = BasicDialogState.Shown( title = dialog.title, message = dialog.message ), onDismissRequest = remember(state) { { viewModel.trySendAction(ManualCodeEntryAction.DismissDialog) } } )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryScreen.kt$( visibilityState = LoadingDialogState.Shown( dialog.message ) )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryViewModel.kt$ManualCodeEntryViewModel$( AuthenticatorItemEntity( id = UUID.randomUUID().toString(), key = state.code, issuer = state.issuer, accountName = "", userId = null, type = if (state.code.startsWith("steam://")) { AuthenticatorItemType.STEAM } else { AuthenticatorItemType.TOTP }, favorite = false, ) )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryViewModel.kt$ManualCodeEntryViewModel$( dialog = ManualCodeEntryState.DialogState.Error( message = R.string.key_is_invalid.asText() ) )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryViewModel.kt$ManualCodeEntryViewModel$( dialog = ManualCodeEntryState.DialogState.Error( message = R.string.key_is_required.asText() ) )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryViewModel.kt$ManualCodeEntryViewModel$( dialog = ManualCodeEntryState.DialogState.Error( message = R.string.name_is_required.asText() ) )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryViewModel.kt$ManualCodeEntryViewModel$( dialog = ManualCodeEntryState.DialogState.Error( title = R.string.an_error_has_occurred.asText(), message = R.string.generic_error_message.asText(), ) )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryViewModel.kt$ManualCodeEntryViewModel$( message = R.string.key_is_invalid.asText() )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryViewModel.kt$ManualCodeEntryViewModel$( message = R.string.key_is_required.asText() )</ID>
<ID>TrailingCommaOnCallSite:ManualCodeEntryViewModel.kt$ManualCodeEntryViewModel$( message = R.string.name_is_required.asText() )</ID>
<ID>TrailingCommaOnCallSite:QrCodeAnalyzerImpl.kt$QrCodeAnalyzerImpl$( DecodeHintType.POSSIBLE_FORMATS to arrayListOf(BarcodeFormat.QR_CODE), DecodeHintType.ALSO_INVERTED to true )</ID>
<ID>TrailingCommaOnCallSite:RootNavScreen.kt$( onUnlocked = { viewModel.trySendAction(RootNavAction.Internal.AppUnlocked) } )</ID>
<ID>TrailingCommaOnCallSite:RootNavViewModel.kt$RootNavViewModel$( initialState = RootNavState( hasSeenWelcomeGuide = settingsRepository.hasSeenWelcomeTutorial, navState = RootNavState.NavState.Splash, ) )</ID>
<ID>TrailingCommaOnCallSite:SearchHandlers.kt$SearchHandlers.Companion$( onBackClick = { viewModel.trySendAction(ItemSearchAction.BackClick) }, onDismissRequest = { viewModel.trySendAction(ItemSearchAction.DismissDialogClick) }, onItemClick = { viewModel.trySendAction(ItemSearchAction.ItemClick(it)) }, onSearchTermChange = { viewModel.trySendAction(ItemSearchAction.SearchTermChange(it)) } )</ID>
<ID>TrailingCommaOnCallSite:SettingsDiskSourceImpl.kt$SettingsDiskSourceImpl$( ALERT_THRESHOLD_SECONDS_KEY, thresholdSeconds )</ID>
<ID>TrailingCommaOnCallSite:SettingsNavigation.kt$( onNavigateBack = { navController.popBackStack() } )</ID>
<ID>TrailingCommaOnCallSite:SettingsNavigation.kt$( onTutorialFinished = { navController.popBackStack() } )</ID>
<ID>TrailingCommaOnCallSite:SettingsNavigation.kt$( route = SETTINGS_ROUTE )</ID>
<ID>TrailingCommaOnCallSite:SettingsNavigation.kt$( startDestination = SETTINGS_ROUTE, route = SETTINGS_GRAPH_ROUTE )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( SettingsAction.SecurityClick.UnlockWithBiometricToggle(it) )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( id = R.drawable.ic_navigate_next )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( modifier = Modifier .padding(innerPadding) .fillMaxSize() .verticalScroll(state = rememberScrollState()) )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( modifier = Modifier.padding(horizontal = 16.dp), label = stringResource(id = R.string.appearance) )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( modifier = Modifier.padding(horizontal = 16.dp), label = stringResource(id = R.string.data) )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( modifier = Modifier.padding(horizontal = 16.dp), label = stringResource(id = R.string.help) )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( modifier = Modifier.padding(horizontal = 16.dp), label = stringResource(id = R.string.security) )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( modifier = modifier, label = stringResource(id = R.string.about) )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( onExportClick = remember(viewModel) { { viewModel.trySendAction(SettingsAction.DataClick.ExportClick) } }, onImportClick = remember(viewModel) { { viewModel.trySendAction(SettingsAction.DataClick.ImportClick) } }, onBackupClick = remember(viewModel) { { viewModel.trySendAction(SettingsAction.DataClick.BackupClick) } } )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( state = state, biometricsManager = biometricsManager, onBiometricToggle = remember(viewModel) { { viewModel.trySendAction( SettingsAction.SecurityClick.UnlockWithBiometricToggle(it) ) } } )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( text = stringResource(id = R.string.export), onClick = onExportClick, modifier = modifier, withDivider = true, content = { Icon( modifier = Modifier .mirrorIfRtl() .size(24.dp), painter = painterResource(id = R.drawable.ic_navigate_next), contentDescription = null, tint = MaterialTheme.colorScheme.onSurface, ) } )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( text = stringResource(id = R.string.import_vault), onClick = onImportClick, modifier = modifier, withDivider = true, content = { Icon( modifier = Modifier .mirrorIfRtl() .size(24.dp), painter = painterResource(id = R.drawable.ic_navigate_next), contentDescription = null, tint = MaterialTheme.colorScheme.onSurface, ) } )</ID>
<ID>TrailingCommaOnCallSite:SettingsScreen.kt$( title = stringResource(id = R.string.settings), scrollBehavior = scrollBehavior )</ID>
<ID>TrailingCommaOnCallSite:SettingsViewModel.kt$SettingsViewModel$( appearance = it.appearance.copy(language = language) )</ID>
<ID>TrailingCommaOnCallSite:SettingsViewModel.kt$SettingsViewModel$( appearance = it.appearance.copy(theme = theme) )</ID>
<ID>TrailingCommaOnCallSite:SettingsViewModel.kt$SettingsViewModel$( clock, settingsRepository.appLanguage, settingsRepository.appTheme, settingsRepository.isUnlockWithBiometricsEnabled, settingsRepository.isCrashLoggingEnabled )</ID>
<ID>TrailingCommaOnCallSite:SettingsViewModel.kt$SettingsViewModel$( initialState = savedStateHandle[KEY_STATE] ?: createInitialState( clock, settingsRepository.appLanguage, settingsRepository.appTheme, settingsRepository.isUnlockWithBiometricsEnabled, settingsRepository.isCrashLoggingEnabled ) )</ID>
<ID>TrailingCommaOnCallSite:SettingsViewModel.kt$SettingsViewModel.Companion$( appearance = SettingsState.Appearance( language = appLanguage, theme = appTheme, ), isUnlockWithBiometricsEnabled = unlockWithBiometricsEnabled, isSubmitCrashLogsEnabled = isSubmitCrashLogsEnabled, dialog = null, version = R.string.version .asText() .concat(": ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})".asText()), copyrightInfo = copyrightInfo )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( R.string.scan_the_qr_code_in_your_2_step_verification_settings_for_any_account )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( R.string.use_your_device_camera_to_scan_codes )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( R.string.when_using_2_step_verification_youll_enter_your_username_and_password_and_a_code_generated_in_this_app )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( TutorialAction.ContinueClick )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( TutorialAction.TutorialPageChange(pagerState.targetPage) )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( modifier = Modifier .height(50.dp) .fillMaxWidth(), horizontalArrangement = Arrangement.Center )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( modifier = Modifier .padding(8.dp) .background(color, CircleShape) .size(10.dp) )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( modifier = Modifier .padding(innerPadding), verticalArrangement = Arrangement.Center )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( onTutorialFinished = {} )</ID>
<ID>TrailingCommaOnCallSite:TutorialScreen.kt$( painter = painterResource(id = R.drawable.ic_tutorial_2fa), contentDescription = stringResource(id = R.string.unique_codes) )</ID>
<ID>TrailingCommaOnCallSite:TutorialViewModel.kt$TutorialState.QrScannerSlide$( continueButtonText = R.string.continue_button.asText(), isLastPage = false )</ID>
<ID>TrailingCommaOnCallSite:TutorialViewModel.kt$TutorialState.UniqueCodesSlide$( continueButtonText = R.string.get_started.asText(), isLastPage = true )</ID>
<ID>TrailingCommaOnCallSite:TutorialViewModel.kt$TutorialViewModel$( initialState = TutorialState.IntroSlide )</ID>
<ID>TrailingCommaOnCallSite:TwoFasExportParser.kt$TwoFasExportParser$( items = exportData.services.toAuthenticatorItemEntities() )</ID>
<ID>TrailingCommaOnCallSite:UnlockScreen.kt$( modifier = Modifier .padding(innerPadding) .fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally )</ID>
<ID>TrailingCommaOnCallSite:UnlockScreen.kt$( title = R.string.an_error_has_occurred.asText(), message = dialog.message )</ID>
<ID>TrailingCommaOnCallSite:UnlockScreen.kt$( visibilityState = LoadingDialogState.Shown(R.string.loading.asText()) )</ID>
<ID>TrailingCommaOnCallSite:UnlockViewModel.kt$UnlockViewModel$( dialog = UnlockState.Dialog.Error( message = R.string.too_many_failed_biometric_attempts.asText(), ) )</ID>
<ID>TrailingCommaOnCallSite:UnlockViewModel.kt$UnlockViewModel$( initialState = savedStateHandle[KEY_STATE] ?: run { UnlockState( isBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled, isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(), dialog = null, ) } )</ID>
<ID>TrailingCommaOnCallSite:VaultVerificationCodeItem.kt$( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(color = MaterialTheme.colorScheme.primary), onClick = onItemClick, onLongClick = { shouldShowDropdownMenu = true } )</ID>
<ID>TrailingCommaOnCallSite:VaultVerificationCodeItem.kt$( painter = painterResource(id = R.drawable.ic_edit_item), contentDescription = stringResource(R.string.edit_item) )</ID>
<ID>TrailingCommaOnCallSite:VaultVerificationCodeItem.kt$( startIcon = IconData.Local(R.drawable.ic_login_item), issuer = "Sample Label", supportingLabel = "Supporting Label", authCode = "1234567890".chunked(3).joinToString(" "), timeLeftSeconds = 15, periodSeconds = 30, onCopyClick = {}, onItemClick = {}, modifier = Modifier.padding(horizontal = 16.dp), alertThresholdSeconds = 7 )</ID>
<ID>TrailingCommaOnCallSite:VaultVerificationCodeItem.kt$( text = { Text(text = stringResource(id = R.string.delete_item)) }, onClick = { shouldShowDropdownMenu = false onDeleteItemClick() }, leadingIcon = { Icon( painter = painterResource(id = R.drawable.ic_delete_item), contentDescription = stringResource(id = R.string.delete_item), ) } )</ID>
<ID>TrailingCommaOnCallSite:VaultVerificationCodeItem.kt$( text = { Text(text = stringResource(id = R.string.edit_item)) }, onClick = { shouldShowDropdownMenu = false onEditItemClick() }, leadingIcon = { Icon( painter = painterResource(id = R.drawable.ic_edit_item), contentDescription = stringResource(R.string.edit_item) ) } )</ID>
<ID>TrailingCommaOnCallSite:VaultVerificationCodeItem.kt$( timeLeftSeconds = timeLeftSeconds, periodSeconds = periodSeconds, alertThresholdSeconds = alertThresholdSeconds )</ID>
<ID>TrailingCommaOnCallSite:VaultVerificationCodeItem.kt$( vertical = 8.dp, horizontal = 16.dp )</ID>
<ID>TrailingCommaOnDeclarationSite:ItemSearchViewModel.kt$SortPriority</ID>
<ID>UndocumentedPublicClass:AegisExportParser.kt$AegisExportParser : ExportParser</ID>
<ID>UndocumentedPublicClass:AegisJsonExport.kt$AegisJsonExport</ID>
<ID>UndocumentedPublicClass:AegisJsonExport.kt$AegisJsonExport$Database</ID>
<ID>UndocumentedPublicClass:AegisJsonExport.kt$AegisJsonExport.Database$Entry</ID>
<ID>UndocumentedPublicClass:AegisJsonExport.kt$AegisJsonExport.Database$Group</ID>
<ID>UndocumentedPublicClass:AegisJsonExport.kt$AegisJsonExport.Database.Entry$Info</ID>
<ID>UndocumentedPublicClass:AuthRepositoryImpl.kt$AuthRepositoryImpl : AuthRepository</ID>
<ID>UndocumentedPublicClass:AuthenticatorApplication.kt$AuthenticatorApplication : Application</ID>
<ID>UndocumentedPublicClass:AuthenticatorDiskSourceImpl.kt$AuthenticatorDiskSourceImpl : AuthenticatorDiskSource</ID>
<ID>UndocumentedPublicClass:AuthenticatorRepositoryImpl.kt$AuthenticatorRepositoryImpl : AuthenticatorRepository</ID>
<ID>UndocumentedPublicClass:AuthenticatorSdkSourceImpl.kt$AuthenticatorSdkSourceImpl : AuthenticatorSdkSource</ID>
<ID>UndocumentedPublicClass:BitwardenExportParser.kt$BitwardenExportParser : ExportParser</ID>
<ID>UndocumentedPublicClass:EditItemViewModel.kt$EditItemAction$ExpandAdvancedOptionsClick : EditItemAction</ID>
<ID>UndocumentedPublicClass:ExpandableFloatingActionButton.kt$ExpandableFabIcon</ID>
<ID>UndocumentedPublicClass:ExpandableFloatingActionButton.kt$ExpandableFabOption</ID>
<ID>UndocumentedPublicClass:ExpandableFloatingActionButton.kt$ExpandableFabState</ID>
<ID>UndocumentedPublicClass:ExpandableFloatingActionButton.kt$ExpandableFabState$Collapsed : ExpandableFabState</ID>
<ID>UndocumentedPublicClass:ExpandableFloatingActionButton.kt$ExpandableFabState$Expanded : ExpandableFabState</ID>
<ID>UndocumentedPublicClass:ExportDataResult.kt$ExportDataResult$Error : ExportDataResult</ID>
<ID>UndocumentedPublicClass:ExportDataResult.kt$ExportDataResult$Success : ExportDataResult</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportAction</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportAction$CloseButtonClick : ExportAction</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportAction$ConfirmExportClick : ExportAction</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportAction$DialogDismiss : ExportAction</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportAction$ExportFormatOptionSelect : ExportAction</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportAction$ExportLocationReceive : ExportAction</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportAction$Internal : ExportAction</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportAction.Internal$SaveExportDataToUriResultReceive : Internal</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportEvent</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportEvent$NavigateBack : ExportEvent</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportEvent$NavigateToSelectExportDestination : ExportEvent</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportEvent$ShowToast : ExportEvent</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportState</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportState$DialogState</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportState.DialogState$Error : DialogState</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportState.DialogState$Loading : DialogState</ID>
<ID>UndocumentedPublicClass:ExportViewModel.kt$ExportViewModel : BaseViewModel</ID>
<ID>UndocumentedPublicClass:FileManagerImpl.kt$FileManagerImpl : FileManager</ID>
<ID>UndocumentedPublicClass:ImportDataResult.kt$ImportDataResult$Error : ImportDataResult</ID>
<ID>UndocumentedPublicClass:ImportDataResult.kt$ImportDataResult$Success : ImportDataResult</ID>
<ID>UndocumentedPublicClass:ItemListingExpandableFabAction.kt$ItemListingExpandableFabAction : ExpandableFabOption</ID>
<ID>UndocumentedPublicClass:ItemListingExpandableFabAction.kt$ItemListingExpandableFabAction$EnterSetupKey : ItemListingExpandableFabAction</ID>
<ID>UndocumentedPublicClass:ItemListingExpandableFabAction.kt$ItemListingExpandableFabAction$ScanQrCode : ItemListingExpandableFabAction</ID>
<ID>UndocumentedPublicClass:ItemListingViewModel.kt$ItemListingState.DialogState$DeleteConfirmationPrompt : DialogState</ID>
<ID>UndocumentedPublicClass:LastPassJsonExport.kt$LastPassJsonExport</ID>
<ID>UndocumentedPublicClass:LastPassJsonExport.kt$LastPassJsonExport$Account</ID>
<ID>UndocumentedPublicClass:LastPassJsonExport.kt$LastPassJsonExport$Folder</ID>
<ID>UndocumentedPublicClass:LastPassJsonExport.kt$LastPassJsonExport.Account$BackupInfo</ID>
<ID>UndocumentedPublicClass:LastPassJsonExport.kt$LastPassJsonExport.Account$FolderData</ID>
<ID>UndocumentedPublicClass:MainActivity.kt$MainActivity : AppCompatActivity</ID>
<ID>UndocumentedPublicClass:ManualCodeEntryViewModel.kt$ManualCodeEntryState$DialogState : Parcelable</ID>
<ID>UndocumentedPublicClass:ManualCodeEntryViewModel.kt$ManualCodeEntryState.DialogState$Error : DialogState</ID>
<ID>UndocumentedPublicClass:ManualCodeEntryViewModel.kt$ManualCodeEntryState.DialogState$Loading : DialogState</ID>
<ID>UndocumentedPublicClass:PlatformDiskModule.kt$PlatformDiskModule</ID>
<ID>UndocumentedPublicClass:PlatformNetworkModule.kt$PlatformNetworkModule</ID>
<ID>UndocumentedPublicClass:RootNavViewModel.kt$RootNavState$NavState : Parcelable</ID>
<ID>UndocumentedPublicClass:RootNavViewModel.kt$RootNavViewModel : BaseViewModel</ID>
<ID>UndocumentedPublicClass:SettingsViewModel.kt$SettingsAction.SecurityClick$UnlockWithBiometricToggle : SecurityClick</ID>
<ID>UndocumentedPublicClass:SettingsViewModel.kt$SettingsState$Dialog : Parcelable</ID>
<ID>UndocumentedPublicClass:SettingsViewModel.kt$SettingsState.Dialog$Loading : Dialog</ID>
<ID>UndocumentedPublicClass:TwoFasJsonExport.kt$TwoFasJsonExport</ID>
<ID>UndocumentedPublicClass:TwoFasJsonExport.kt$TwoFasJsonExport$Group</ID>
<ID>UndocumentedPublicClass:TwoFasJsonExport.kt$TwoFasJsonExport$Service</ID>
<ID>UndocumentedPublicClass:TwoFasJsonExport.kt$TwoFasJsonExport.Service$Badge</ID>
<ID>UndocumentedPublicClass:TwoFasJsonExport.kt$TwoFasJsonExport.Service$Icon</ID>
<ID>UndocumentedPublicClass:TwoFasJsonExport.kt$TwoFasJsonExport.Service$Order</ID>
<ID>UndocumentedPublicClass:TwoFasJsonExport.kt$TwoFasJsonExport.Service$Otp</ID>
<ID>UndocumentedPublicClass:TwoFasJsonExport.kt$TwoFasJsonExport.Service.Icon$IconCollection</ID>
<ID>UndocumentedPublicClass:TwoFasJsonExport.kt$TwoFasJsonExport.Service.Icon$Label</ID>
<ID>UndocumentedPublicFunction:AuthenticatorItemAlgorithm.kt$AuthenticatorItemAlgorithm.Companion$fun fromStringOrNull(value: String): AuthenticatorItemAlgorithm?</ID>
<ID>UndocumentedPublicFunction:AuthenticatorItemEntity.kt$AuthenticatorItemEntity$fun toOtpAuthUriString(): String</ID>
<ID>UndocumentedPublicFunction:AuthenticatorItemType.kt$AuthenticatorItemType.Companion$fun fromStringOrNull(value: String): AuthenticatorItemType?</ID>
<ID>UndocumentedPublicFunction:AuthenticatorSdkSource.kt$AuthenticatorSdkSource$suspend fun generateBiometricsKey(): Result&lt;String&gt;</ID>
<ID>UndocumentedPublicFunction:AuthenticatorTheme.kt$@Composable fun AuthenticatorTheme( theme: AppTheme = AppTheme.DEFAULT, dynamicColor: Boolean = false, content: @Composable () -&gt; Unit, )</ID>
<ID>UndocumentedPublicFunction:EditItemScreen.kt$@Composable fun EditItemContent( modifier: Modifier = Modifier, viewState: EditItemState.ViewState.Content, onIssuerNameTextChange: (String) -&gt; Unit = {}, onUsernameTextChange: (String) -&gt; Unit = {}, onToggleFavorite: (Boolean) -&gt; Unit = {}, onTypeOptionClicked: (AuthenticatorItemType) -&gt; Unit = {}, onTotpCodeTextChange: (String) -&gt; Unit = {}, onAlgorithmOptionClicked: (AuthenticatorItemAlgorithm) -&gt; Unit = {}, onRefreshPeriodOptionClicked: (AuthenticatorRefreshPeriodOption) -&gt; Unit = {}, onNumberOfDigitsChanged: (Int) -&gt; Unit = {}, onExpandAdvancedOptionsClicked: () -&gt; Unit = {}, )</ID>
<ID>UndocumentedPublicFunction:EditItemViewModel.kt$AuthenticatorRefreshPeriodOption.Companion$fun fromSeconds(seconds: Int)</ID>
<ID>UndocumentedPublicFunction:ExpandableFloatingActionButton.kt$@Composable fun &lt;T : ExpandableFabOption&gt; ExpandableFloatingActionButton( modifier: Modifier = Modifier, label: Text?, items: List&lt;T&gt;, expandableFabState: MutableState&lt;ExpandableFabState&gt; = rememberExpandableFabState(), expandableFabIcon: ExpandableFabIcon, onStateChange: (expandableFabState: ExpandableFabState) -&gt; Unit = { }, )</ID>
<ID>UndocumentedPublicFunction:ExpandableFloatingActionButton.kt$@Composable fun rememberExpandableFabState()</ID>
<ID>UndocumentedPublicFunction:ExpandableFloatingActionButton.kt$ExpandableFabState$fun isExpanded()</ID>
<ID>UndocumentedPublicFunction:ExpandableFloatingActionButton.kt$ExpandableFabState$fun toggleValue()</ID>
<ID>UndocumentedPublicFunction:ExportNavigation.kt$fun NavController.navigateToExport(navOptions: NavOptions? = null)</ID>
<ID>UndocumentedPublicFunction:ExportNavigation.kt$fun NavGraphBuilder.exportDestination( onNavigateBack: () -&gt; Unit, )</ID>
<ID>UndocumentedPublicFunction:ExportScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun ExportScreen( viewModel: ExportViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, onNavigateBack: () -&gt; Unit, )</ID>
<ID>UndocumentedPublicFunction:ImportingScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun ImportingScreen( viewModel: ImportingViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, onNavigateBack: () -&gt; Unit, )</ID>
<ID>UndocumentedPublicFunction:ItemListingScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable @Preview(showBackground = true) fun EmptyListingContentPreview()</ID>
<ID>UndocumentedPublicFunction:SearchHandlers.kt$SearchHandlers.Companion$fun create(viewModel: ItemSearchViewModel): SearchHandlers</ID>
<ID>UndocumentedPublicFunction:SettingsScreen.kt$@Composable fun SecuritySettings( state: SettingsState, biometricsManager: BiometricsManager = LocalBiometricsManager.current, onBiometricToggle: (Boolean) -&gt; Unit, )</ID>
<ID>UndocumentedPublicFunction:SettingsScreen.kt$@Composable fun VaultSettings( modifier: Modifier = Modifier, onExportClick: () -&gt; Unit, onImportClick: () -&gt; Unit, onBackupClick: () -&gt; Unit, )</ID>
<ID>UndocumentedPublicFunction:SettingsViewModel.kt$SettingsViewModel.Companion$fun createInitialState( clock: Clock, appLanguage: AppLanguage, appTheme: AppTheme, unlockWithBiometricsEnabled: Boolean, isSubmitCrashLogsEnabled: Boolean, ): SettingsState</ID>
<ID>UndocumentedPublicFunction:TutorialNavigation.kt$fun NavController.navigateToSettingsTutorial(navOptions: NavOptions? = null)</ID>
<ID>UndocumentedPublicFunction:TutorialNavigation.kt$fun NavController.navigateToTutorial(navOptions: NavOptions? = null)</ID>
<ID>UndocumentedPublicFunction:TutorialNavigation.kt$fun NavGraphBuilder.tutorialDestination(onTutorialFinished: () -&gt; Unit)</ID>
<ID>UndocumentedPublicFunction:TutorialNavigation.kt$fun NavGraphBuilder.tutorialSettingsDestination(onTutorialFinished: () -&gt; Unit)</ID>
<ID>UndocumentedPublicFunction:TutorialScreen.kt$@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun TutorialScreen( viewModel: TutorialViewModel = hiltViewModel(), onTutorialFinished: () -&gt; Unit, )</ID>
<ID>UndocumentedPublicFunction:TutorialScreen.kt$@Preview @Composable fun TutorialScreenPreview()</ID>
<ID>UndocumentedPublicFunction:UnlockScreen.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable fun UnlockScreen( viewModel: UnlockViewModel = hiltViewModel(), biometricsManager: BiometricsManager = LocalBiometricsManager.current, onUnlocked: () -&gt; Unit, )</ID>
<ID>UndocumentedPublicFunction:VerificationCodeItemExtensions.kt$fun List&lt;VerificationCodeItem&gt;.toViewState( alertThresholdSeconds: Int, ): ItemListingState.ViewState</ID>
<ID>UndocumentedPublicFunction:VerificationCodeItemExtensions.kt$fun VerificationCodeItem.toDisplayItem(alertThresholdSeconds: Int)</ID>
</CurrentIssues>
</SmellBaseline>