Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.autofill.model.FilledData
import com.x8bit.bitwarden.data.autofill.model.FilledPartition
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.util.buildFilledItemOrNull
import com.x8bit.bitwarden.data.autofill.util.buildUri
import timber.log.Timber

/**
Expand Down Expand Up @@ -83,6 +84,7 @@ class FilledDataBuilderImpl(
autofillCipher = autofillCipher,
autofillViews = autofillRequest.partition.views,
inlinePresentationSpec = getCipherInlinePresentationOrNull(),
packageName = autofillRequest.packageName,
)
}
}
Expand All @@ -96,7 +98,9 @@ class FilledDataBuilderImpl(
?.getOrLastOrNull(inlineSuggestionsAdded)

return FilledData(
filledPartitions = filledPartitions.take(n = MAX_FILLED_PARTITIONS_COUNT),
filledPartitions = filledPartitions
.filter { it.filledItems.isNotEmpty() }
.take(n = MAX_FILLED_PARTITIONS_COUNT),
ignoreAutofillIds = autofillRequest.ignoreAutofillIds,
originalPartition = autofillRequest.partition,
uri = autofillRequest.uri,
Expand Down Expand Up @@ -140,16 +144,21 @@ class FilledDataBuilderImpl(
autofillCipher: AutofillCipher.Login,
autofillViews: List<AutofillView.Login>,
inlinePresentationSpec: InlinePresentationSpec?,
packageName: String?,
): FilledPartition {
val filledItems = autofillViews
.mapNotNull { autofillView ->
val value = when (autofillView) {
is AutofillView.Login.Username -> autofillCipher.username
is AutofillView.Login.Password -> autofillCipher.password
if (autofillView.data.website == autofillCipher.website ||
buildUri(packageName.orEmpty(), "androidapp") == autofillCipher.website
) {
val value = when (autofillView) {
is AutofillView.Login.Username -> autofillCipher.username
is AutofillView.Login.Password -> autofillCipher.password
}
autofillView.buildFilledItemOrNull(value = value)
} else {
null
}
autofillView.buildFilledItemOrNull(
value = value,
)
}

return FilledPartition(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ sealed class AutofillCipher {
override val subtitle: String,
val password: String,
val username: String,
val website: String,
) : AutofillCipher() {
override val iconRes: Int
@DrawableRes get() = BitwardenDrawable.ic_globe
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ sealed class AutofillView {
* @param isFocused Whether the view is currently focused.
* @param textValue A text value that represents the input present in the field.
* @param hasPasswordTerms Indicates that the field includes password terms.
* @param website website associated with this view.
*/
data class Data(
val autofillId: AutofillId,
Expand All @@ -24,6 +25,7 @@ sealed class AutofillView {
val isFocused: Boolean,
val textValue: String?,
val hasPasswordTerms: Boolean,
val website: String?,
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import android.view.autofill.AutofillId
* @param autofillViews The list of views we care about for autofilling.
* @param idPackage The package id for this view, if there is one.
* @param ignoreAutofillIds The list of [AutofillId]s that should be ignored in the fill response.
* @param website The website that is being displayed in the app, given there is one.
*/
data class ViewNodeTraversalData(
val autofillViews: List<AutofillView>,
val idPackage: String?,
val ignoreAutofillIds: List<AutofillId>,
val website: String?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,21 @@ class AutofillParserImpl(
.firstOrNull { it.data.isFocused }
?: autofillViews.firstOrNull()

if (focusedView == null) {
// The view is unfillable if there are no focused views.
return AutofillRequest.Unfillable
}

val packageName = traversalDataList.buildPackageNameOrNull(
assistStructure = assistStructure,
)
val uri = traversalDataList.buildUriOrNull(
val uri = focusedView.buildUriOrNull(
packageName = packageName,
)

val blockListedURIs = settingsRepository.blockedAutofillUris + BLOCK_LISTED_URIS
if (focusedView == null || blockListedURIs.contains(uri)) {
// The view is unfillable if there are no focused views or the URI is block listed.
if (blockListedURIs.contains(uri)) {
// The view is unfillable if the URI is block listed.
return AutofillRequest.Unfillable
}

Expand Down Expand Up @@ -165,7 +170,7 @@ private fun AssistStructure.traverse(): List<ViewNodeTraversalData> =
.mapNotNull { windowNode ->
windowNode
.rootViewNode
?.traverse()
?.traverse(parentWebsite = null)
?.updateForMissingPasswordFields()
?.updateForMissingUsernameFields()
}
Expand Down Expand Up @@ -243,24 +248,25 @@ private fun ViewNodeTraversalData.copyAndMapAutofillViews(
* Recursively traverse this [AssistStructure.ViewNode] and all of its descendants. Convert the
* data into [ViewNodeTraversalData].
*/
private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
private fun AssistStructure.ViewNode.traverse(
parentWebsite: String?,
): ViewNodeTraversalData {
// Set up mutable lists for collecting valid AutofillViews and ignorable view ids.
val mutableAutofillViewList: MutableList<AutofillView> = mutableListOf()
val mutableIgnoreAutofillIdList: MutableList<AutofillId> = mutableListOf()
var idPackage: String? = this.idPackage
var website: String? = this.website

// Try converting this `ViewNode` into an `AutofillView`. If a valid instance is returned, add
// it to the list. Otherwise, ignore the `AutofillId` associated with this `ViewNode`.
toAutofillView()
toAutofillView(parentWebsite = parentWebsite)
?.run(mutableAutofillViewList::add)
?: autofillId?.run(mutableIgnoreAutofillIdList::add)

// Recursively traverse all of this view node's children.
for (i in 0 until childCount) {
// Extract the traversal data from each child view node and add it to the lists.
getChildAt(i)
.traverse()
.traverse(parentWebsite = website)
.let { viewNodeTraversalData ->
viewNodeTraversalData.autofillViews.forEach(mutableAutofillViewList::add)
viewNodeTraversalData.ignoreAutofillIds.forEach(mutableIgnoreAutofillIdList::add)
Expand All @@ -273,10 +279,6 @@ private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
) {
idPackage = viewNodeTraversalData.idPackage
}
// Get the first non-null website.
if (website == null) {
website = viewNodeTraversalData.website
}
}
}

Expand All @@ -286,6 +288,5 @@ private fun AssistStructure.ViewNode.traverse(): ViewNodeTraversalData {
autofillViews = mutableAutofillViewList,
idPackage = idPackage,
ignoreAutofillIds = mutableIgnoreAutofillIdList,
website = website,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class AutofillCipherProviderImpl(
password = cipherView.login?.password.orEmpty(),
subtitle = cipherView.subtitle.orEmpty(),
username = cipherView.login?.username.orEmpty(),
website = uri,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import android.view.View
import android.view.autofill.AutofillValue
import com.x8bit.bitwarden.data.autofill.model.AutofillView
import com.x8bit.bitwarden.data.autofill.model.FilledItem
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull

/**
* The android app URI scheme. Example: androidapp://com.x8bit.bitwarden
*/
private const val ANDROID_APP_SCHEME: String = "androidapp"

/**
* Convert this [AutofillView] into a [FilledItem]. Return null if not possible.
*/
Expand Down Expand Up @@ -96,3 +102,17 @@ private fun AutofillView.buildListAutofillValueOrNull(
?.let { AutofillValue.forList(it) }
}
}

/**
* Try and build a URI. First, try building a website from the list of [ViewNodeTraversalData]. If
* that fails, try converting [packageName] into an Android app URI.
*/
fun AutofillView.buildUriOrNull(
packageName: String?,
): String? {
// Search list of ViewNodeTraversalData for a website URI.
this.data.website?.let { websiteUri -> return websiteUri }

// If the package name is available, build a URI out of that.
return packageName?.let { buildUri(domain = it, scheme = ANDROID_APP_SCHEME) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fun CipherView.toAutofillCipherProvider(): AutofillCipherProvider =
password = login.password.orEmpty(),
subtitle = subtitle.orEmpty(),
username = login.username.orEmpty(),
website = uri,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ private val AssistStructure.ViewNode.isInputField: Boolean
* doesn't contain a valid autofillId, it isn't an a view setup for autofill, so we return null. If
* it doesn't have a supported hint and isn't an input field, we also return null.
*/
fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
fun AssistStructure.ViewNode.toAutofillView(
parentWebsite: String?,
): AutofillView? =
this
.autofillId
// We only care about nodes with a valid `AutofillId`.
Expand All @@ -67,6 +69,7 @@ fun AssistStructure.ViewNode.toAutofillView(): AutofillView? =
isFocused = this.isFocused,
textValue = this.autofillValue?.extractTextValue(),
hasPasswordTerms = this.hasPasswordTerms(),
website = this.website ?: parentWebsite,
)
buildAutofillView(
autofillOptions = autofillOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,6 @@ import android.app.assist.AssistStructure
import com.bitwarden.ui.platform.base.util.orNullIfBlank
import com.x8bit.bitwarden.data.autofill.model.ViewNodeTraversalData

/**
* The android app URI scheme. Example: androidapp://com.x8bit.bitwarden
*/
private const val ANDROID_APP_SCHEME: String = "androidapp"

/**
* Try and build a URI. First, try building a website from the list of [ViewNodeTraversalData]. If
* that fails, try converting [packageName] into an Android app URI.
*/
fun List<ViewNodeTraversalData>.buildUriOrNull(
packageName: String?,
): String? {
// Search list of ViewNodeTraversalData for a website URI.
this
.firstOrNull { it.website != null }
?.website
?.let { websiteUri ->
return websiteUri
}

// If the package name is available, build a URI out of that.
return packageName
?.let { nonNullPackageName ->
buildUri(
domain = nonNullPackageName,
scheme = ANDROID_APP_SCHEME,
)
}
}

/**
* Try and build a package name. First, try searching traversal data for package names. If that
* fails, try extracting a package name from [assistStructure].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ class FillResponseBuilderTest {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = null,
),
),
),
Expand Down Expand Up @@ -246,6 +247,7 @@ class FillResponseBuilderTest {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = null,
),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,20 @@ class FilledDataBuilderTest {
password = password,
username = username,
subtitle = "Subtitle",
website = URI,
)
val filledItemPassword: FilledItem = mockk()
val filledItemUsername: FilledItem = mockk()
val autofillViewPassword: AutofillView.Login.Password = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(password) } returns filledItemPassword
}
val autofillViewUsernameOne: AutofillView.Login.Username = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(username) } returns filledItemUsername
}
val autofillViewUsernameTwo: AutofillView.Login.Username = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(username) } returns null
}
val autofillPartition = AutofillPartition.Login(
Expand Down Expand Up @@ -341,15 +345,8 @@ class FilledDataBuilderTest {
partition = autofillPartition,
uri = URI,
)
val filledPartition = FilledPartition(
autofillCipher = autofillCipher,
filledItems = emptyList(),
inlinePresentationSpec = null,
)
val expected = FilledData(
filledPartitions = listOf(
filledPartition,
),
filledPartitions = emptyList(),
ignoreAutofillIds = ignoreAutofillIds,
originalPartition = autofillPartition,
uri = URI,
Expand Down Expand Up @@ -396,14 +393,17 @@ class FilledDataBuilderTest {
password = password,
username = username,
subtitle = "Subtitle",
website = URI,
)

val filledItemPassword: FilledItem = mockk()
val filledItemUsername: FilledItem = mockk()
val autofillViewPassword: AutofillView.Login.Password = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(password) } returns filledItemPassword
}
val autofillViewUsername: AutofillView.Login.Username = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(username) } returns filledItemUsername
}
val autofillPartition = AutofillPartition.Login(
Expand Down Expand Up @@ -490,13 +490,16 @@ class FilledDataBuilderTest {
password = password,
username = username,
subtitle = "Subtitle",
website = URI,
)
val filledItemPassword: FilledItem = mockk()
val filledItemUsername: FilledItem = mockk()
val autofillViewPassword: AutofillView.Login.Password = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(password) } returns filledItemPassword
}
val autofillViewUsername: AutofillView.Login.Username = mockk {
every { data } returns mockk { every { website } returns URI }
every { buildFilledItemOrNull(username) } returns filledItemUsername
}
val autofillPartition = AutofillPartition.Login(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class SaveInfoBuilderTest {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = null,
)
private val autofillIdValid: AutofillId = mockk()
private val autofillViewDataValid = AutofillView.Data(
Expand All @@ -44,6 +45,7 @@ class SaveInfoBuilderTest {
isFocused = true,
textValue = null,
hasPasswordTerms = false,
website = null,
)
private val autofillPartitionCard: AutofillPartition.Card = AutofillPartition.Card(
views = listOf(
Expand Down
Loading
Loading