mirror of
https://github.com/bitwarden/android.git
synced 2026-06-05 12:16:32 -05:00
BIT-1093: Add TOTP copying to autofill flow (#879)
Co-authored-by: David Perez <david@livefront.com>
This commit is contained in:
committed by
Álison Fernandes
parent
2be47c5b0f
commit
a92d9ff823
@@ -0,0 +1,72 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.x8bit.bitwarden.data.autofill.manager.AutofillCompletionManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* An activity for copying a TOTP code to the clipboard. This is done when an autofill item is
|
||||
* selected and it requires TOTP authentication. Due to the constraints of the autofill framework,
|
||||
* we also have to re-fulfill the autofill for the views that are being filled.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class AutofillTotpCopyActivity : AppCompatActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var autofillCompletionManager: AutofillCompletionManager
|
||||
|
||||
private val autofillTotpCopyViewModel: AutofillTotpCopyViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
observeViewModelEvents()
|
||||
|
||||
autofillTotpCopyViewModel.trySendAction(
|
||||
AutofillTotpCopyAction.IntentReceived(
|
||||
intent = intent,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun observeViewModelEvents() {
|
||||
autofillTotpCopyViewModel
|
||||
.eventFlow
|
||||
.onEach { event ->
|
||||
when (event) {
|
||||
is AutofillTotpCopyEvent.CompleteAutofill -> {
|
||||
handleCompleteAutofill(event)
|
||||
}
|
||||
|
||||
is AutofillTotpCopyEvent.FinishActivity -> {
|
||||
finishActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete autofill with the provided data.
|
||||
*/
|
||||
private fun handleCompleteAutofill(event: AutofillTotpCopyEvent.CompleteAutofill) {
|
||||
autofillCompletionManager.completeAutofill(
|
||||
activity = this,
|
||||
cipherView = event.cipherView,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish the activity.
|
||||
*/
|
||||
private fun finishActivity() {
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.x8bit.bitwarden
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.util.getTotpCopyIntentOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.launchWithTimeout
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockData
|
||||
import com.x8bit.bitwarden.data.vault.repository.util.statusFor
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* The amount of time we should wait for ciphers to be loaded before timing out.
|
||||
*/
|
||||
private const val CIPHER_WAIT_TIMEOUT_MILLIS: Long = 500
|
||||
|
||||
/**
|
||||
* A view model that handles logic for the [AutofillTotpCopyActivity].
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AutofillTotpCopyViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : BaseViewModel<Unit, AutofillTotpCopyEvent, AutofillTotpCopyAction>(Unit) {
|
||||
private val activeUserId: String? get() = authRepository.activeUserId
|
||||
|
||||
override fun handleAction(action: AutofillTotpCopyAction): Unit = when (action) {
|
||||
is AutofillTotpCopyAction.IntentReceived -> handleIntentReceived(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the received intent and alert the activity of what to do next.
|
||||
*/
|
||||
private fun handleIntentReceived(action: AutofillTotpCopyAction.IntentReceived) {
|
||||
viewModelScope
|
||||
.launchWithTimeout(
|
||||
timeoutBlock = { finishActivity() },
|
||||
timeoutDuration = CIPHER_WAIT_TIMEOUT_MILLIS,
|
||||
) {
|
||||
// Extract TOTP copy data from the intent.
|
||||
val cipherId = action
|
||||
.intent
|
||||
.getTotpCopyIntentOrNull()
|
||||
?.cipherId
|
||||
|
||||
if (cipherId == null || isVaultLocked()) {
|
||||
finishActivity()
|
||||
return@launchWithTimeout
|
||||
}
|
||||
|
||||
// Try and find the matching cipher.
|
||||
vaultRepository
|
||||
.ciphersStateFlow
|
||||
.mapNotNull { it.data }
|
||||
.first()
|
||||
.find { it.id == cipherId }
|
||||
?.let { cipherView ->
|
||||
sendEvent(
|
||||
AutofillTotpCopyEvent.CompleteAutofill(
|
||||
cipherView = cipherView,
|
||||
),
|
||||
)
|
||||
}
|
||||
?: finishActivity()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event to the activity that signals it to finish.
|
||||
*/
|
||||
private fun finishActivity() {
|
||||
sendEvent(AutofillTotpCopyEvent.FinishActivity)
|
||||
}
|
||||
|
||||
private suspend fun isVaultLocked(): Boolean {
|
||||
val userId = activeUserId ?: return true
|
||||
|
||||
// Wait for any unlocking actions to finish. This can be relevant on startup for Never lock
|
||||
// accounts.
|
||||
vaultRepository.vaultUnlockDataStateFlow.first {
|
||||
it.statusFor(userId) != VaultUnlockData.Status.UNLOCKING
|
||||
}
|
||||
|
||||
return !vaultRepository.isVaultUnlocked(userId = userId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents actions that can be sent to the [AutofillTotpCopyViewModel].
|
||||
*/
|
||||
sealed class AutofillTotpCopyAction {
|
||||
/**
|
||||
* An [intent] has been received and is ready to be processed.
|
||||
*/
|
||||
data class IntentReceived(
|
||||
val intent: Intent,
|
||||
) : AutofillTotpCopyAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents events emitted by the [AutofillTotpCopyViewModel].
|
||||
*/
|
||||
sealed class AutofillTotpCopyEvent {
|
||||
/**
|
||||
* Complete autofill with the provided [cipherView].
|
||||
*/
|
||||
data class CompleteAutofill(
|
||||
val cipherView: CipherView,
|
||||
) : AutofillTotpCopyEvent()
|
||||
|
||||
/**
|
||||
* Finish the activity.
|
||||
*/
|
||||
data object FinishActivity : AutofillTotpCopyEvent()
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.autofill.builder
|
||||
|
||||
import android.content.IntentSender
|
||||
import android.service.autofill.FillResponse
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledData
|
||||
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.buildVaultItemDataset
|
||||
import com.x8bit.bitwarden.data.autofill.util.createTotpCopyIntentSender
|
||||
import com.x8bit.bitwarden.data.autofill.util.fillableAutofillIds
|
||||
|
||||
/**
|
||||
@@ -28,7 +31,11 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||
// We build a dataset for each filled partition. A filled partition is a
|
||||
// copy of all the views that we are going to fill, loaded with the data
|
||||
// from one of the ciphers that can fulfill this partition type.
|
||||
val authIntentSender = filledPartition.toAuthIntentSenderOrNull(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
)
|
||||
val dataset = filledPartition.buildDataset(
|
||||
authIntentSender = authIntentSender,
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
)
|
||||
|
||||
@@ -56,3 +63,22 @@ class FillResponseBuilderImpl : FillResponseBuilder {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this [FilledPartition] and [autofillAppInfo] into an [IntentSender] if totp is enabled
|
||||
* and there the [FilledPartition.autofillCipher] has a valid cipher id.
|
||||
*/
|
||||
private fun FilledPartition.toAuthIntentSenderOrNull(
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
): IntentSender? {
|
||||
val isTotpEnabled = this.autofillCipher.isTotpEnabled
|
||||
val cipherId = this.autofillCipher.cipherId
|
||||
return if (isTotpEnabled && cipherId != null) {
|
||||
createTotpCopyIntentSender(
|
||||
cipherId = cipherId,
|
||||
context = autofillAppInfo.context,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
@@ -50,11 +51,17 @@ object AutofillModule {
|
||||
@Provides
|
||||
fun provideAutofillCompletionManager(
|
||||
autofillParser: AutofillParser,
|
||||
authRepository: AuthRepository,
|
||||
clipboardManager: BitwardenClipboardManager,
|
||||
dispatcherManager: DispatcherManager,
|
||||
vaultRepository: VaultRepository,
|
||||
): AutofillCompletionManager =
|
||||
AutofillCompletionManagerImpl(
|
||||
authRepository = authRepository,
|
||||
autofillParser = autofillParser,
|
||||
clipboardManager = clipboardManager,
|
||||
dispatcherManager = dispatcherManager,
|
||||
vaultRepository = vaultRepository,
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -2,7 +2,11 @@ package com.x8bit.bitwarden.data.autofill.manager
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilder
|
||||
import com.x8bit.bitwarden.data.autofill.builder.FilledDataBuilderImpl
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillRequest
|
||||
@@ -12,7 +16,10 @@ import com.x8bit.bitwarden.data.autofill.util.createAutofillSelectionResultInten
|
||||
import com.x8bit.bitwarden.data.autofill.util.getAutofillAssistStructureOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillAppInfo
|
||||
import com.x8bit.bitwarden.data.autofill.util.toAutofillCipherProvider
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.GenerateTotpResult
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -20,10 +27,13 @@ import kotlinx.coroutines.launch
|
||||
* Primary implementation of [AutofillCompletionManager].
|
||||
*/
|
||||
class AutofillCompletionManagerImpl(
|
||||
private val authRepository: AuthRepository,
|
||||
private val autofillParser: AutofillParser,
|
||||
private val clipboardManager: BitwardenClipboardManager,
|
||||
private val dispatcherManager: DispatcherManager,
|
||||
private val filledDataBuilderProvider: (CipherView) -> FilledDataBuilder =
|
||||
{ createSingleItemFilledDataBuilder(cipherView = it) },
|
||||
private val vaultRepository: VaultRepository,
|
||||
) : AutofillCompletionManager {
|
||||
private val mainScope = CoroutineScope(dispatcherManager.main)
|
||||
|
||||
@@ -58,15 +68,55 @@ class AutofillCompletionManagerImpl(
|
||||
.build(autofillRequest)
|
||||
.filledPartitions
|
||||
.firstOrNull()
|
||||
?.buildDataset(autofillAppInfo = autofillAppInfo)
|
||||
?.buildDataset(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
authIntentSender = null,
|
||||
)
|
||||
?: run {
|
||||
activity.cancelAndFinish()
|
||||
return@launch
|
||||
}
|
||||
tryCopyTotpToClipboard(
|
||||
activity = activity,
|
||||
cipherView = cipherView,
|
||||
)
|
||||
val resultIntent = createAutofillSelectionResultIntent(dataset)
|
||||
activity.setResultAndFinish(resultIntent = resultIntent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to copy the totp code to clipboard. If it succeeds show a toast.
|
||||
*
|
||||
* @param activity An activity for launching a toast.
|
||||
* @param cipherView The [CipherView] for which to generate a TOTP code.
|
||||
*/
|
||||
private suspend fun tryCopyTotpToClipboard(
|
||||
activity: Activity,
|
||||
cipherView: CipherView,
|
||||
) {
|
||||
val isPremium = authRepository.userStateFlow.value?.activeAccount?.isPremium == true
|
||||
val totpCode = cipherView.login?.totp
|
||||
|
||||
// TODO check global TOTP enabled status BIT-1093
|
||||
if (isPremium && totpCode != null) {
|
||||
val totpResult = vaultRepository.generateTotp(
|
||||
time = DateTime.now(),
|
||||
totpCode = totpCode,
|
||||
)
|
||||
|
||||
if (totpResult is GenerateTotpResult.Success) {
|
||||
clipboardManager.setText(totpResult.code)
|
||||
Toast
|
||||
.makeText(
|
||||
activity.applicationContext,
|
||||
R.string.verification_code_totp,
|
||||
Toast.LENGTH_LONG,
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSingleItemFilledDataBuilder(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.bitwarden.core.Uuid
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,11 @@ sealed class AutofillCipher {
|
||||
*/
|
||||
abstract val iconRes: Int
|
||||
|
||||
/**
|
||||
* Whether or not TOTP is enabled for this cipher.
|
||||
*/
|
||||
abstract val isTotpEnabled: Boolean
|
||||
|
||||
/**
|
||||
* The name of the cipher.
|
||||
*/
|
||||
@@ -22,11 +28,17 @@ sealed class AutofillCipher {
|
||||
*/
|
||||
abstract val subtitle: String
|
||||
|
||||
/**
|
||||
* The ID that corresponds to the CipherView used to create this [AutofillCipher].
|
||||
*/
|
||||
abstract val cipherId: String?
|
||||
|
||||
/**
|
||||
* The card [AutofillCipher] model. This contains all of the data for building fulfilling a card
|
||||
* partition.
|
||||
*/
|
||||
data class Card(
|
||||
override val cipherId: String?,
|
||||
override val name: String,
|
||||
override val subtitle: String,
|
||||
val cardholderName: String,
|
||||
@@ -37,6 +49,9 @@ sealed class AutofillCipher {
|
||||
) : AutofillCipher() {
|
||||
override val iconRes: Int
|
||||
@DrawableRes get() = R.drawable.ic_card_item
|
||||
|
||||
override val isTotpEnabled: Boolean
|
||||
get() = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,6 +59,8 @@ sealed class AutofillCipher {
|
||||
* login partition.
|
||||
*/
|
||||
data class Login(
|
||||
override val cipherId: Uuid?,
|
||||
override val isTotpEnabled: Boolean,
|
||||
override val name: String,
|
||||
override val subtitle: String,
|
||||
val password: String,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.autofill.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* Represents data for a TOTP copying during the autofill flow via authentication intents.
|
||||
*
|
||||
* @property cipherId The cipher for which we are copying a TOTP to the clipboard.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AutofillTotpCopyData(
|
||||
val cipherId: String,
|
||||
) : Parcelable
|
||||
@@ -44,6 +44,7 @@ class AutofillCipherProviderImpl(
|
||||
.takeIf { cipherView.type == CipherType.CARD && cipherView.deletedDate == null }
|
||||
?.let { nonNullCipherView ->
|
||||
AutofillCipher.Card(
|
||||
cipherId = cipherView.id,
|
||||
name = nonNullCipherView.name,
|
||||
subtitle = nonNullCipherView.subtitle.orEmpty(),
|
||||
cardholderName = nonNullCipherView.card?.cardholderName.orEmpty(),
|
||||
@@ -72,6 +73,8 @@ class AutofillCipherProviderImpl(
|
||||
)
|
||||
.map { cipherView ->
|
||||
AutofillCipher.Login(
|
||||
cipherId = cipherView.id,
|
||||
isTotpEnabled = cipherView.login?.totp != null,
|
||||
name = cipherView.name,
|
||||
password = cipherView.login?.password.orEmpty(),
|
||||
subtitle = cipherView.subtitle.orEmpty(),
|
||||
|
||||
@@ -2,17 +2,22 @@
|
||||
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.assist.AssistStructure
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentSender
|
||||
import android.service.autofill.Dataset
|
||||
import android.view.autofill.AutofillManager
|
||||
import com.x8bit.bitwarden.AutofillTotpCopyActivity
|
||||
import com.x8bit.bitwarden.MainActivity
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillTotpCopyData
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.data.platform.util.getSafeParcelableExtra
|
||||
|
||||
private const val AUTOFILL_SELECTION_DATA_KEY = "autofill-selection-data"
|
||||
private const val AUTOFILL_TOTP_COPY_DATA_KEY = "autofill-totp-copy-data"
|
||||
|
||||
/**
|
||||
* Creates an [Intent] in order to send the user to a manual selection process for autofill.
|
||||
@@ -36,6 +41,37 @@ fun createAutofillSelectionIntent(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [IntentSender] built with the data required for performing a TOTP copying during
|
||||
* the autofill flow.
|
||||
*/
|
||||
fun createTotpCopyIntentSender(
|
||||
cipherId: String,
|
||||
context: Context,
|
||||
): IntentSender {
|
||||
val intent = Intent(
|
||||
context,
|
||||
AutofillTotpCopyActivity::class.java,
|
||||
)
|
||||
.apply {
|
||||
putExtra(
|
||||
AUTOFILL_TOTP_COPY_DATA_KEY,
|
||||
AutofillTotpCopyData(
|
||||
cipherId = cipherId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return PendingIntent
|
||||
.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT.toPendingIntentMutabilityFlag(),
|
||||
)
|
||||
.intentSender
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an [Intent] in order to specify that there is a successful selection during a manual
|
||||
* autofill process.
|
||||
@@ -61,3 +97,10 @@ fun Intent.getAutofillAssistStructureOrNull(): AssistStructure? =
|
||||
*/
|
||||
fun Intent.getAutofillSelectionDataOrNull(): AutofillSelectionData? =
|
||||
this.getSafeParcelableExtra(AUTOFILL_SELECTION_DATA_KEY)
|
||||
|
||||
/**
|
||||
* Checks if the given [Intent] contains data for TOTP copying. The [AutofillTotpCopyData] will be
|
||||
* returned when present.
|
||||
*/
|
||||
fun Intent.getTotpCopyIntentOrNull(): AutofillTotpCopyData? =
|
||||
this.getSafeParcelableExtra(AUTOFILL_TOTP_COPY_DATA_KEY)
|
||||
|
||||
@@ -16,6 +16,7 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
val card = this@toAutofillCipherProvider.card ?: return emptyList()
|
||||
return listOf(
|
||||
AutofillCipher.Card(
|
||||
cipherId = id,
|
||||
name = name,
|
||||
subtitle = subtitle.orEmpty(),
|
||||
cardholderName = card.cardholderName.orEmpty(),
|
||||
@@ -33,6 +34,8 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
|
||||
val login = this@toAutofillCipherProvider.login ?: return emptyList()
|
||||
return listOf(
|
||||
AutofillCipher.Login(
|
||||
cipherId = id,
|
||||
isTotpEnabled = login.totp != null,
|
||||
name = name,
|
||||
password = login.password.orEmpty(),
|
||||
subtitle = subtitle.orEmpty(),
|
||||
|
||||
@@ -160,15 +160,3 @@ private fun Dataset.Builder.addVaultItemDataPreTiramisu(
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from an initial pending intent flag (ex: [PendingIntent.FLAG_CANCEL_CURRENT], derives
|
||||
* a new flag with the correct mutability determined by [isMutable].
|
||||
*/
|
||||
private fun Int.toPendingIntentMutabilityFlag(): Int =
|
||||
// Mutable flag was added on API level 31
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
this or PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.IntentSender
|
||||
import android.os.Build
|
||||
import android.service.autofill.Dataset
|
||||
import android.service.autofill.Presentations
|
||||
@@ -13,10 +14,11 @@ import com.x8bit.bitwarden.ui.autofill.util.createCipherInlinePresentationOrNull
|
||||
|
||||
/**
|
||||
* Build a [Dataset] to represent the [FilledPartition]. This dataset includes an overlay UI
|
||||
* presentation for each filled item.
|
||||
* presentation for each filled item. If an [authIntentSender] is present, add it to the dataset.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun FilledPartition.buildDataset(
|
||||
authIntentSender: IntentSender?,
|
||||
autofillAppInfo: AutofillAppInfo,
|
||||
): Dataset {
|
||||
val remoteViewsPlaceholder = buildAutofillRemoteViews(
|
||||
@@ -25,6 +27,11 @@ fun FilledPartition.buildDataset(
|
||||
)
|
||||
val datasetBuilder = Dataset.Builder()
|
||||
|
||||
authIntentSender
|
||||
?.let { intentSender ->
|
||||
datasetBuilder.setAuthentication(intentSender)
|
||||
}
|
||||
|
||||
if (autofillAppInfo.sdkInt >= Build.VERSION_CODES.TIRAMISU) {
|
||||
applyToDatasetPostTiramisu(
|
||||
autofillAppInfo = autofillAppInfo,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.x8bit.bitwarden.data.autofill.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
import android.text.InputType
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
|
||||
/**
|
||||
* Whether this [Int] is a password [InputType].
|
||||
@@ -29,3 +32,15 @@ val Int.isUsernameInputType: Boolean
|
||||
* Whether this [Int] contains [flag].
|
||||
*/
|
||||
private fun Int.hasFlag(flag: Int): Boolean = (this and flag) == flag
|
||||
|
||||
/**
|
||||
* Starting from an initial pending intent flag. (ex: [PendingIntent.FLAG_CANCEL_CURRENT])
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
fun Int.toPendingIntentMutabilityFlag(): Int =
|
||||
// Mutable flag was added on API level 31
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
this or PendingIntent.FLAG_MUTABLE
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.platform.util
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
/**
|
||||
* Launch a new coroutine that runs [block] and will safely timeout and invoke [timeoutBlock] after
|
||||
* a duration of length [timeoutDuration] in milliseconds is elapsed.
|
||||
*/
|
||||
fun CoroutineScope.launchWithTimeout(
|
||||
timeoutBlock: () -> Unit,
|
||||
timeoutDuration: Long,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
): Job =
|
||||
launch {
|
||||
try {
|
||||
withTimeout(timeoutDuration, block)
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
timeoutBlock()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user