Improve Autofill UI and enable inline presentations (#1181)

* Improve Autofill UI and enable inline presentations

Improves the Autofill UI in the following ways:

* Add support for Android 11 inline presentations of Autofill datasets.
* Instead of showing the identifier (app name or web origin) of the
current app on top of every Autofill dataset, it is now shown 1) as a
header dataset on Android 9 and 10 as well as 2) at the top of the
search activity on all supported versions of Android. Rationale: The
identifier is only used in trust decisions when choosing an existing
entry to fill and should feature prominently in that view, not
elsewhere.
* Show the actual identifier part of a matched entry's path, which may
differ from the identifier of the matched app/website.
* Slightly tweak the labels of Search/Generate Autofill actions to
indicate that a) this is about entries and b) the user may skip the
generation of a password and supply a custom one as well.

* Suppress lint error

* Address review comments

* Add a fixme about properly handling fill-in datasets

* CHANGELOG: add entry for inline presentation

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>

* Remove unused parameter

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>

Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Fabian Henneke 2020-11-02 20:25:37 +01:00 committed by GitHub
parent cff8d41c91
commit 1d13a1fbd6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 211 additions and 107 deletions

View file

@ -17,6 +17,7 @@
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View file

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
### Added
- On Android 11, Autofill will use the new [inline autofill](https://developer.android.com/guide/topics/text/ime-autofill#configure-provider) UI that integrates Autofill results into your keyboard app.
### Fixed ### Fixed
- Cancelling the Autofill "Generate password" action now correctly returns you to the original app. - Cancelling the Autofill "Generate password" action now correctly returns you to the original app.

View file

@ -89,6 +89,7 @@ dependencies {
compileOnly(Dependencies.AndroidX.annotation) compileOnly(Dependencies.AndroidX.annotation)
implementation(Dependencies.AndroidX.activity_ktx) implementation(Dependencies.AndroidX.activity_ktx)
implementation(Dependencies.AndroidX.appcompat) implementation(Dependencies.AndroidX.appcompat)
implementation(Dependencies.AndroidX.autofill)
implementation(Dependencies.AndroidX.biometric) implementation(Dependencies.AndroidX.biometric)
implementation(Dependencies.AndroidX.constraint_layout) implementation(Dependencies.AndroidX.constraint_layout)
implementation(Dependencies.AndroidX.core_ktx) implementation(Dependencies.AndroidX.core_ktx)

View file

@ -12,7 +12,8 @@ import android.service.autofill.Dataset
import android.service.autofill.FillCallback import android.service.autofill.FillCallback
import android.service.autofill.FillResponse import android.service.autofill.FillResponse
import android.service.autofill.SaveInfo import android.service.autofill.SaveInfo
import android.widget.RemoteViews import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.e import com.github.ajalt.timberkt.e
import com.github.androidpasswordstore.autofillparser.AutofillAction import com.github.androidpasswordstore.autofillparser.AutofillAction
@ -41,70 +42,85 @@ class AutofillResponseBuilder(form: FillableForm) {
scenario.fieldsToSave.minus(listOfNotNull(scenario.username)).isNotEmpty() scenario.fieldsToSave.minus(listOfNotNull(scenario.username)).isNotEmpty()
private val canBeSaved = saveFlags != null && scenarioSupportsSave private val canBeSaved = saveFlags != null && scenarioSupportsSave
private fun makePlaceholderDataset( private fun makeIntentDataset(
remoteView: RemoteViews, context: Context,
action: AutofillAction,
intentSender: IntentSender, intentSender: IntentSender,
action: AutofillAction metadata: DatasetMetadata,
imeSpec: InlinePresentationSpec?,
): Dataset { ): Dataset {
return Dataset.Builder(remoteView).run { return Dataset.Builder(makeRemoteView(context, metadata)).run {
fillWith(scenario, action, credentials = null) fillWith(scenario, action, credentials = null)
setAuthentication(intentSender) setAuthentication(intentSender)
if (imeSpec != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val inlinePresentation = makeInlinePresentation(context, imeSpec, metadata)
if (inlinePresentation != null) {
setInlinePresentation(inlinePresentation)
}
}
build() build()
} }
} }
private fun makeMatchDataset(context: Context, file: File): Dataset? { private fun makeMatchDataset(context: Context, file: File, imeSpec: InlinePresentationSpec?): Dataset? {
if (scenario.fieldsToFillOn(AutofillAction.Match).isEmpty()) return null if (scenario.fieldsToFillOn(AutofillAction.Match).isEmpty()) return null
val remoteView = makeFillMatchRemoteView(context, file, formOrigin) val metadata = makeFillMatchMetadata(context, file)
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context) val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Match) return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec)
} }
private fun makeSearchDataset(context: Context): Dataset? { private fun makeSearchDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? {
if (scenario.fieldsToFillOn(AutofillAction.Search).isEmpty()) return null if (scenario.fieldsToFillOn(AutofillAction.Search).isEmpty()) return null
val remoteView = makeSearchAndFillRemoteView(context, formOrigin) val metadata = makeSearchAndFillMetadata(context)
val intentSender = val intentSender =
AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin) AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin)
return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Search) return makeIntentDataset(context, AutofillAction.Search, intentSender, metadata, imeSpec)
} }
private fun makeGenerateDataset(context: Context): Dataset? { private fun makeGenerateDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? {
if (scenario.fieldsToFillOn(AutofillAction.Generate).isEmpty()) return null if (scenario.fieldsToFillOn(AutofillAction.Generate).isEmpty()) return null
val remoteView = makeGenerateAndFillRemoteView(context, formOrigin) val metadata = makeGenerateAndFillMetadata(context)
val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin) val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin)
return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Generate) return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata, imeSpec)
} }
private fun makeFillOtpFromSmsDataset(context: Context): Dataset? { private fun makeFillOtpFromSmsDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? {
if (scenario.fieldsToFillOn(AutofillAction.FillOtpFromSms).isEmpty()) return null if (scenario.fieldsToFillOn(AutofillAction.FillOtpFromSms).isEmpty()) return null
if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
val remoteView = makeFillOtpFromSmsRemoteView(context, formOrigin) val metadata = makeFillOtpFromSmsMetadata(context)
val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context) val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context)
return makePlaceholderDataset(remoteView, intentSender, AutofillAction.FillOtpFromSms) return makeIntentDataset(context, AutofillAction.FillOtpFromSms, intentSender, metadata, imeSpec)
} }
private fun makePublisherChangedDataset( private fun makePublisherChangedDataset(
context: Context, context: Context,
publisherChangedException: AutofillPublisherChangedException publisherChangedException: AutofillPublisherChangedException,
imeSpec: InlinePresentationSpec?
): Dataset { ): Dataset {
val remoteView = makeWarningRemoteView(context) val metadata = makeWarningMetadata(context)
// If the user decides to trust the new publisher, they can choose reset the list of // If the user decides to trust the new publisher, they can choose reset the list of
// matches. In this case we need to immediately show a new `FillResponse` as if the app were // matches. In this case we need to immediately show a new `FillResponse` as if the app were
// autofilled for the first time. This `FillResponse` needs to be returned as a result from // autofilled for the first time. This `FillResponse` needs to be returned as a result from
// `AutofillPublisherChangedActivity`, which is why we create and pass it on here. // `AutofillPublisherChangedActivity`, which is why we create and pass it on here.
val fillResponseAfterReset = makeFillResponse(context, emptyList()) val fillResponseAfterReset = makeFillResponse(context, null, emptyList())
val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender( val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender(
context, publisherChangedException, fillResponseAfterReset context, publisherChangedException, fillResponseAfterReset
) )
return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Match) return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec)
} }
private fun makePublisherChangedResponse( private fun makePublisherChangedResponse(
context: Context, context: Context,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
publisherChangedException: AutofillPublisherChangedException publisherChangedException: AutofillPublisherChangedException
): FillResponse { ): FillResponse {
val imeSpec = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.inlinePresentationSpecs?.firstOrNull()
} else {
null
}
return FillResponse.Builder().run { return FillResponse.Builder().run {
addDataset(makePublisherChangedDataset(context, publisherChangedException)) addDataset(makePublisherChangedDataset(context, publisherChangedException, imeSpec))
setIgnoredIds(*ignoredIds.toTypedArray()) setIgnoredIds(*ignoredIds.toTypedArray())
build() build()
} }
@ -127,28 +143,36 @@ class AutofillResponseBuilder(form: FillableForm) {
} }
} }
private fun makeFillResponse(context: Context, matchedFiles: List<File>): FillResponse? { private fun makeFillResponse(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, matchedFiles: List<File>): FillResponse? {
var hasDataset = false var datasetCount = 0
val imeSpecs: List<InlinePresentationSpec> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
inlineSuggestionsRequest?.inlinePresentationSpecs
} else {
null
} ?: emptyList()
return FillResponse.Builder().run { return FillResponse.Builder().run {
for (file in matchedFiles) { for (file in matchedFiles) {
makeMatchDataset(context, file)?.let { makeMatchDataset(context, file, imeSpecs.getOrNull(datasetCount))?.let {
hasDataset = true datasetCount++
addDataset(it) addDataset(it)
} }
} }
makeSearchDataset(context)?.let { makeSearchDataset(context, imeSpecs.getOrNull(datasetCount))?.let {
hasDataset = true datasetCount++
addDataset(it) addDataset(it)
} }
makeGenerateDataset(context)?.let { makeGenerateDataset(context, imeSpecs.getOrNull(datasetCount))?.let {
hasDataset = true datasetCount++
addDataset(it) addDataset(it)
} }
makeFillOtpFromSmsDataset(context)?.let { makeFillOtpFromSmsDataset(context, imeSpecs.getOrNull(datasetCount))?.let {
hasDataset = true datasetCount++
addDataset(it) addDataset(it)
} }
if (!hasDataset) return null if (datasetCount == 0) return null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setHeader(makeRemoteView(context, makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true))))
}
makeSaveInfo()?.let { setSaveInfo(it) } makeSaveInfo()?.let { setSaveInfo(it) }
setClientState(clientState) setClientState(clientState)
setIgnoredIds(*ignoredIds.toTypedArray()) setIgnoredIds(*ignoredIds.toTypedArray())
@ -159,14 +183,14 @@ class AutofillResponseBuilder(form: FillableForm) {
/** /**
* Creates and returns a suitable [FillResponse] to the Autofill framework. * Creates and returns a suitable [FillResponse] to the Autofill framework.
*/ */
fun fillCredentials(context: Context, callback: FillCallback) { fun fillCredentials(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) {
AutofillMatcher.getMatchesFor(context, formOrigin).fold( AutofillMatcher.getMatchesFor(context, formOrigin).fold(
success = { matchedFiles -> success = { matchedFiles ->
callback.onSuccess(makeFillResponse(context, matchedFiles)) callback.onSuccess(makeFillResponse(context, inlineSuggestionsRequest, matchedFiles))
}, },
failure = { e -> failure = { e ->
e(e) e(e)
callback.onSuccess(makePublisherChangedResponse(context, e)) callback.onSuccess(makePublisherChangedResponse(context, inlineSuggestionsRequest, e))
} }
) )
} }
@ -178,14 +202,17 @@ class AutofillResponseBuilder(form: FillableForm) {
clientState: Bundle, clientState: Bundle,
action: AutofillAction action: AutofillAction
): Dataset { ): Dataset {
val remoteView = makePlaceholderRemoteView(context)
val scenario = AutofillScenario.fromBundle(clientState) val scenario = AutofillScenario.fromBundle(clientState)
// Before Android P, Datasets used for fill-in had to come with a RemoteViews, even // Before Android P, Datasets used for fill-in had to come with a RemoteViews, even
// though they are never shown. // though they are rarely shown.
// FIXME: We should clone the original dataset here and add the credentials to be filled
// in. Otherwise, the entry in the cached list of datasets will be overwritten by the
// fill-in dataset without any visual representation. This causes it to be missing from
// the Autofill suggestions shown after the user clears the filled out form fields.
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Dataset.Builder() Dataset.Builder()
} else { } else {
Dataset.Builder(remoteView) Dataset.Builder(makeRemoteView(context, makeEmptyMetadata()))
} }
return builder.run { return builder.run {
if (scenario != null) fillWith(scenario, action, credentials) if (scenario != null) fillWith(scenario, action, credentials)

View file

@ -4,64 +4,110 @@
*/ */
package com.zeapo.pwdstore.autofill.oreo package com.zeapo.pwdstore.autofill.oreo
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.drawable.Icon
import android.os.Build
import android.service.autofill.InlinePresentation
import android.view.View
import android.widget.RemoteViews import android.widget.RemoteViews
import com.github.androidpasswordstore.autofillparser.FormOrigin import android.widget.inline.InlinePresentationSpec
import androidx.annotation.DrawableRes
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import com.zeapo.pwdstore.PasswordStore
import com.zeapo.pwdstore.R import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.utils.PasswordRepository import com.zeapo.pwdstore.utils.PasswordRepository
import java.io.File import java.io.File
private fun makeRemoteView( data class DatasetMetadata(val title: String, val subtitle: String?, @DrawableRes val iconRes: Int)
context: Context,
title: String, fun makeRemoteView(context: Context, metadata: DatasetMetadata): RemoteViews {
summary: String,
iconRes: Int
): RemoteViews {
return RemoteViews(context.packageName, R.layout.oreo_autofill_dataset).apply { return RemoteViews(context.packageName, R.layout.oreo_autofill_dataset).apply {
setTextViewText(R.id.title, title) setTextViewText(R.id.title, metadata.title)
setTextViewText(R.id.summary, summary) if (metadata.subtitle != null) {
setImageViewResource(R.id.icon, iconRes) setTextViewText(R.id.summary, metadata.subtitle)
} else {
setViewVisibility(R.id.summary, View.GONE)
}
if (metadata.iconRes != Resources.ID_NULL) {
setImageViewResource(R.id.icon, metadata.iconRes)
} else {
setViewVisibility(R.id.icon, View.GONE)
}
} }
} }
fun makeFillMatchRemoteView(context: Context, file: File, formOrigin: FormOrigin): RemoteViews { @SuppressLint("RestrictedApi")
val title = formOrigin.getPrettyIdentifier(context, untrusted = false) fun makeInlinePresentation(context: Context, imeSpec: InlinePresentationSpec, metadata: DatasetMetadata): InlinePresentation? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
return null
if (UiVersions.INLINE_UI_VERSION_1 !in UiVersions.getVersions(imeSpec.style))
return null
val launchIntent = PendingIntent.getActivity(context, 0, Intent(context, PasswordStore::class.java), 0)
val slice = InlineSuggestionUi.newContentBuilder(launchIntent).run {
setTitle(metadata.title)
if (metadata.subtitle != null)
setSubtitle(metadata.subtitle)
setContentDescription(if (metadata.subtitle != null) "${metadata.title} - ${metadata.subtitle}" else metadata.title)
setStartIcon(Icon.createWithResource(context, metadata.iconRes))
build().slice
}
return InlinePresentation(slice, imeSpec, false)
}
fun makeFillMatchMetadata(context: Context, file: File): DatasetMetadata {
val directoryStructure = AutofillPreferences.directoryStructure(context) val directoryStructure = AutofillPreferences.directoryStructure(context)
val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory()) val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory())
val summary = directoryStructure.getUsernameFor(relativeFile) val title = directoryStructure.getIdentifierFor(relativeFile)
?: directoryStructure.getPathToIdentifierFor(relativeFile) ?: "" ?: directoryStructure.getAccountPartFor(relativeFile)!!
val iconRes = R.drawable.ic_person_black_24dp val subtitle = directoryStructure.getAccountPartFor(relativeFile)
return makeRemoteView(context, title, summary, iconRes) return DatasetMetadata(
title,
subtitle,
R.drawable.ic_person_black_24dp
)
} }
fun makeSearchAndFillRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews { fun makeSearchAndFillMetadata(context: Context) = DatasetMetadata(
val title = formOrigin.getPrettyIdentifier(context, untrusted = true) context.getString(R.string.oreo_autofill_search_in_store),
val summary = context.getString(R.string.oreo_autofill_search_in_store) null,
val iconRes = R.drawable.ic_search_black_24dp R.drawable.ic_search_black_24dp
return makeRemoteView(context, title, summary, iconRes) )
}
fun makeGenerateAndFillRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews { fun makeGenerateAndFillMetadata(context: Context) = DatasetMetadata(
val title = formOrigin.getPrettyIdentifier(context, untrusted = true) context.getString(R.string.oreo_autofill_generate_password),
val summary = context.getString(R.string.oreo_autofill_generate_password) null,
val iconRes = R.drawable.ic_autofill_new_password R.drawable.ic_autofill_new_password
return makeRemoteView(context, title, summary, iconRes) )
}
fun makeFillOtpFromSmsRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews { fun makeFillOtpFromSmsMetadata(context: Context) = DatasetMetadata(
val title = formOrigin.getPrettyIdentifier(context, untrusted = true) context.getString(R.string.oreo_autofill_fill_otp_from_sms),
val summary = context.getString(R.string.oreo_autofill_fill_otp_from_sms) null,
val iconRes = R.drawable.ic_autofill_sms R.drawable.ic_autofill_sms
return makeRemoteView(context, title, summary, iconRes) )
}
fun makePlaceholderRemoteView(context: Context): RemoteViews { fun makeEmptyMetadata() = DatasetMetadata(
return makeRemoteView(context, "PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher) "PLACEHOLDER",
} "PLACEHOLDER",
R.mipmap.ic_launcher
)
fun makeWarningRemoteView(context: Context): RemoteViews { fun makeWarningMetadata(context: Context) = DatasetMetadata(
val title = context.getString(R.string.oreo_autofill_warning_publisher_dataset_title) context.getString(R.string.oreo_autofill_warning_publisher_dataset_title),
val summary = context.getString(R.string.oreo_autofill_warning_publisher_dataset_summary) context.getString(R.string.oreo_autofill_warning_publisher_dataset_summary),
val iconRes = R.drawable.ic_warning_red_24dp R.drawable.ic_warning_red_24dp
return makeRemoteView(context, title, summary, iconRes) )
}
fun makeHeaderMetadata(title: String) = DatasetMetadata(
title,
null,
0
)

View file

@ -86,7 +86,13 @@ class OreoAutofillService : AutofillService() {
callback.onSuccess(null) callback.onSuccess(null)
return return
} }
AutofillResponseBuilder(formToFill).fillCredentials(this, callback) val inlineSuggestionsRequest =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
request.inlineSuggestionsRequest
} else {
null
}
AutofillResponseBuilder(formToFill).fillCredentials(this, inlineSuggestionsRequest, callback)
} }
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {

View file

@ -162,6 +162,13 @@ class AutofillFilterView : AppCompatActivity() {
setText(initialSearch, TextView.BufferType.EDITABLE) setText(initialSearch, TextView.BufferType.EDITABLE)
addTextChangedListener { updateSearch() } addTextChangedListener { updateSearch() }
} }
origin.text = buildSpannedString {
append(getString(R.string.oreo_autofill_select_and_fill_into))
append("\n")
bold {
append(formOrigin.getPrettyIdentifier(applicationContext, untrusted = true))
}
}
strictDomainSearch.apply { strictDomainSearch.apply {
visibility = if (formOrigin is FormOrigin.Web) View.VISIBLE else View.GONE visibility = if (formOrigin is FormOrigin.Web) View.VISIBLE else View.GONE
isChecked = formOrigin is FormOrigin.Web isChecked = formOrigin is FormOrigin.Web

View file

@ -14,16 +14,28 @@
<ImageView <ImageView
android:id="@+id/cover" android:id="@+id/cover"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="80dp" android:layout_height="60dp"
android:background="@color/primary_color" android:background="@color/primary_color"
android:contentDescription="@string/app_name" android:contentDescription="@string/app_name"
android:src="@mipmap/ic_launcher_foreground" android:src="@mipmap/ic_launcher_foreground"
app:layout_constraintBottom_toTopOf="@id/searchLayout" app:layout_constraintBottom_toTopOf="@id/origin"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" /> app:layout_constraintVertical_bias="0.0" />
<TextView
android:id="@+id/origin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="false"
android:textAlignment="center"
app:layout_constraintBottom_toTopOf="@id/searchLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cover"
app:layout_constraintVertical_bias="0.0" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/searchLayout" android:id="@+id/searchLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -35,7 +47,7 @@
app:layout_constraintBottom_toTopOf="@id/rvPasswordSwitcher" app:layout_constraintBottom_toTopOf="@id/rvPasswordSwitcher"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cover"> app:layout_constraintTop_toBottomOf="@id/origin">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/search" android:id="@+id/search"

View file

@ -6,9 +6,10 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingLeft="10dp" android:paddingLeft="10dp"
android:paddingTop="5dp" android:paddingTop="5dp"
android:paddingRight="10dp" android:paddingRight="10dp"
@ -20,6 +21,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:maxWidth="20dp" android:maxWidth="20dp"
android:maxHeight="20dp" android:maxHeight="20dp"

View file

@ -157,12 +157,15 @@
<string name="send_plaintext_password_to">Passwort unverschlüsselt senden an…</string> <string name="send_plaintext_password_to">Passwort unverschlüsselt senden an…</string>
<string name="app_icon_hint">App Icon</string> <string name="app_icon_hint">App Icon</string>
<!-- Oreo Autofill --> <!-- Oreo Autofill -->
<string name="oreo_autofill_select_and_fill_into">Eintrag auswählen für Autofill in</string>
<string name="oreo_autofill_strict_domain_search">Phishing-resistente Suche</string> <string name="oreo_autofill_strict_domain_search">Phishing-resistente Suche</string>
<string name="oreo_autofill_filter_no_results">Keine Ergebnisse.</string> <string name="oreo_autofill_filter_no_results">Keine Ergebnisse.</string>
<string name="oreo_autofill_save_internal_error">Speichern aufgrund eines internen Fehlers fehlgeschlagen</string> <string name="oreo_autofill_save_internal_error">Speichern aufgrund eines internen Fehlers fehlgeschlagen</string>
<string name="oreo_autofill_save_app_not_supported">Diese App wird derzeit nicht unterstützt</string> <string name="oreo_autofill_save_app_not_supported">Diese App wird derzeit nicht unterstützt</string>
<string name="oreo_autofill_save_passwords_dont_match">Die Passwörter stimmen nicht überein</string> <string name="oreo_autofill_save_passwords_dont_match">Die Passwörter stimmen nicht überein</string>
<string name="oreo_autofill_generate_password">Passwort generieren…</string> <string name="oreo_autofill_generate_password">Eintrag erstellen</string>
<string name="oreo_autofill_search_in_store">Eintrag suchen</string>
<string name="oreo_autofill_fill_otp_from_sms">Code aus SMS einfügen</string>
<string name="oreo_autofill_warning_publisher_footer"><b>Die derzeit installierte App versucht, Ihre Anmeldeinformationen zu stehlen, indem sie vorgibt, eine vertrauenswürdige App zu sein.</b>\n\nVersuchen Sie die App zu deinstallieren und installieren Sie sie erneut aus einer vertrauenswürdigen Quelle wie dem Play Store, Amazon Appstore, F-Droid oder dem Shop Ihres Telefonherstellers.</string> <string name="oreo_autofill_warning_publisher_footer"><b>Die derzeit installierte App versucht, Ihre Anmeldeinformationen zu stehlen, indem sie vorgibt, eine vertrauenswürdige App zu sein.</b>\n\nVersuchen Sie die App zu deinstallieren und installieren Sie sie erneut aus einer vertrauenswürdigen Quelle wie dem Play Store, Amazon Appstore, F-Droid oder dem Shop Ihres Telefonherstellers.</string>
<string name="oreo_autofill_warning_publisher_install_time">Installiert: %1$s</string> <string name="oreo_autofill_warning_publisher_install_time">Installiert: %1$s</string>
<string name="oreo_autofill_warning_publisher_warning_sign_description">Warnung</string> <string name="oreo_autofill_warning_publisher_warning_sign_description">Warnung</string>

View file

@ -130,11 +130,9 @@
<!-- Oreo Autofill --> <!-- Oreo Autofill -->
<string name="oreo_autofill_match_with">Coincide con %1$s</string> <string name="oreo_autofill_match_with">Coincide con %1$s</string>
<string name="oreo_autofill_filter_no_results">Sin resultados.</string> <string name="oreo_autofill_filter_no_results">Sin resultados.</string>
<string name="oreo_autofill_search_in_store">Buscar en la tienda…</string>
<string name="oreo_autofill_save_internal_error">Error al guardar debido a un error interno</string> <string name="oreo_autofill_save_internal_error">Error al guardar debido a un error interno</string>
<string name="oreo_autofill_save_app_not_supported">Esta aplicación no es compatible actualmente</string> <string name="oreo_autofill_save_app_not_supported">Esta aplicación no es compatible actualmente</string>
<string name="oreo_autofill_save_passwords_dont_match">Las contraseñas no coinciden</string> <string name="oreo_autofill_save_passwords_dont_match">Las contraseñas no coinciden</string>
<string name="oreo_autofill_generate_password">Generar contraseña…</string>
<string name="oreo_autofill_warning_publisher_install_time">Instalada: %1$s</string> <string name="oreo_autofill_warning_publisher_install_time">Instalada: %1$s</string>
<string name="oreo_autofill_warning_publisher_advanced_info_button">Información avanzada</string> <string name="oreo_autofill_warning_publisher_advanced_info_button">Información avanzada</string>
<string name="oreo_autofill_warning_publisher_changed_disable_autofill_button">Mantener el Autocompletado desactivado</string> <string name="oreo_autofill_warning_publisher_changed_disable_autofill_button">Mantener el Autocompletado desactivado</string>

View file

@ -185,17 +185,15 @@
<string name="oreo_autofill_match_with">Apparier avec %1$s</string> <string name="oreo_autofill_match_with">Apparier avec %1$s</string>
<string name="oreo_autofill_matches_clear_existing">Effacer lappairage actuel</string> <string name="oreo_autofill_matches_clear_existing">Effacer lappairage actuel</string>
<string name="oreo_autofill_filter_no_results">Aucun résultat.</string> <string name="oreo_autofill_filter_no_results">Aucun résultat.</string>
<string name="oreo_autofill_search_in_store">Rechercher dans le dépôt…</string>
<string name="oreo_autofill_save_internal_error">Échec de la sauvegarde : erreur interne</string> <string name="oreo_autofill_save_internal_error">Échec de la sauvegarde : erreur interne</string>
<string name="oreo_autofill_save_app_not_supported">Cette application n\'est actuellement pas prise en charge</string> <string name="oreo_autofill_save_app_not_supported">Cette application n\'est actuellement pas prise en charge</string>
<string name="oreo_autofill_save_passwords_dont_match">Les mots de passe ne coïncident pas</string> <string name="oreo_autofill_save_passwords_dont_match">Les mots de passe ne coïncident pas</string>
<string name="oreo_autofill_generate_password">Générer un mot de passe…</string> <string name="oreo_autofill_fill_otp_from_sms">Extraire le code depuis un SMS</string>
<string name="oreo_autofill_fill_otp_from_sms">Extraire le code depuis un SMS…</string>
<string name="oreo_autofill_warning_publisher_header">L\'éditeur de cette application a changé depuis que vous avez appairé un mot de passe avec celle-ci:</string> <string name="oreo_autofill_warning_publisher_header">L\'éditeur de cette application a changé depuis que vous avez appairé un mot de passe avec celle-ci:</string>
<string name="oreo_autofill_warning_publisher_footer"><b>L\'application actuellement installée peut essayer de voler vos identifiants en faisant semblant d\'être une application de confiance.</b>\n\nEssayez de désinstaller et de réinstaller l\'application à partir d\'une source fiable, comme le Play Store, l\'AppStore d\'Amazon, le F-Droid ou la boutique du fabricant de votre téléphone.</string> <string name="oreo_autofill_warning_publisher_footer"><b>L\'application actuellement installée peut essayer de voler vos identifiants en faisant semblant d\'être une application de confiance.</b>\n\nEssayez de désinstaller et de réinstaller l\'application à partir d\'une source fiable, comme le Play Store, l\'AppStore d\'Amazon, le F-Droid ou la boutique du fabricant de votre téléphone.</string>
<string name="oreo_autofill_warning_publisher_install_time">Installé : %1$s</string> <string name="oreo_autofill_warning_publisher_install_time">Installé : %1$s</string>
<string name="oreo_autofill_warning_publisher_warning_sign_description">Avertissement</string> <string name="oreo_autofill_warning_publisher_warning_sign_description">Avertissement</string>
<string name="oreo_autofill_warning_publisher_dataset_summary">Appuyez pour en savoir plus</string> <string name="oreo_autofill_warning_publisher_dataset_summary">Appuyez pour en savoir plus</string>
<string name="oreo_autofill_warning_publisher_dataset_title">Tentative possible d\'hameçonnage</string> <string name="oreo_autofill_warning_publisher_dataset_title">Tentative possible d\'hameçonnage</string>
<string name="oreo_autofill_general_fill_and_save_support">Remplir et enregistrer les identifiants</string> <string name="oreo_autofill_general_fill_and_save_support">Remplir et enregistrer les identifiants</string>
<string name="oreo_autofill_general_fill_support">Remplir les identifiants</string> <string name="oreo_autofill_general_fill_support">Remplir les identifiants</string>

View file

@ -187,12 +187,10 @@
<string name="oreo_autofill_match_with">Combinar com %1$s</string> <string name="oreo_autofill_match_with">Combinar com %1$s</string>
<string name="oreo_autofill_matches_clear_existing">Limpar correspondências existentes</string> <string name="oreo_autofill_matches_clear_existing">Limpar correspondências existentes</string>
<string name="oreo_autofill_filter_no_results">Sem resultados.</string> <string name="oreo_autofill_filter_no_results">Sem resultados.</string>
<string name="oreo_autofill_search_in_store">Pesquisar no armazenamento…</string>
<string name="oreo_autofill_save_internal_error">Falha ao salvar devido a um erro interno</string> <string name="oreo_autofill_save_internal_error">Falha ao salvar devido a um erro interno</string>
<string name="oreo_autofill_save_app_not_supported">Este app não é suportado no momento</string> <string name="oreo_autofill_save_app_not_supported">Este app não é suportado no momento</string>
<string name="oreo_autofill_save_passwords_dont_match">As senhas não coincidem</string> <string name="oreo_autofill_save_passwords_dont_match">As senhas não coincidem</string>
<string name="oreo_autofill_generate_password">Gerar senha…</string> <string name="oreo_autofill_fill_otp_from_sms">Extrair código do SMS</string>
<string name="oreo_autofill_fill_otp_from_sms">Extrair código do SMS…</string>
<string name="oreo_autofill_max_matches_reached">Número máximo de correspondências (%1$d) atingidas; limpar correspondências antes de adicionar novas.</string> <string name="oreo_autofill_max_matches_reached">Número máximo de correspondências (%1$d) atingidas; limpar correspondências antes de adicionar novas.</string>
<string name="oreo_autofill_warning_publisher_header">O editor deste aplicativo mudou desde a primeira vez que você associou uma entrada no Password Store:</string> <string name="oreo_autofill_warning_publisher_header">O editor deste aplicativo mudou desde a primeira vez que você associou uma entrada no Password Store:</string>
<string name="oreo_autofill_warning_publisher_footer"><b>O aplicativo atualmente instalado pode estar tentando roubar suas credenciais fingindo ser um aplicativo confiável.</b>\n\nTente desinstalar e reinstalar o aplicativo de uma fonte confiável, como a Play Store, Amazon Appstore, F-Droid ou a loja do fabricante do seu telefone.</string> <string name="oreo_autofill_warning_publisher_footer"><b>O aplicativo atualmente instalado pode estar tentando roubar suas credenciais fingindo ser um aplicativo confiável.</b>\n\nTente desinstalar e reinstalar o aplicativo de uma fonte confiável, como a Play Store, Amazon Appstore, F-Droid ou a loja do fabricante do seu telefone.</string>
@ -201,7 +199,7 @@
<string name="oreo_autofill_warning_publisher_changed_disable_autofill_button">Manter o preenchimento automático desativado</string> <string name="oreo_autofill_warning_publisher_changed_disable_autofill_button">Manter o preenchimento automático desativado</string>
<string name="oreo_autofill_warning_publisher_reenable_button">Reativar preenchimento automático</string> <string name="oreo_autofill_warning_publisher_reenable_button">Reativar preenchimento automático</string>
<string name="oreo_autofill_warning_publisher_warning_sign_description">Alerta</string> <string name="oreo_autofill_warning_publisher_warning_sign_description">Alerta</string>
<string name="oreo_autofill_warning_publisher_dataset_summary">Toque para detalhes</string> <string name="oreo_autofill_warning_publisher_dataset_summary">Toque para detalhes</string>
<string name="oreo_autofill_warning_publisher_dataset_title">Possível tentativa de phishing</string> <string name="oreo_autofill_warning_publisher_dataset_title">Possível tentativa de phishing</string>
<string name="oreo_autofill_general_fill_and_save_support">Preencher e salvar credenciais</string> <string name="oreo_autofill_general_fill_and_save_support">Preencher e salvar credenciais</string>
<string name="oreo_autofill_general_fill_support">Preencher as credenciais</string> <string name="oreo_autofill_general_fill_support">Preencher as credenciais</string>

View file

@ -191,12 +191,10 @@
<string name="oreo_autofill_match_with">Совпадает с %1$s</string> <string name="oreo_autofill_match_with">Совпадает с %1$s</string>
<string name="oreo_autofill_matches_clear_existing">Очистить существующие совпадения</string> <string name="oreo_autofill_matches_clear_existing">Очистить существующие совпадения</string>
<string name="oreo_autofill_filter_no_results">Не найдено.</string> <string name="oreo_autofill_filter_no_results">Не найдено.</string>
<string name="oreo_autofill_search_in_store">Искать в хранилище...</string>
<string name="oreo_autofill_save_internal_error">Сохранение не удалось из-за внутренней ошибки</string> <string name="oreo_autofill_save_internal_error">Сохранение не удалось из-за внутренней ошибки</string>
<string name="oreo_autofill_save_app_not_supported">Это приложение в настоящее время не поддерживается</string> <string name="oreo_autofill_save_app_not_supported">Это приложение в настоящее время не поддерживается</string>
<string name="oreo_autofill_save_passwords_dont_match">Пароли не совпадают</string> <string name="oreo_autofill_save_passwords_dont_match">Пароли не совпадают</string>
<string name="oreo_autofill_generate_password">Сгенерировать пароль...</string> <string name="oreo_autofill_fill_otp_from_sms">Извлечение кодов из SMS</string>
<string name="oreo_autofill_fill_otp_from_sms">Извлечение кодов из SMS…</string>
<string name="oreo_autofill_max_matches_reached">Достигнуто максимальное количество совпадений (%1$d); очистите совпадения перед тем как добавите новые.</string> <string name="oreo_autofill_max_matches_reached">Достигнуто максимальное количество совпадений (%1$d); очистите совпадения перед тем как добавите новые.</string>
<string name="oreo_autofill_warning_publisher_header">Издатель приложения изменился с тех пор как вы первый раз связали с ним запись хранилища паролей:</string> <string name="oreo_autofill_warning_publisher_header">Издатель приложения изменился с тех пор как вы первый раз связали с ним запись хранилища паролей:</string>
<string name="oreo_autofill_warning_publisher_footer"><b>Установленное приложение может попытаться украсть ваши учетные данные, выдавая себя за доверенное приложение</b>\n\nПопробуйте удалить или переустановить  приложение из доверенного источника, такого как Play Store, Amazon Appstore, F-Droid или магазин приложений производителя вашего смартфона.</string> <string name="oreo_autofill_warning_publisher_footer"><b>Установленное приложение может попытаться украсть ваши учетные данные, выдавая себя за доверенное приложение</b>\n\nПопробуйте удалить или переустановить  приложение из доверенного источника, такого как Play Store, Amazon Appstore, F-Droid или магазин приложений производителя вашего смартфона.</string>
@ -205,7 +203,7 @@
<string name="oreo_autofill_warning_publisher_changed_disable_autofill_button">Оставить автозаполнение отключенным</string> <string name="oreo_autofill_warning_publisher_changed_disable_autofill_button">Оставить автозаполнение отключенным</string>
<string name="oreo_autofill_warning_publisher_reenable_button">Включить автозаполнение снова</string> <string name="oreo_autofill_warning_publisher_reenable_button">Включить автозаполнение снова</string>
<string name="oreo_autofill_warning_publisher_warning_sign_description">Предупреждение</string> <string name="oreo_autofill_warning_publisher_warning_sign_description">Предупреждение</string>
<string name="oreo_autofill_warning_publisher_dataset_summary">Нажмите для получения подробностей...</string> <string name="oreo_autofill_warning_publisher_dataset_summary">Нажмите для получения подробностей</string>
<string name="oreo_autofill_warning_publisher_dataset_title">Возможная попытка фишинга</string> <string name="oreo_autofill_warning_publisher_dataset_title">Возможная попытка фишинга</string>
<string name="oreo_autofill_general_fill_and_save_support">Заполнить и сохранить учетные данные</string> <string name="oreo_autofill_general_fill_and_save_support">Заполнить и сохранить учетные данные</string>
<string name="oreo_autofill_general_fill_support">Заполнить учетные данные</string> <string name="oreo_autofill_general_fill_support">Заполнить учетные данные</string>

View file

@ -231,16 +231,17 @@
<string name="app_icon_hint">App icon</string> <string name="app_icon_hint">App icon</string>
<!-- Oreo Autofill --> <!-- Oreo Autofill -->
<string name="oreo_autofill_select_and_fill_into">Select entry to fill into</string>
<string name="oreo_autofill_strict_domain_search">Phishing-resistant search</string> <string name="oreo_autofill_strict_domain_search">Phishing-resistant search</string>
<string name="oreo_autofill_match_with">Match with %1$s</string> <string name="oreo_autofill_match_with">Match with %1$s</string>
<string name="oreo_autofill_matches_clear_existing">Clear existing matches</string> <string name="oreo_autofill_matches_clear_existing">Clear existing matches</string>
<string name="oreo_autofill_filter_no_results">No results.</string> <string name="oreo_autofill_filter_no_results">No results.</string>
<string name="oreo_autofill_search_in_store">Search in store…</string> <string name="oreo_autofill_search_in_store">Search entry</string>
<string name="oreo_autofill_save_internal_error">Save failed due to an internal error</string> <string name="oreo_autofill_save_internal_error">Save failed due to an internal error</string>
<string name="oreo_autofill_save_app_not_supported">This app is currently not supported</string> <string name="oreo_autofill_save_app_not_supported">This app is currently not supported</string>
<string name="oreo_autofill_save_passwords_dont_match">Passwords don\'t match</string> <string name="oreo_autofill_save_passwords_dont_match">Passwords don\'t match</string>
<string name="oreo_autofill_generate_password">Generate password…</string> <string name="oreo_autofill_generate_password">Create entry</string>
<string name="oreo_autofill_fill_otp_from_sms">Extract code from SMS</string> <string name="oreo_autofill_fill_otp_from_sms">Extract code from SMS</string>
<string name="oreo_autofill_max_matches_reached">Maximum number of matches (%1$d) reached; clear matches before adding new ones.</string> <string name="oreo_autofill_max_matches_reached">Maximum number of matches (%1$d) reached; clear matches before adding new ones.</string>
<string name="oreo_autofill_warning_publisher_header">This app\'s publisher has changed since you first associated a Password Store entry with it:</string> <string name="oreo_autofill_warning_publisher_header">This app\'s publisher has changed since you first associated a Password Store entry with it:</string>
<string name="oreo_autofill_warning_publisher_footer"><b>The currently installed app may be trying to steal your credentials by pretending to be a trusted app.</b>\n\nTry to uninstall and reinstall the app from a trusted source, such as the Play Store, Amazon Appstore, F-Droid, or your phone manufacturer\'s store.</string> <string name="oreo_autofill_warning_publisher_footer"><b>The currently installed app may be trying to steal your credentials by pretending to be a trusted app.</b>\n\nTry to uninstall and reinstall the app from a trusted source, such as the Play Store, Amazon Appstore, F-Droid, or your phone manufacturer\'s store.</string>
@ -250,7 +251,7 @@
<string name="oreo_autofill_warning_publisher_changed_disable_autofill_button">Keep Autofill disabled</string> <string name="oreo_autofill_warning_publisher_changed_disable_autofill_button">Keep Autofill disabled</string>
<string name="oreo_autofill_warning_publisher_reenable_button">Re-enable Autofill</string> <string name="oreo_autofill_warning_publisher_reenable_button">Re-enable Autofill</string>
<string name="oreo_autofill_warning_publisher_warning_sign_description">Warning</string> <string name="oreo_autofill_warning_publisher_warning_sign_description">Warning</string>
<string name="oreo_autofill_warning_publisher_dataset_summary">Tap for details</string> <string name="oreo_autofill_warning_publisher_dataset_summary">Tap for details</string>
<string name="oreo_autofill_warning_publisher_dataset_title">Possible phishing attempt</string> <string name="oreo_autofill_warning_publisher_dataset_title">Possible phishing attempt</string>
<string name="oreo_autofill_general_fill_and_save_support">Fill and save credentials</string> <string name="oreo_autofill_general_fill_and_save_support">Fill and save credentials</string>
<string name="oreo_autofill_general_fill_support">Fill credentials</string> <string name="oreo_autofill_general_fill_support">Fill credentials</string>

View file

@ -5,6 +5,7 @@
<autofill-service xmlns:android="http://schemas.android.com/apk/res/android" <autofill-service xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:supportsInlineSuggestions="true"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<compatibility-package android:name="com.android.chrome" /> <compatibility-package android:name="com.android.chrome" />
<compatibility-package android:name="com.brave.browser" /> <compatibility-package android:name="com.brave.browser" />