mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 11:02:51 -05:00
Merge branch 'main' into sdlc/sdk-update
This commit is contained in:
59 files changed
+301
-610
No files matched your search
@@ -153,8 +153,8 @@ The following is a list of all third-party dependencies included as part of the
|
||||
- Purpose: Provides a consistent API for navigating between Android components.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **AndroidX Room**
|
||||
- https://developer.android.com/jetpack/androidx/releases/room
|
||||
- **AndroidX Room3**
|
||||
- https://developer.android.com/jetpack/androidx/releases/room3
|
||||
- Purpose: A convenient SQLite-based persistence layer for Android.
|
||||
- License: Apache 2.0
|
||||
|
||||
@@ -168,16 +168,16 @@ The following is a list of all third-party dependencies included as part of the
|
||||
- Purpose: The WorkManager is used to schedule deferrable, asynchronous tasks that must be run reliably.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Coil**
|
||||
- https://github.com/coil-kt/coil
|
||||
- Purpose: Image loading and caching.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Dagger Hilt**
|
||||
- https://github.com/google/dagger
|
||||
- Purpose: Dependency injection framework.
|
||||
- License: Apache 2.0
|
||||
|
||||
- **Glide**
|
||||
- https://github.com/bumptech/glide
|
||||
- Purpose: Image loading and caching.
|
||||
- License: BSD, part MIT and Apache 2.0
|
||||
|
||||
- **kotlinx.collections.immutable**
|
||||
- https://github.com/Kotlin/kotlinx.collections.immutable
|
||||
- Purpose: Immutable collection interfaces and implementation prototypes for Kotlin.
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.androidx.room)
|
||||
alias(libs.plugins.androidx.room3)
|
||||
// Crashlytics is enabled for all builds initially but removed for FDroid builds in gradle and
|
||||
// standardDebug builds in the merged manifest.
|
||||
alias(libs.plugins.crashlytics)
|
||||
@@ -49,7 +49,7 @@ base {
|
||||
archivesName.set("com.x8bit.bitwarden")
|
||||
}
|
||||
|
||||
room {
|
||||
room3 {
|
||||
schemaDirectory("$projectDir/schemas")
|
||||
}
|
||||
|
||||
@@ -268,16 +268,14 @@ dependencies {
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.androidx.room.runtime)
|
||||
ksp(libs.androidx.room3.compiler)
|
||||
implementation(libs.androidx.room3.runtime)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(libs.androidx.splashscreen)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.bitwarden.sdk)
|
||||
implementation(libs.bumptech.glide)
|
||||
implementation(libs.bumptech.glide.okhttp)
|
||||
ksp(libs.bumptech.glide.compiler)
|
||||
implementation(libs.coil.coil)
|
||||
implementation(libs.coil.coil.network.okhttp)
|
||||
implementation(libs.google.hilt.android)
|
||||
ksp(libs.google.hilt.compiler)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
Vendored
-7
@@ -55,13 +55,6 @@
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Glide
|
||||
################################################################################
|
||||
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
|
||||
################################################################################
|
||||
# JNA
|
||||
################################################################################
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.app.Application
|
||||
import coil3.ImageLoader
|
||||
import coil3.PlatformContext
|
||||
import coil3.SingletonImageLoader
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.data.manager.flightrecorder.FlightRecorderManager
|
||||
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
|
||||
@@ -11,6 +14,7 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConfigManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.restriction.RestrictionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import dagger.Lazy
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -20,7 +24,7 @@ import javax.inject.Inject
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@HiltAndroidApp
|
||||
class BitwardenApplication : Application() {
|
||||
class BitwardenApplication : Application(), SingletonImageLoader.Factory {
|
||||
// Inject classes here that must be triggered on startup but are not otherwise consumed by
|
||||
// other callers.
|
||||
@Inject
|
||||
@@ -50,6 +54,9 @@ class BitwardenApplication : Application() {
|
||||
@Inject
|
||||
lateinit var environmentRepository: EnvironmentRepository
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: Lazy<ImageLoader>
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// These must be initialized in order to ensure that the restrictionManager does not
|
||||
@@ -62,4 +69,6 @@ class BitwardenApplication : Application() {
|
||||
super.onLowMemory()
|
||||
Timber.w("onLowMemory")
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: PlatformContext): ImageLoader = imageLoader.get()
|
||||
}
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.credentials.datasource.disk.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.OnConflictStrategy
|
||||
import androidx.room3.Query
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.entity.PrivilegedAppEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.credentials.datasource.disk.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room3.Database
|
||||
import androidx.room3.RoomDatabase
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.dao.PrivilegedAppDao
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.entity.PrivilegedAppEntity
|
||||
|
||||
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.credentials.datasource.disk.di
|
||||
|
||||
import android.app.Application
|
||||
import androidx.room.Room
|
||||
import androidx.room3.Room
|
||||
import androidx.sqlite.driver.AndroidSQLiteDriver
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.PrivilegedAppDiskSource
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.PrivilegedAppDiskSourceImpl
|
||||
import com.x8bit.bitwarden.data.credentials.datasource.disk.dao.PrivilegedAppDao
|
||||
@@ -25,12 +26,12 @@ object CredentialsDiskModule {
|
||||
app: Application,
|
||||
): PrivilegedAppDatabase =
|
||||
Room
|
||||
.databaseBuilder(
|
||||
.databaseBuilder<PrivilegedAppDatabase>(
|
||||
context = app,
|
||||
klass = PrivilegedAppDatabase::class.java,
|
||||
name = "privileged_apps_database",
|
||||
)
|
||||
.fallbackToDestructiveMigration(dropAllTables = true)
|
||||
.setDriver(driver = AndroidSQLiteDriver())
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.credentials.datasource.disk.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room3.ColumnInfo
|
||||
import androidx.room3.Entity
|
||||
|
||||
/**
|
||||
* Entity representing a trusted privileged app in the database.
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.OnConflictStrategy
|
||||
import androidx.room3.Query
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.entity.OrganizationEventEntity
|
||||
|
||||
/**
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.database
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room3.AutoMigration
|
||||
import androidx.room3.ColumnTypeConverters
|
||||
import androidx.room3.Database
|
||||
import androidx.room3.RoomDatabase
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.dao.OrganizationEventDao
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.entity.OrganizationEventEntity
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.InstantTypeConverter
|
||||
@@ -21,7 +21,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.InstantTypeConve
|
||||
AutoMigration(from = 1, to = 2),
|
||||
],
|
||||
)
|
||||
@TypeConverters(InstantTypeConverter::class)
|
||||
@ColumnTypeConverters(InstantTypeConverter::class)
|
||||
abstract class PlatformDatabase : RoomDatabase() {
|
||||
/**
|
||||
* Provides the DAO for accessing organization event data.
|
||||
|
||||
+5
-7
@@ -3,7 +3,8 @@ package com.x8bit.bitwarden.data.platform.datasource.disk.di
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.room.Room
|
||||
import androidx.room3.Room
|
||||
import androidx.sqlite.driver.AndroidSQLiteDriver
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.bitwarden.data.datasource.disk.FlightRecorderDiskSource
|
||||
import com.bitwarden.data.datasource.disk.di.EncryptedPreferences
|
||||
@@ -69,14 +70,11 @@ object PlatformDiskModule {
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
): PlatformDatabase =
|
||||
Room
|
||||
.databaseBuilder(
|
||||
context = app,
|
||||
klass = PlatformDatabase::class.java,
|
||||
name = "platform_database",
|
||||
)
|
||||
.databaseBuilder<PlatformDatabase>(context = app, name = "platform_database")
|
||||
.fallbackToDestructiveMigration(dropAllTables = false)
|
||||
.addTypeConverter(InstantTypeConverter())
|
||||
.addColumnTypeConverter(typeConverter = InstantTypeConverter())
|
||||
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
|
||||
.setDriver(driver = AndroidSQLiteDriver())
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.disk.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room3.ColumnInfo
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.PrimaryKey
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.Query
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room3.Database
|
||||
import androidx.room3.RoomDatabase
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.dao.PasswordHistoryDao
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity.PasswordHistoryEntity
|
||||
|
||||
|
||||
+6
-6
@@ -2,7 +2,8 @@ package com.x8bit.bitwarden.data.tools.generator.datasource.disk.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.room.Room
|
||||
import androidx.room3.Room
|
||||
import androidx.sqlite.driver.AndroidSQLiteDriver
|
||||
import com.bitwarden.data.datasource.disk.di.UnencryptedPreferences
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.tools.generator.datasource.disk.GeneratorDiskSource
|
||||
@@ -50,16 +51,15 @@ object GeneratorDiskModule {
|
||||
fun providePasswordHistoryDatabase(
|
||||
app: Application,
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
): PasswordHistoryDatabase {
|
||||
return Room
|
||||
.databaseBuilder(
|
||||
): PasswordHistoryDatabase =
|
||||
Room
|
||||
.databaseBuilder<PasswordHistoryDatabase>(
|
||||
context = app,
|
||||
klass = PasswordHistoryDatabase::class.java,
|
||||
name = "passcode_history_database",
|
||||
)
|
||||
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
|
||||
.setDriver(driver = AndroidSQLiteDriver())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.tools.generator.datasource.disk.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room3.ColumnInfo
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.PrimaryKey
|
||||
import com.bitwarden.vault.PasswordHistory
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.callback
|
||||
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room3.RoomDatabase
|
||||
import androidx.sqlite.SQLiteConnection
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
class DatabaseSchemeCallback(
|
||||
private val databaseSchemeManager: DatabaseSchemeManager,
|
||||
) : RoomDatabase.Callback() {
|
||||
override fun onDestructiveMigration(connection: SQLiteConnection) {
|
||||
override suspend fun onDestructiveMigration(connection: SQLiteConnection) {
|
||||
super.onDestructiveMigration(connection)
|
||||
databaseSchemeManager.clearSyncState()
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,26 +1,26 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.convertor
|
||||
|
||||
import androidx.room.ProvidedTypeConverter
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room3.ColumnTypeConverter
|
||||
import androidx.room3.ProvidedColumnTypeConverter
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* A [TypeConverter] to convert an [Instant] to and from a [Long].
|
||||
* A [ColumnTypeConverter] to convert an [Instant] to and from a [Long].
|
||||
*/
|
||||
@ProvidedTypeConverter
|
||||
@ProvidedColumnTypeConverter
|
||||
class InstantTypeConverter {
|
||||
/**
|
||||
* A [TypeConverter] to convert a [Long] to an [Instant].
|
||||
* A [ColumnTypeConverter] to convert a [Long] to an [Instant].
|
||||
*/
|
||||
@TypeConverter
|
||||
@ColumnTypeConverter
|
||||
fun fromTimestamp(
|
||||
value: Long?,
|
||||
): Instant? = value?.let { Instant.ofEpochSecond(it) }
|
||||
|
||||
/**
|
||||
* A [TypeConverter] to convert an [Instant] to a [Long].
|
||||
* A [ColumnTypeConverter] to convert an [Instant] to a [Long].
|
||||
*/
|
||||
@TypeConverter
|
||||
@ColumnTypeConverter
|
||||
fun toTimestamp(
|
||||
instant: Instant?,
|
||||
): Long? = instant?.epochSecond
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.OnConflictStrategy
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Transaction
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CipherEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.OnConflictStrategy
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Transaction
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.CollectionEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.OnConflictStrategy
|
||||
import androidx.room3.Query
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.DomainsEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.OnConflictStrategy
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Transaction
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.FolderEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@@ -39,7 +39,7 @@ interface FoldersDao {
|
||||
* Retrieves all folders from the database for a given [userId].
|
||||
*/
|
||||
@Query("SELECT * FROM folders WHERE user_id = :userId")
|
||||
fun getAllFolders(
|
||||
suspend fun getAllFolders(
|
||||
userId: String,
|
||||
): List<FolderEntity>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.OnConflictStrategy
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Transaction
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
+7
-7
@@ -1,11 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.database
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import androidx.room3.AutoMigration
|
||||
import androidx.room3.ColumnTypeConverters
|
||||
import androidx.room3.Database
|
||||
import androidx.room3.DeleteColumn
|
||||
import androidx.room3.RoomDatabase
|
||||
import androidx.room3.migration.AutoMigrationSpec
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.convertor.InstantTypeConverter
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CiphersDao
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.dao.CollectionsDao
|
||||
@@ -38,7 +38,7 @@ import com.x8bit.bitwarden.data.vault.datasource.disk.entity.SendEntity
|
||||
AutoMigration(from = 9, to = 10, RemoveTotpAutoMigration::class),
|
||||
],
|
||||
)
|
||||
@TypeConverters(InstantTypeConverter::class)
|
||||
@ColumnTypeConverters(InstantTypeConverter::class)
|
||||
abstract class VaultDatabase : RoomDatabase() {
|
||||
|
||||
/**
|
||||
|
||||
+5
-7
@@ -1,7 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.di
|
||||
|
||||
import android.app.Application
|
||||
import androidx.room.Room
|
||||
import androidx.room3.Room
|
||||
import androidx.sqlite.driver.AndroidSQLiteDriver
|
||||
import com.bitwarden.core.data.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
|
||||
@@ -35,14 +36,11 @@ class VaultDiskModule {
|
||||
databaseSchemeManager: DatabaseSchemeManager,
|
||||
): VaultDatabase =
|
||||
Room
|
||||
.databaseBuilder(
|
||||
context = app,
|
||||
klass = VaultDatabase::class.java,
|
||||
name = "vault_database",
|
||||
)
|
||||
.databaseBuilder<VaultDatabase>(context = app, name = "vault_database")
|
||||
.fallbackToDestructiveMigration(dropAllTables = false)
|
||||
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
|
||||
.addTypeConverter(InstantTypeConverter())
|
||||
.addColumnTypeConverter(typeConverter = InstantTypeConverter())
|
||||
.setDriver(driver = AndroidSQLiteDriver())
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room3.ColumnInfo
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.Index
|
||||
import androidx.room3.PrimaryKey
|
||||
|
||||
/**
|
||||
* Entity representing a cipher in the database.
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room3.ColumnInfo
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.PrimaryKey
|
||||
|
||||
/**
|
||||
* Entity representing a collection in the database.
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room3.ColumnInfo
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.PrimaryKey
|
||||
|
||||
/**
|
||||
* Entity representing a set of domains in the database.
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room3.ColumnInfo
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.PrimaryKey
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.disk.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room3.ColumnInfo
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.PrimaryKey
|
||||
|
||||
/**
|
||||
* Entity representing a send in the database.
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.credentials.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bumptech.glide.Glide
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
/**
|
||||
* Creates an IconCompat from an IconData, or falls back to a default resource if IconData is null
|
||||
* or is not of type Network.
|
||||
*
|
||||
* @param iconData The IconData to create the IconCompat from.
|
||||
* @param defaultResourceId The resource ID of the default icon to use if IconData is null or not of
|
||||
* type Network.
|
||||
* @return An IconCompat created from the IconData or the default resource.
|
||||
*/
|
||||
suspend fun Context.createFido2IconCompatFromIconDataOrDefault(
|
||||
iconData: IconData?,
|
||||
@DrawableRes defaultResourceId: Int,
|
||||
): IconCompat = if (iconData != null && iconData is IconData.Network) {
|
||||
createFido2IconCompatFromRemoteUriOrDefaultResource(
|
||||
uri = iconData.uri,
|
||||
defaultResourceId = defaultResourceId,
|
||||
)
|
||||
} else {
|
||||
createFido2IconCompatFromResource(defaultResourceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an IconCompat from a drawable resource ID.
|
||||
*/
|
||||
fun Context.createFido2IconCompatFromResource(@DrawableRes resourceId: Int) =
|
||||
IconCompat.createWithResource(this, resourceId)
|
||||
|
||||
// futureTargetBitmap.get() is a blocking call so this function must be called from a coroutine.
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
private suspend fun Context.createFido2IconCompatFromRemoteUriOrDefaultResource(
|
||||
uri: String,
|
||||
@DrawableRes defaultResourceId: Int,
|
||||
): IconCompat {
|
||||
val futureTargetBitmap = Glide
|
||||
.with(this)
|
||||
.asBitmap()
|
||||
.load(uri)
|
||||
.placeholder(defaultResourceId)
|
||||
.submit()
|
||||
return try {
|
||||
IconCompat.createWithBitmap(futureTargetBitmap.get())
|
||||
} catch (e: CancellationException) {
|
||||
Timber.e(e, "Cancellation exception while loading icon.")
|
||||
IconCompat.createWithResource(this, defaultResourceId)
|
||||
} catch (e: ExecutionException) {
|
||||
Timber.e(e, "Execution exception while loading icon.")
|
||||
IconCompat.createWithResource(this, defaultResourceId)
|
||||
} catch (e: InterruptedException) {
|
||||
Timber.e(e, "Interrupted while loading icon.")
|
||||
IconCompat.createWithResource(this, defaultResourceId)
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.glide
|
||||
|
||||
import android.content.Context
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.network.ssl.createMtlsOkHttpClient
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.x8bit.bitwarden.data.platform.manager.CertificateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkCookieManager
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Custom Glide module for the Bitwarden app that configures Glide to use an OkHttpClient
|
||||
* with mTLS (mutual TLS) support.
|
||||
*
|
||||
* This ensures that all icon/image loading requests through Glide present the client certificate
|
||||
* for mutual TLS authentication, allowing them to pass through Cloudflare's mTLS checks.
|
||||
*
|
||||
* The configuration mirrors the SSL setup used in RetrofitsImpl for API calls.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@GlideModule
|
||||
class BitwardenAppGlideModule : AppGlideModule() {
|
||||
|
||||
/**
|
||||
* Entry point to access Hilt-provided dependencies from non-Hilt managed classes.
|
||||
*/
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface BitwardenGlideEntryPoint {
|
||||
/**
|
||||
* Provides access to the [CertificateManager] for mTLS certificate management.
|
||||
*/
|
||||
fun certificateManager(): CertificateManager
|
||||
|
||||
/**
|
||||
* Provides access to the [NetworkCookieManager] for cookie-based authentication.
|
||||
*/
|
||||
fun networkCookieManager(): NetworkCookieManager
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
// Get CertificateManager from Hilt
|
||||
val entryPoint = EntryPointAccessors.fromApplication(
|
||||
context = context.applicationContext,
|
||||
entryPoint = BitwardenGlideEntryPoint::class.java,
|
||||
)
|
||||
val certificateManager = entryPoint.certificateManager()
|
||||
val networkCookieManager = entryPoint.networkCookieManager()
|
||||
|
||||
// Build OkHttpClient with mTLS and cookie support
|
||||
val client = certificateManager
|
||||
.createMtlsOkHttpClient()
|
||||
.newBuilder()
|
||||
.addNetworkInterceptor(GlideCookieInterceptor(networkCookieManager))
|
||||
.build()
|
||||
|
||||
// Register OkHttpUrlLoader that uses our mTLS + cookie OkHttpClient
|
||||
registry.replace(
|
||||
GlideUrl::class.java,
|
||||
InputStream::class.java,
|
||||
OkHttpUrlLoader.Factory(client),
|
||||
)
|
||||
}
|
||||
|
||||
override fun isManifestParsingEnabled(): Boolean = false
|
||||
}
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.ui.platform.glide
|
||||
package com.x8bit.bitwarden.ui.platform.image
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import com.bitwarden.network.provider.CookieProvider
|
||||
@@ -9,16 +9,16 @@ private const val HEADER_COOKIE = "Cookie"
|
||||
private const val HTTP_302 = 302
|
||||
|
||||
/**
|
||||
* Interceptor that attaches cookies to Glide image requests for enterprise environments
|
||||
* requiring cookie-based authentication.
|
||||
* Interceptor that attaches cookies to image requests for enterprise environments requiring
|
||||
* cookie-based authentication.
|
||||
*
|
||||
* Unlike [com.bitwarden.network.interceptor.CookieInterceptor], this interceptor does not
|
||||
* trigger cookie acquisition. It throws [CookieRedirectException] on HTTP 302 responses
|
||||
* to prevent Glide from following redirects and caching invalid content.
|
||||
* to prevent following redirects and caching invalid content.
|
||||
*
|
||||
* @property cookieProvider Provider for retrieving cookies by hostname.
|
||||
*/
|
||||
class GlideCookieInterceptor(
|
||||
class ImageCookieInterceptor(
|
||||
private val cookieProvider: CookieProvider,
|
||||
) : Interceptor {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.x8bit.bitwarden.ui.platform.image.di
|
||||
|
||||
import android.content.Context
|
||||
import coil3.ImageLoader
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import com.bitwarden.network.ssl.createMtlsOkHttpClient
|
||||
import com.x8bit.bitwarden.data.platform.manager.CertificateManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkCookieManager
|
||||
import com.x8bit.bitwarden.ui.platform.image.ImageCookieInterceptor
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides dependencies for loading images.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ImageModule {
|
||||
/**
|
||||
* Provides the [ImageLoader] used by Coil, configured to use an OkHttpClient with (mutual TLS)
|
||||
* and cookie support.
|
||||
*
|
||||
* This ensures that all icon/image loading requests through Coil present the client
|
||||
* certificate for mutual TLS authentication, allowing them to pass through Cloudflare's mTLS
|
||||
* checks.
|
||||
*
|
||||
* The configuration mirrors the SSL setup used for non-image API calls.
|
||||
*/
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideImageLoader(
|
||||
@ApplicationContext context: Context,
|
||||
certificateManager: CertificateManager,
|
||||
networkCookieManager: NetworkCookieManager,
|
||||
): ImageLoader = ImageLoader
|
||||
.Builder(context = context)
|
||||
.components {
|
||||
this.add(
|
||||
OkHttpNetworkFetcherFactory(
|
||||
callFactory = {
|
||||
certificateManager
|
||||
.createMtlsOkHttpClient()
|
||||
.newBuilder()
|
||||
.addNetworkInterceptor(ImageCookieInterceptor(networkCookieManager))
|
||||
.build()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
+2
-1
@@ -7,6 +7,7 @@ import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DatabaseSchemeCallbackTest {
|
||||
@@ -17,7 +18,7 @@ class DatabaseSchemeCallbackTest {
|
||||
private val callback = DatabaseSchemeCallback(databaseSchemeManager)
|
||||
|
||||
@Test
|
||||
fun `onDestructiveMigration calls clearSyncState`() {
|
||||
fun `onDestructiveMigration calls clearSyncState`() = runTest {
|
||||
callback.onDestructiveMigration(mockk<SQLiteConnection>())
|
||||
verify(exactly = 1) { databaseSchemeManager.clearSyncState() }
|
||||
}
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ class FakeFoldersDao : FoldersDao {
|
||||
folderId: String,
|
||||
): FolderEntity? = storedFolders.find { it.userId == userId && it.id == folderId }
|
||||
|
||||
override fun getAllFolders(
|
||||
override suspend fun getAllFolders(
|
||||
userId: String,
|
||||
): List<FolderEntity> = storedFolders.filter { it.userId == userId }
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.credentials.util
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Icon
|
||||
import android.net.Uri
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bumptech.glide.Glide
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
class ContextExtensionsTest {
|
||||
|
||||
val mockContext = mockk<Context>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkStatic(
|
||||
Glide::class,
|
||||
Icon::class,
|
||||
IconCompat::class,
|
||||
)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(
|
||||
Glide::class,
|
||||
Icon::class,
|
||||
IconCompat::class,
|
||||
Uri::class,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when IconData is null`() =
|
||||
runTest {
|
||||
every { IconCompat.createWithResource(mockContext, 0) } returns IconCompat()
|
||||
mockContext.createFido2IconCompatFromIconDataOrDefault(
|
||||
iconData = null,
|
||||
defaultResourceId = 0,
|
||||
)
|
||||
verify { IconCompat.createWithResource(mockContext, 0) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when IconData is not Network type`() =
|
||||
runTest {
|
||||
every { IconCompat.createWithResource(mockContext, 0) } returns IconCompat()
|
||||
mockContext.createFido2IconCompatFromIconDataOrDefault(
|
||||
iconData = IconData.Local(0),
|
||||
defaultResourceId = 0,
|
||||
)
|
||||
verify { IconCompat.createWithResource(mockContext, 0) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createFido2IconCompatFromIconDataOrDefault should load icon from URI when IconData is Network type`() =
|
||||
runTest {
|
||||
val mockBitmap = mockk<Bitmap>()
|
||||
setupMockGlide(mockBitmap)
|
||||
every { IconCompat.createWithBitmap(mockBitmap) } returns mockk()
|
||||
mockContext.createFido2IconCompatFromIconDataOrDefault(
|
||||
iconData = IconData.Network("https://www.mockuri.com", 0),
|
||||
defaultResourceId = 0,
|
||||
)
|
||||
verify { IconCompat.createWithBitmap(mockBitmap) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when loading from URI throws CancellationException`() =
|
||||
runTest {
|
||||
val mockBitmap = mockk<Bitmap>()
|
||||
setupMockGlide(mockBitmap)
|
||||
every { IconCompat.createWithResource(mockContext, 0) } returns mockk()
|
||||
every { IconCompat.createWithBitmap(mockBitmap) } throws CancellationException()
|
||||
mockContext.createFido2IconCompatFromIconDataOrDefault(
|
||||
iconData = IconData.Network("https://www.mockuri.com", 0),
|
||||
defaultResourceId = 0,
|
||||
)
|
||||
verify { IconCompat.createWithResource(mockContext, 0) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when loading from URI throws ExecutionException`() =
|
||||
runTest {
|
||||
val mockBitmap = mockk<Bitmap>()
|
||||
setupMockGlide(mockBitmap)
|
||||
every { IconCompat.createWithResource(mockContext, 0) } returns mockk()
|
||||
every {
|
||||
IconCompat.createWithBitmap(mockBitmap)
|
||||
} throws ExecutionException("message", Throwable())
|
||||
|
||||
mockContext.createFido2IconCompatFromIconDataOrDefault(
|
||||
iconData = IconData.Network("https://www.mockuri.com", 0),
|
||||
defaultResourceId = 0,
|
||||
)
|
||||
verify { IconCompat.createWithResource(mockContext, 0) }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `createFido2IconCompatFromIconDataOrDefault should return default icon when loading from URI throws InterruptedException`() =
|
||||
runTest {
|
||||
val mockBitmap = mockk<Bitmap>()
|
||||
setupMockGlide(mockBitmap)
|
||||
every { IconCompat.createWithResource(mockContext, 0) } returns mockk()
|
||||
every { IconCompat.createWithBitmap(mockBitmap) } throws InterruptedException()
|
||||
mockContext.createFido2IconCompatFromIconDataOrDefault(
|
||||
iconData = IconData.Network("https://www.mockuri.com", 0),
|
||||
defaultResourceId = 0,
|
||||
)
|
||||
verify { IconCompat.createWithResource(mockContext, 0) }
|
||||
}
|
||||
|
||||
private fun setupMockGlide(mockBitmap: Bitmap) {
|
||||
every { Glide.with(mockContext) } returns mockk {
|
||||
every { asBitmap() } returns mockk {
|
||||
every { load(any<String>()) } returns mockk {
|
||||
every { placeholder(0) } returns mockk {
|
||||
every { submit() } returns mockk {
|
||||
every { get() } returns mockBitmap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
package com.x8bit.bitwarden.ui.platform.glide
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Test class for [BitwardenAppGlideModule] to verify mTLS configuration is properly applied
|
||||
* to Glide without requiring a real mTLS server.
|
||||
*
|
||||
* These tests verify the module's structure and that it can be instantiated.
|
||||
* Full integration testing requires running the app and checking logcat for
|
||||
* "BitwardenGlide" logs when images are loaded.
|
||||
*/
|
||||
class BitwardenAppGlideModuleTest {
|
||||
|
||||
@Test
|
||||
fun `BitwardenAppGlideModule should be instantiable`() {
|
||||
// Verify the module can be created
|
||||
val module = BitwardenAppGlideModule()
|
||||
|
||||
assertNotNull(module)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BitwardenAppGlideModule should have EntryPoint interface for Hilt dependency injection`() {
|
||||
// Verify the Hilt EntryPoint interface exists for accessing CertificateManager
|
||||
val entryPointInterface = BitwardenAppGlideModule::class.java
|
||||
.declaredClasses
|
||||
.firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" }
|
||||
|
||||
assertNotNull(entryPointInterface)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BitwardenGlideEntryPoint should declare certificateManager method`() {
|
||||
// Verify the EntryPoint has the required method to access CertificateManager
|
||||
val entryPointInterface = BitwardenAppGlideModule::class.java
|
||||
.declaredClasses
|
||||
.firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" }
|
||||
|
||||
val methods = requireNotNull(entryPointInterface).declaredMethods
|
||||
val hasCertificateManagerMethod = methods.any { it.name == "certificateManager" }
|
||||
|
||||
assertTrue(hasCertificateManagerMethod)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BitwardenGlideEntryPoint should declare networkCookieManager method`() {
|
||||
val entryPointInterface = BitwardenAppGlideModule::class.java
|
||||
.declaredClasses
|
||||
.firstOrNull { it.simpleName == "BitwardenGlideEntryPoint" }
|
||||
|
||||
val methods = requireNotNull(entryPointInterface).declaredMethods
|
||||
val hasNetworkCookieManagerMethod =
|
||||
methods.any { it.name == "networkCookieManager" }
|
||||
|
||||
assertTrue(hasNetworkCookieManagerMethod)
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.ui.platform.glide
|
||||
package com.x8bit.bitwarden.ui.platform.image
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import com.bitwarden.network.interceptor.FakeInterceptorChain
|
||||
@@ -15,11 +15,11 @@ import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
||||
class GlideCookieInterceptorTest {
|
||||
class ImageCookieInterceptorTest {
|
||||
|
||||
private val mockCookieProvider: CookieProvider = mockk()
|
||||
|
||||
private val interceptor = GlideCookieInterceptor(
|
||||
private val interceptor = ImageCookieInterceptor(
|
||||
cookieProvider = mockCookieProvider,
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.Properties
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.androidx.room)
|
||||
alias(libs.plugins.androidx.room3)
|
||||
alias(libs.plugins.crashlytics)
|
||||
alias(libs.plugins.hilt)
|
||||
alias(libs.plugins.kotlin.compose.compiler)
|
||||
@@ -35,7 +35,7 @@ base {
|
||||
archivesName.set("com.bitwarden.authenticator")
|
||||
}
|
||||
|
||||
room {
|
||||
room3 {
|
||||
schemaDirectory("$projectDir/schemas")
|
||||
}
|
||||
|
||||
@@ -220,14 +220,12 @@ dependencies {
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
testImplementation(libs.testng)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
implementation(libs.androidx.room.runtime)
|
||||
ksp(libs.androidx.room3.compiler)
|
||||
implementation(libs.androidx.room3.runtime)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(libs.androidx.splashscreen)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
implementation(libs.bitwarden.sdk)
|
||||
implementation(libs.bumptech.glide)
|
||||
implementation(platform(libs.google.firebase.bom))
|
||||
implementation(libs.google.firebase.cloud.messaging)
|
||||
implementation(libs.google.firebase.crashlytics)
|
||||
|
||||
Vendored
-7
@@ -46,13 +46,6 @@
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Glide
|
||||
################################################################################
|
||||
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
|
||||
################################################################################
|
||||
# Google Protobuf generated files
|
||||
################################################################################
|
||||
|
||||
+8
-8
@@ -1,25 +1,25 @@
|
||||
package com.bitwarden.authenticator.data.authenticator.datasource.disk.convertor
|
||||
|
||||
import androidx.room.ProvidedTypeConverter
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room3.ColumnTypeConverter
|
||||
import androidx.room3.ProvidedColumnTypeConverter
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemAlgorithm
|
||||
|
||||
/**
|
||||
* A [TypeConverter] to convert [AuthenticatorItemAlgorithm] to and from a [String].
|
||||
* A [ColumnTypeConverter] to convert [AuthenticatorItemAlgorithm] to and from a [String].
|
||||
*/
|
||||
@ProvidedTypeConverter
|
||||
@ProvidedColumnTypeConverter
|
||||
class AuthenticatorItemAlgorithmConverter {
|
||||
|
||||
/**
|
||||
* A [TypeConverter] to convert an [AuthenticatorItemAlgorithm] to a [String].
|
||||
* A [ColumnTypeConverter] to convert an [AuthenticatorItemAlgorithm] to a [String].
|
||||
*/
|
||||
@TypeConverter
|
||||
@ColumnTypeConverter
|
||||
fun toString(item: AuthenticatorItemAlgorithm): String = item.name
|
||||
|
||||
/**
|
||||
* A [TypeConverter] to convert a [String] to an [AuthenticatorItemAlgorithm].
|
||||
* A [ColumnTypeConverter] to convert a [String] to an [AuthenticatorItemAlgorithm].
|
||||
*/
|
||||
@TypeConverter
|
||||
@ColumnTypeConverter
|
||||
fun fromString(itemName: String) = AuthenticatorItemAlgorithm
|
||||
.entries
|
||||
.find { it.name == itemName }
|
||||
|
||||
+8
-8
@@ -1,25 +1,25 @@
|
||||
package com.bitwarden.authenticator.data.authenticator.datasource.disk.convertor
|
||||
|
||||
import androidx.room.ProvidedTypeConverter
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room3.ColumnTypeConverter
|
||||
import androidx.room3.ProvidedColumnTypeConverter
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemType
|
||||
|
||||
/**
|
||||
* A [TypeConverter] to convert [AuthenticatorItemType] to and from a [String].
|
||||
* A [ColumnTypeConverter] to convert [AuthenticatorItemType] to and from a [String].
|
||||
*/
|
||||
@ProvidedTypeConverter
|
||||
@ProvidedColumnTypeConverter
|
||||
class AuthenticatorItemTypeConverter {
|
||||
|
||||
/**
|
||||
* A [TypeConverter] to convert an [AuthenticatorItemType] to a [String].
|
||||
* A [ColumnTypeConverter] to convert an [AuthenticatorItemType] to a [String].
|
||||
*/
|
||||
@TypeConverter
|
||||
@ColumnTypeConverter
|
||||
fun toString(item: AuthenticatorItemType): String = item.name
|
||||
|
||||
/**
|
||||
* A [TypeConverter] to convert a [String] to an [AuthenticatorItemType].
|
||||
* A [ColumnTypeConverter] to convert a [String] to an [AuthenticatorItemType].
|
||||
*/
|
||||
@TypeConverter
|
||||
@ColumnTypeConverter
|
||||
fun fromString(itemName: String) = AuthenticatorItemType
|
||||
.entries
|
||||
.find { it.name == itemName }
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
package com.bitwarden.authenticator.data.authenticator.datasource.disk.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Insert
|
||||
import androidx.room3.OnConflictStrategy
|
||||
import androidx.room3.Query
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.AuthenticatorItemEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
package com.bitwarden.authenticator.data.authenticator.datasource.disk.database
|
||||
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room3.AutoMigration
|
||||
import androidx.room3.ColumnTypeConverters
|
||||
import androidx.room3.Database
|
||||
import androidx.room3.RoomDatabase
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.convertor.AuthenticatorItemAlgorithmConverter
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.convertor.AuthenticatorItemTypeConverter
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.dao.ItemDao
|
||||
@@ -22,7 +22,7 @@ import com.bitwarden.authenticator.data.authenticator.datasource.disk.entity.Aut
|
||||
version = 2,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(
|
||||
@ColumnTypeConverters(
|
||||
AuthenticatorItemTypeConverter::class,
|
||||
AuthenticatorItemAlgorithmConverter::class,
|
||||
)
|
||||
|
||||
+6
-8
@@ -1,7 +1,8 @@
|
||||
package com.bitwarden.authenticator.data.authenticator.datasource.disk.di
|
||||
|
||||
import android.app.Application
|
||||
import androidx.room.Room
|
||||
import androidx.room3.Room
|
||||
import androidx.sqlite.driver.AndroidSQLiteDriver
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.AuthenticatorDiskSource
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.AuthenticatorDiskSourceImpl
|
||||
import com.bitwarden.authenticator.data.authenticator.datasource.disk.convertor.AuthenticatorItemAlgorithmConverter
|
||||
@@ -25,14 +26,11 @@ object AuthenticatorDiskModule {
|
||||
@Singleton
|
||||
fun provideAuthenticatorDatabase(app: Application): AuthenticatorDatabase =
|
||||
Room
|
||||
.databaseBuilder(
|
||||
context = app,
|
||||
klass = AuthenticatorDatabase::class.java,
|
||||
name = "authenticator_database",
|
||||
)
|
||||
.databaseBuilder<AuthenticatorDatabase>(context = app, name = "authenticator_database")
|
||||
.fallbackToDestructiveMigration(dropAllTables = true)
|
||||
.addTypeConverter(AuthenticatorItemTypeConverter())
|
||||
.addTypeConverter(AuthenticatorItemAlgorithmConverter())
|
||||
.addColumnTypeConverter(typeConverter = AuthenticatorItemTypeConverter())
|
||||
.addColumnTypeConverter(typeConverter = AuthenticatorItemAlgorithmConverter())
|
||||
.setDriver(driver = AndroidSQLiteDriver())
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@ package com.bitwarden.authenticator.data.authenticator.datasource.disk.entity
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.text.htmlEncode
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room3.ColumnInfo
|
||||
import androidx.room3.Entity
|
||||
import androidx.room3.PrimaryKey
|
||||
|
||||
/**
|
||||
* Entity representing an authenticator item in the database.
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import io.gitlab.arturbosch.detekt.Detekt
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.android.library) apply false
|
||||
alias(libs.plugins.androidx.room) apply false
|
||||
alias(libs.plugins.androidx.room3) apply false
|
||||
alias(libs.plugins.detekt) apply true
|
||||
alias(libs.plugins.hilt) apply false
|
||||
alias(libs.plugins.kotlin.compose.compiler) apply false
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# This file is expected to be part of source control.
|
||||
androidx.databinding:databinding-common:9.4.1=classpath
|
||||
androidx.databinding:databinding-compiler-common:9.4.1=classpath
|
||||
androidx.room:androidx.room.gradle.plugin:2.8.5=classpath
|
||||
androidx.room:room-gradle-plugin:2.8.5=classpath
|
||||
androidx.room3:androidx.room3.gradle.plugin:3.0.3=classpath
|
||||
androidx.room3:room3-gradle-plugin:3.0.3=classpath
|
||||
com.android.application:com.android.application.gradle.plugin:9.4.1=classpath
|
||||
com.android.databinding:baseLibrary:9.4.1=classpath
|
||||
com.android.library:com.android.library.gradle.plugin:9.4.1=classpath
|
||||
|
||||
@@ -72,7 +72,7 @@ Note that any functions exposed by a data layer class that must perform asynchro
|
||||
|
||||
### Data Sources
|
||||
|
||||
The lowest level of the data layer are the "data source" classes. These are the raw sources of data that include data persisted in [Room](https://developer.android.com/jetpack/androidx/releases/room) / [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences), data retrieved from network requests using [Retrofit](https://github.com/square/retrofit), and data retrieved via interactions with the [Bitwarden SDK](https://github.com/bitwarden/sdk).
|
||||
The lowest level of the data layer are the "data source" classes. These are the raw sources of data that include data persisted in [Room](https://developer.android.com/jetpack/androidx/releases/room3) / [SharedPreferences](https://developer.android.com/reference/android/content/SharedPreferences), data retrieved from network requests using [Retrofit](https://github.com/square/retrofit), and data retrieved via interactions with the [Bitwarden SDK](https://github.com/bitwarden/sdk).
|
||||
|
||||
Note that these data sources are constructed in a manner that adheres to a very important principle of the app: **that function calls should not throw exceptions** (see the [style and best practices documentation](STYLE_AND_BEST_PRACTICES.md#best-practices--kotlin) for more details.) In the case of data sources, this tends to mean that suspending functions like those representing network requests or Bitwarden SDK calls should return a [Result](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/) type. This is an important responsibility of the data layer as a wrapper around other third party libraries, as dependencies like Retrofit and the Bitwarden SDK tend to throw exceptions to indicate errors instead.
|
||||
|
||||
|
||||
@@ -25,16 +25,15 @@ androidxCredentialsProviderEvents = "1.0.0-alpha06"
|
||||
androidxHiltNavigationCompose = "1.4.0"
|
||||
androidxLifecycle = "2.11.0"
|
||||
androidxNavigation = "2.9.8"
|
||||
androidxRoom = "2.8.5"
|
||||
androidxRoom3 = "3.0.3"
|
||||
androidxSecurityCrypto = "1.1.0"
|
||||
androidxSplash = "1.2.0"
|
||||
androidxWork = "2.12.0"
|
||||
bitwardenSdk = "4.0.0-9547-06f2c6f3"
|
||||
coil = "3.6.3"
|
||||
crashlytics = "3.0.8"
|
||||
detekt = "1.23.8"
|
||||
firebaseBom = "35.0.0"
|
||||
glide = "5.0.9"
|
||||
glideCompose = "1.0.0-beta01"
|
||||
googleBilling = "9.1.0"
|
||||
googleMlkitTextRecognition = "16.0.1"
|
||||
googleGuava = "33.7.2-jre"
|
||||
@@ -92,16 +91,16 @@ androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process",
|
||||
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
|
||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
|
||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }
|
||||
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidxRoom" }
|
||||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" }
|
||||
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "androidxRoom3" }
|
||||
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "androidxRoom3" }
|
||||
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "androidxSecurityCrypto" }
|
||||
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxSplash" }
|
||||
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWork" }
|
||||
bitwarden-sdk = { module = "com.bitwarden:sdk-android", version.ref = "bitwardenSdk" }
|
||||
bumptech-glide = { module = "com.github.bumptech.glide:compose", version.ref = "glideCompose" }
|
||||
bumptech-glide-okhttp = { module = "com.github.bumptech.glide:okhttp3-integration", version.ref = "glide" }
|
||||
bumptech-glide-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
|
||||
coil-coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
|
||||
coil-coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||
coil-coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" }
|
||||
coil-coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
|
||||
detekt-detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
|
||||
detekt-detekt-rules = { module = "io.gitlab.arturbosch.detekt:detekt-rules-libraries", version.ref = "detekt" }
|
||||
google-billing = { module = "com.android.billingclient:billing", version.ref = "googleBilling" }
|
||||
@@ -144,7 +143,7 @@ testng = { group = "org.testng", name = "testng", version.ref = "testng" }
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
|
||||
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
|
||||
androidx-room = { id = "androidx.room", version.ref = "androidxRoom" }
|
||||
androidx-room3 = { id = "androidx.room3", version.ref = "androidxRoom3" }
|
||||
crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "crashlytics" }
|
||||
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
|
||||
google-protobuf = { id = "com.google.protobuf", version.ref = "googleProtoBufPlugin" }
|
||||
|
||||
+2
-1
@@ -75,7 +75,8 @@ dependencies {
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.bumptech.glide)
|
||||
implementation(libs.coil.coil.compose)
|
||||
implementation(libs.coil.coil.gif)
|
||||
implementation(libs.kotlinx.serialization)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
@@ -4,21 +4,19 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import coil3.compose.AsyncImage
|
||||
import com.bitwarden.ui.platform.base.util.nullableTestTag
|
||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||
import com.bumptech.glide.integration.compose.GlideImage
|
||||
import com.bumptech.glide.integration.compose.placeholder
|
||||
|
||||
/**
|
||||
* Represents a Bitwarden icon that is either locally loaded or loaded using glide.
|
||||
* Represents a Bitwarden icon that is either locally loaded or loaded using Coil.
|
||||
*
|
||||
* @param iconData Label for the text field.
|
||||
* @param modifier A [Modifier] for the composable.
|
||||
* @param tint the color to be applied as the tint for the icon.
|
||||
*/
|
||||
@OptIn(ExperimentalGlideComposeApi::class)
|
||||
@Composable
|
||||
fun BitwardenIcon(
|
||||
iconData: IconData,
|
||||
@@ -27,14 +25,14 @@ fun BitwardenIcon(
|
||||
) {
|
||||
when (iconData) {
|
||||
is IconData.Network -> {
|
||||
GlideImage(
|
||||
val fallbackPainter = painterResource(id = iconData.fallbackIconRes)
|
||||
AsyncImage(
|
||||
model = iconData.uri,
|
||||
failure = placeholder(iconData.fallbackIconRes),
|
||||
contentDescription = iconData.contentDescription?.invoke(),
|
||||
placeholder = fallbackPainter,
|
||||
error = fallbackPainter,
|
||||
modifier = modifier.nullableTestTag(tag = iconData.testTag),
|
||||
) {
|
||||
it.placeholder(iconData.fallbackIconRes)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is IconData.Local -> {
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
package com.bitwarden.ui.platform.components.image
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||
import com.bumptech.glide.integration.compose.GlideImage
|
||||
import coil3.compose.AsyncImage
|
||||
|
||||
/**
|
||||
* A composable that displays a gif image.
|
||||
*
|
||||
* The content will also be scaled to fit the image to the container.
|
||||
*/
|
||||
@OptIn(ExperimentalGlideComposeApi::class)
|
||||
@Composable
|
||||
fun BitwardenGifImage(
|
||||
@DrawableRes resId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
// The need for this workaround of loading into a drawable first is related to issue reported
|
||||
// [here](https://github.com/bumptech/glide/issues/3751)
|
||||
val context = LocalContext.current
|
||||
val drawable = AppCompatResources.getDrawable(context, resId)
|
||||
GlideImage(
|
||||
model = drawable,
|
||||
// Coil automatically registers the `coil-gif` decoder, which handles the animation.
|
||||
AsyncImage(
|
||||
model = resId,
|
||||
contentDescription = contentDescription,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = modifier,
|
||||
|
||||
@@ -1441,7 +1441,7 @@ Szeretnénk átváltani erre a fiókra?</string>
|
||||
<string name="use_this_passphrase">Jelmondat használata</string>
|
||||
<string name="use_this_password">Jelszó használata</string>
|
||||
<string name="use_this_username">Felhasználónév használata</string>
|
||||
<string name="create_login">Create login</string>
|
||||
<string name="create_login">Bejelentkezés létrehozása</string>
|
||||
<string name="keep_unlocking_with_biometrics">Feloldás folytatása biometrikus adatokkal</string>
|
||||
<string name="weve_made_a_security_update">Biztonsági frissítés történt. Erősítsük meg újra a biometrikus adatokat, hogy továbbra is használjuk azokat a Bitwarden feloldásához.</string>
|
||||
<string name="turn_off">Kikapcsolás</string>
|
||||
|
||||
@@ -21,47 +21,47 @@
|
||||
<string name="enter_pin">PIN-Code aginn</string>
|
||||
<string name="favorites">Favoritten</string>
|
||||
<string name="folder">Dossier</string>
|
||||
<string name="folder_created">New folder created</string>
|
||||
<string name="folder_deleted">Folder deleted.</string>
|
||||
<string name="folder_none">No Folder</string>
|
||||
<string name="folder_updated">Folder saved</string>
|
||||
<string name="folders">Folders</string>
|
||||
<string name="folder_created">Neien Dossier erstellt.</string>
|
||||
<string name="folder_deleted">Dossier geläscht.</string>
|
||||
<string name="folder_none">Keen Dossier</string>
|
||||
<string name="folder_updated">Dossier gespäichert</string>
|
||||
<string name="folders">Dossieren</string>
|
||||
<string name="hide">Verstoppen</string>
|
||||
<string name="internet_connection_required_message">Please connect to the internet before continuing.</string>
|
||||
<string name="internet_connection_required_title">Internet connection required</string>
|
||||
<string name="invalid_master_password">Invalid master password. Try again.</string>
|
||||
<string name="invalid_pin">Invalid PIN. Try again.</string>
|
||||
<string name="launch">Launch</string>
|
||||
<string name="log_in_verb">Log in</string>
|
||||
<string name="internet_connection_required_message">Wannechgelift verbann dech mam Internet fir virunzefueren.</string>
|
||||
<string name="internet_connection_required_title">Internetverbindung noutwenneg</string>
|
||||
<string name="invalid_master_password">Ongëltegt Master-Passwuert. Wgl. erëm probéieren.</string>
|
||||
<string name="invalid_pin">Ongëlteg PIN. Wgl. erëm probéieren.</string>
|
||||
<string name="launch">Opmaachen</string>
|
||||
<string name="log_in_verb">Umellen</string>
|
||||
<string name="log_in_noun">Login</string>
|
||||
<string name="log_out">Log out</string>
|
||||
<string name="logout_confirmation">Are you sure you want to log out?</string>
|
||||
<string name="remove_account">Remove account</string>
|
||||
<string name="remove_account_confirmation">Are you sure you want to remove this account?</string>
|
||||
<string name="account_already_added">Account already added</string>
|
||||
<string name="switch_to_already_added_account_confirmation">Would you like to switch to it now?</string>
|
||||
<string name="master_password">Master password</string>
|
||||
<string name="log_out">Ofmellen</string>
|
||||
<string name="logout_confirmation">Bass du sécher, datt s du dech ofmelle wëlls?</string>
|
||||
<string name="remove_account">Kont ewechhuelen</string>
|
||||
<string name="remove_account_confirmation">Wëlls du dëse Kont wierklech ewechhuelen?</string>
|
||||
<string name="account_already_added">Kont scho bäigesat</string>
|
||||
<string name="switch_to_already_added_account_confirmation">Wëlls du elo dohi wiesselen?</string>
|
||||
<string name="master_password">Master-Passwuert</string>
|
||||
<string name="master_password_required">Master password (required)</string>
|
||||
<string name="new_master_password_required">New master password (required)</string>
|
||||
<string name="more_options">Méi Optiounen</string>
|
||||
<string name="my_folder">Mäin Dossier</string>
|
||||
<string name="my_folders">Meng Dossieren</string>
|
||||
<string name="my_vault">My vault</string>
|
||||
<string name="name">Name</string>
|
||||
<string name="my_vault">Mäin Tresor</string>
|
||||
<string name="name">Numm</string>
|
||||
<string name="item_name_required">Item name (required)</string>
|
||||
<string name="send_name_required">Send name (required)</string>
|
||||
<string name="no">No</string>
|
||||
<string name="notes">Notes</string>
|
||||
<string name="no">Nee</string>
|
||||
<string name="notes">Notizen</string>
|
||||
<string name="private_notes">Privat Notizen</string>
|
||||
<string name="okay">Okay</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="move">Move</string>
|
||||
<string name="saving">Saving…</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="password">Passwuert</string>
|
||||
<string name="save">Späicheren</string>
|
||||
<string name="move">Réckelen</string>
|
||||
<string name="saving">Späicheren…</string>
|
||||
<string name="settings">Astellungen</string>
|
||||
<string name="show">Weisen</string>
|
||||
<string name="item_deleted">Item deleted</string>
|
||||
<string name="submit">Submit</string>
|
||||
<string name="item_deleted">Element geläscht</string>
|
||||
<string name="submit">Ofschécken</string>
|
||||
<string name="sync">Synchroniséieren</string>
|
||||
<string name="uri">URI</string>
|
||||
<string name="website_uri">Website (URI)</string>
|
||||
@@ -932,7 +932,7 @@ Do you want to switch to this account?</string>
|
||||
<string name="copy_private_key">Copy private key</string>
|
||||
<string name="login_credentials">Login Credentials</string>
|
||||
<string name="autofill_options">Autofill Options</string>
|
||||
<string name="use_this_button_to_generate_a_new_unique_password">Use this button to generate a new unique password.</string>
|
||||
<string name="use_this_button_to_generate_a_new_unique_password">Verwenn dëse Knäppchen, fir en neit, eenzegaartegt Passwuert ze generéieren.</string>
|
||||
<string name="coachmark_1_of_3">1 OF 3</string>
|
||||
<string name="coachmark_2_of_3">2 OF 3</string>
|
||||
<string name="you_ll_only_need_to_set_up_authenticator_key">You’ll only need to set up Authenticator Key for logins that require two-factor authentication with a code. The key will continuously generate six-digit codes you can use to log in.</string>
|
||||
|
||||
@@ -574,8 +574,8 @@ Skanningen sker automatiskt.</string>
|
||||
<string name="resend_code">Skicka koden igen</string>
|
||||
<string name="a_verification_code_was_sent_to_your_email">En verifieringskod skickades till din e-postadress</string>
|
||||
<string name="submit_crash_logs">Skicka kraschloggar</string>
|
||||
<string name="expand">expand</string>
|
||||
<string name="collapse">collapse</string>
|
||||
<string name="expand">fäll ut</string>
|
||||
<string name="collapse">fäll ihop</string>
|
||||
<string name="uppercase_ato_z">Versaler (A till Z)</string>
|
||||
<string name="lowercase_ato_z">Gemener (A till Z)</string>
|
||||
<string name="numbers_zero_to_nine">Siffror (0 till 9)</string>
|
||||
@@ -1442,10 +1442,10 @@ Vill du byta till detta konto?</string>
|
||||
<string name="use_this_passphrase">Använd den här lösenordsfrasen</string>
|
||||
<string name="use_this_password">Använd det här lösenordet</string>
|
||||
<string name="use_this_username">Använd det här användarnamnet</string>
|
||||
<string name="create_login">Create login</string>
|
||||
<string name="create_login">Skapa inloggning</string>
|
||||
<string name="keep_unlocking_with_biometrics">Behåll upplåsning med biometri</string>
|
||||
<string name="weve_made_a_security_update">Vi har gjort en säkerhetsuppdatering. Bekräfta din biometri igen för att fortsätta använda den för att låsa upp Bitwarden.</string>
|
||||
<string name="turn_off">Stäng av</string>
|
||||
<string name="turn_off_biometric_question">Stänga av biometri?</string>
|
||||
<string name="unlocking_will_require_your_master_password">Unlocking will require your master password. You can turn biometrics back on in Settings.</string>
|
||||
<string name="unlocking_will_require_your_master_password">Upplåsning kräver ditt huvudlösenord. Du kan aktivera biometri igen i Inställningar.</string>
|
||||
</resources>
|
||||
@@ -1464,10 +1464,10 @@
|
||||
<string name="use_this_passphrase">Використати цю парольну фразу</string>
|
||||
<string name="use_this_password">Використати цей пароль</string>
|
||||
<string name="use_this_username">Використати це ім\'я користувача</string>
|
||||
<string name="create_login">Create login</string>
|
||||
<string name="keep_unlocking_with_biometrics">Продовжуйте розблоковувати за допомогою біометрії</string>
|
||||
<string name="weve_made_a_security_update">Ми внесли оновлення безпеки. Підтвердьте свої біометричні дані ще раз, щоб і надалі використовувати їх для розблокування Bitwarden.</string>
|
||||
<string name="create_login">Створити запис входу</string>
|
||||
<string name="keep_unlocking_with_biometrics">Надалі розблоковувати за допомогою біометрії</string>
|
||||
<string name="weve_made_a_security_update">В нещодавньому оновленні вдосконалено безпеку. Підтвердьте свої біометричні дані ще раз, щоб і надалі використовувати їх для розблокування Bitwarden.</string>
|
||||
<string name="turn_off">Вимкнути</string>
|
||||
<string name="turn_off_biometric_question">Вимкнути біометрику?</string>
|
||||
<string name="unlocking_will_require_your_master_password">Для розблокування знадобиться ваш головний пароль. Ви можете знову увімкнути біометричну аутентифікацію в розділі «Налаштування».</string>
|
||||
<string name="turn_off_biometric_question">Вимкнути біометрію?</string>
|
||||
<string name="unlocking_will_require_your_master_password">Для розблокування знадобиться ваш головний пароль. Ви можете знову ввімкнути біометрію в налаштуваннях.</string>
|
||||
</resources>
|
||||
@@ -838,7 +838,7 @@
|
||||
<string name="save_and_protect_your_data">保存和保护您的数据</string>
|
||||
<string name="the_vault_protects_more_than_just_passwords">密码库不仅保护密码。在这里还可以安全地存储登录、ID、支付卡和笔记。</string>
|
||||
<string name="add_login">添加登录</string>
|
||||
<string name="share_files_and_data_securely_with_anyone_on_any_platform">在任何平台上安全地与任何人共享文件和数据。您的信息将在限制曝光的同时保持端到端加密。</string>
|
||||
<string name="share_files_and_data_securely_with_anyone_on_any_platform">在任何平台上与任何人安全地分享文件和数据。您的信息将在限制暴露的同时保持端到端加密。</string>
|
||||
<string name="send_sensitive_information_safely">安全地发送敏感信息</string>
|
||||
<string name="import_saved_logins">导入已保存的登录信息</string>
|
||||
<string name="use_a_computer_to_import_logins">使用计算机从现有密码管理器导入登录信息</string>
|
||||
|
||||
Reference in new issue
Block a user