summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabian Henneke <FabianHenneke@users.noreply.github.com>2020-07-07 17:02:57 +0200
committerGitHub <noreply@github.com>2020-07-07 20:32:57 +0530
commitd192ab2d9a6f45fb23e3d3f709c144ce1be3a850 (patch)
tree7e946de1f3e15db63be9c184d74b49d610c49d09
parent5d170249cdd0050349f40d3a5852a8ba996663bc (diff)
Work around Chrome Autofill issue (#921)
-rw-r--r--CHANGELOG.md1
-rw-r--r--app/src/main/AndroidManifest.xml13
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/UserPreference.kt57
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ChromeCompatFix.kt93
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt1
-rw-r--r--app/src/main/res/values-v28/bools.xml4
-rw-r--r--app/src/main/res/values/bools.xml1
-rw-r--r--app/src/main/res/values/strings.xml12
-rw-r--r--app/src/main/res/xml/oreo_autofill_chrome_compat_fix.xml13
-rw-r--r--app/src/main/res/xml/preference.xml5
10 files changed, 191 insertions, 9 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d83343e1..1ee49497 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file.
- Fix authentication failure with usernames that contain the `@` character
- Text input boxes were illegible on dark theme
- Top-level password names had inconsistent top margin making them look askew
+- Autofill can now be made more reliable in Chrome by enabling an accessibility service that works around known Chrome limitations
### Added
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 40bcb481..1a49b017 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -99,6 +99,19 @@
android:name="android.accessibilityservice"
android:resource="@xml/autofill_config" />
</service>
+
+ <service
+ android:name=".autofill.oreo.ChromeCompatFix"
+ android:enabled="@bool/enable_chrome_compat_fix"
+ android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+ <intent-filter>
+ <action android:name="android.accessibilityservice.AccessibilityService" />
+ </intent-filter>
+ <meta-data
+ android:name="android.accessibilityservice"
+ android:resource="@xml/oreo_autofill_chrome_compat_fix" />
+ </service>
+
<service
android:name=".ClipboardService"
android:process=":clipboard_service_process" />
diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
index ea3bac38..4d68834b 100644
--- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
@@ -41,7 +41,9 @@ import com.github.ajalt.timberkt.w
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
+import com.zeapo.pwdstore.autofill.AutofillService
import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel
+import com.zeapo.pwdstore.autofill.oreo.ChromeCompatFix
import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel
import com.zeapo.pwdstore.crypto.BasePgpActivity
import com.zeapo.pwdstore.crypto.GetKeyIdsActivity
@@ -73,6 +75,7 @@ class UserPreference : AppCompatActivity() {
class PrefsFragment : PreferenceFragmentCompat() {
private var autoFillEnablePreference: SwitchPreferenceCompat? = null
+ private var oreoAutofillChromeCompatFix: SwitchPreferenceCompat? = null
private var clearSavedPassPreference: Preference? = null
private lateinit var autofillDependencies: List<Preference>
private lateinit var oreoAutofillDependencies: List<Preference>
@@ -118,6 +121,7 @@ class UserPreference : AppCompatActivity() {
// Autofill preferences
autoFillEnablePreference = findPreference(PreferenceKeys.AUTOFILL_ENABLE)
+ oreoAutofillChromeCompatFix = findPreference(PreferenceKeys.OREO_AUTOFILL_CHROME_COMPAT_FIX)
val oreoAutofillDirectoryStructurePreference = findPreference<ListPreference>(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE)
val oreoAutofillDefaultUsername = findPreference<EditTextPreference>(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)
val oreoAutofillCustomPublixSuffixes = findPreference<EditTextPreference>(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
@@ -276,6 +280,16 @@ class UserPreference : AppCompatActivity() {
true
}
+ oreoAutofillChromeCompatFix?.onPreferenceClickListener = ClickListener {
+ if (oreoAutofillChromeCompatFix!!.isChecked) {
+ startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
+ true
+ } else {
+ // Service will disable itself on startup if the preference has the value false.
+ false
+ }
+ }
+
findPreference<Preference>(PreferenceKeys.EXPORT_PASSWORDS)?.apply {
isVisible = sharedPreferences.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
onPreferenceClickListener = Preference.OnPreferenceClickListener {
@@ -398,16 +412,20 @@ class UserPreference : AppCompatActivity() {
}
private fun updateAutofillSettings() {
- val isAccessibilityServiceEnabled = callingActivity.isAccessibilityServiceEnabled
+ val isAccessibilityAutofillServiceEnabled = callingActivity.isAccessibilityAutofillServiceEnabled
val isAutofillServiceEnabled = callingActivity.isAutofillServiceEnabled
autoFillEnablePreference?.isChecked =
- isAccessibilityServiceEnabled || isAutofillServiceEnabled
+ isAccessibilityAutofillServiceEnabled || isAutofillServiceEnabled
autofillDependencies.forEach {
- it.isVisible = isAccessibilityServiceEnabled
+ it.isVisible = isAccessibilityAutofillServiceEnabled
}
oreoAutofillDependencies.forEach {
it.isVisible = isAutofillServiceEnabled
}
+ oreoAutofillChromeCompatFix?.apply {
+ isChecked = callingActivity.isChromeCompatFixServiceEnabled
+ isVisible = callingActivity.isChromeCompatFixServiceSupported
+ }
}
private fun updateClearSavedPassphrasePrefs() {
@@ -428,13 +446,16 @@ class UserPreference : AppCompatActivity() {
}
private fun onEnableAutofillClick() {
- if (callingActivity.isAccessibilityServiceEnabled) {
+ if (callingActivity.isAccessibilityAutofillServiceEnabled) {
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
} else if (callingActivity.isAutofillServiceEnabled) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
callingActivity.autofillManager!!.disableAutofillServices()
- else
+ ChromeCompatFix.setStatusInPreferences(requireContext(), false)
+ updateAutofillSettings()
+ } else {
throw IllegalStateException("isAutofillServiceEnabled == true, but Build.VERSION.SDK_INT < Build.VERSION_CODES.O")
+ }
} else {
val enableOreoAutofill = callingActivity.isAutofillServiceSupported
MaterialAlertDialogBuilder(callingActivity).run {
@@ -710,14 +731,32 @@ class UserPreference : AppCompatActivity() {
File("$filesDir/.ssh_key").writeText(lines.joinToString("\n"))
}
- private val isAccessibilityServiceEnabled: Boolean
+ private val isAccessibilityAutofillServiceEnabled: Boolean
get() {
val am = getSystemService<AccessibilityManager>() ?: return false
val runningServices = am
.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC)
return runningServices
- .map { it.id.substringBefore("/") }
- .any { it == BuildConfig.APPLICATION_ID }
+ .mapNotNull { it?.resolveInfo?.serviceInfo }
+ .any { it.packageName == BuildConfig.APPLICATION_ID && it.name == AutofillService::class.java.name }
+ }
+
+ private val isChromeCompatFixServiceEnabled: Boolean
+ get() {
+ val am = getSystemService<AccessibilityManager>() ?: return false
+ val runningServices = am
+ .getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC)
+ return runningServices
+ .mapNotNull { it?.resolveInfo?.serviceInfo }
+ .any { it.packageName == BuildConfig.APPLICATION_ID && it.name == ChromeCompatFix::class.java.name }
+ }
+
+ private val isChromeCompatFixServiceSupported: Boolean
+ get() {
+ // Autofill compat mode is only available starting with Android Pie and only makes sense
+ // when used with Autofill enabled.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return false
+ return isAutofillServiceEnabled
}
private val isAutofillServiceSupported: Boolean
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ChromeCompatFix.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ChromeCompatFix.kt
new file mode 100644
index 00000000..75d9539a
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ChromeCompatFix.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.autofill.oreo
+
+import android.accessibilityservice.AccessibilityService
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.view.accessibility.AccessibilityEvent
+import androidx.annotation.RequiresApi
+import androidx.core.content.edit
+import androidx.preference.PreferenceManager
+import com.github.ajalt.timberkt.i
+import com.github.ajalt.timberkt.v
+import com.github.ajalt.timberkt.w
+import com.zeapo.pwdstore.utils.PreferenceKeys
+import com.zeapo.pwdstore.utils.autofillManager
+
+@RequiresApi(Build.VERSION_CODES.P)
+class ChromeCompatFix : AccessibilityService() {
+
+ companion object {
+ fun setStatusInPreferences(context: Context, enabled: Boolean) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit {
+ putBoolean(PreferenceKeys.OREO_AUTOFILL_CHROME_COMPAT_FIX, enabled)
+ }
+ }
+ }
+
+ private val isEnabledInPreferences
+ get() = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(PreferenceKeys.OREO_AUTOFILL_CHROME_COMPAT_FIX, true)
+
+ private val handler = Handler(Looper.getMainLooper())
+ private val forceRootNodePopulation = Runnable {
+ val rootPackageName = rootInActiveWindow?.packageName.toString()
+ v { "$rootPackageName: forced root node population" }
+ }
+ private val disableListener = SharedPreferences.OnSharedPreferenceChangeListener { prefs: SharedPreferences, key: String ->
+ if (key != PreferenceKeys.OREO_AUTOFILL_CHROME_COMPAT_FIX)
+ return@OnSharedPreferenceChangeListener
+ if (!isEnabledInPreferences) {
+ i { "Disabled in settings, shutting down..." }
+ disableSelf()
+ }
+ }
+
+ override fun onAccessibilityEvent(event: AccessibilityEvent) {
+ handler.removeCallbacks(forceRootNodePopulation)
+ when (event.eventType) {
+ AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_ANNOUNCEMENT -> {
+ // WINDOW_STATE_CHANGED: Triggered on long press in a text field, replacement for
+ // the missing Autofill action menu item.
+ // ANNOUNCEMENT: Triggered when a password field is selected.
+ //
+ // These events are triggered only by user actions and thus don't need to be handled
+ // with debounce. However, they only trigger Autofill popups on the *next* input
+ // field selected by the user.
+ forceRootNodePopulation.run()
+ v { "${event.packageName} (${AccessibilityEvent.eventTypeToString(event.eventType)}): forced root node population" }
+ }
+ AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> {
+ // WINDOW_CONTENT_CHANGED: Triggered whenever the page contents change.
+ //
+ // This event is triggered many times during page load, which makes a debounce
+ // necessary to prevent huge performance regressions in Chrome. However, it is the
+ // only event that reliably runs before the user selects a text field.
+ handler.postDelayed(forceRootNodePopulation, 300)
+ v { "${event.packageName} (${AccessibilityEvent.eventTypeToString(event.eventType)}): debounced root node population" }
+ }
+ }
+ }
+
+ override fun onServiceConnected() {
+ super.onServiceConnected()
+ // Allow the service to be activated only if the Autofill service is already enabled.
+ if (autofillManager?.hasEnabledAutofillServices() != true) {
+ w { "Autofill service not enabled, shutting down..." }
+ disableSelf()
+ return
+ }
+ // Update preferences if the user manually activated the service.
+ setStatusInPreferences(this, true)
+
+ PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(disableListener)
+ }
+
+ override fun onInterrupt() {}
+}
+
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
index 05f9c741..7d019508 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
@@ -35,6 +35,7 @@ object PreferenceKeys {
const val OPENPGP_KEY_IDS_SET = "openpgp_key_ids_set"
const val OPENPGP_KEY_ID_PREF = "openpgp_key_id_pref"
const val OPENPGP_PROVIDER_LIST = "openpgp_provider_list"
+ const val OREO_AUTOFILL_CHROME_COMPAT_FIX = "oreo_autofill_chrome_compat_fix"
const val OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES = "oreo_autofill_custom_public_suffixes"
const val OREO_AUTOFILL_DEFAULT_USERNAME = "oreo_autofill_default_username"
const val OREO_AUTOFILL_DIRECTORY_STRUCTURE = "oreo_autofill_directory_structure"
diff --git a/app/src/main/res/values-v28/bools.xml b/app/src/main/res/values-v28/bools.xml
new file mode 100644
index 00000000..0ce64e0b
--- /dev/null
+++ b/app/src/main/res/values-v28/bools.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="enable_chrome_compat_fix">true</bool>
+</resources>
diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml
index fcf624a7..fbcc1c73 100644
--- a/app/src/main/res/values/bools.xml
+++ b/app/src/main/res/values/bools.xml
@@ -3,4 +3,5 @@
<bool name="leak_canary_allow_in_non_debuggable_build">true</bool>
<bool name="enable_accessibility_autofill">true</bool>
<bool name="light_status_bar">true</bool>
+ <bool name="enable_chrome_compat_fix">false</bool>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d62b5292..01b63579 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -275,6 +275,16 @@
<string name="oreo_autofill_enable_dialog_description">Password Store can offer to fill login forms and even save credentials you enter in apps or on websites.</string>
<string name="oreo_autofill_enable_dialog_instructions">To enable this feature, tap OK to go to Autofill settings. There, select Password Store from the list and confirm the confirmation prompt with OK.</string>
<string name="oreo_autofill_enable_dialog_installed_browsers">Autofill support with installed browsers:</string>
+ <string name="oreo_autofill_chrome_compat_fix_summary">Make Autofill more reliable in Chrome</string>
+ <string name="oreo_autofill_chrome_compat_fix_description">This accessibility service makes
+ Autofill work more reliably in Chrome. It can only be activated if you are already using
+ Password Store as your Autofill service.\n\nThis service is only active while you are
+ using Chrome. It does not access any data or take any actions on your behalf, but forces
+ Chrome to properly forward user interactions to the Password Store Autofill
+ service.\n\nChrome\'s performance should not be noticeably affected. If you are experiencing
+ any problems with this service, please create an issue at
+ https://msfjarvis.dev/aps.
+ </string>
<!-- Autofill -->
<string name="autofill_description">Autofills password fields in apps. Only works for Android versions 4.3 and up. Does not rely on the clipboard for Android versions 5.0 and up.</string>
@@ -388,4 +398,6 @@
<string name="add_otp">Add OTP</string>
<string name="otp_import_success">Successfully imported TOTP configuration</string>
<string name="otp_import_failure">Failed to import TOTP configuration</string>
+ <string name="oreo_autofill_chrome_compat_fix_preference_title">Improve reliability in Chrome</string>
+ <string name="oreo_autofill_chrome_compat_fix_preference_summary">Requires activating an accessibility service and may affect overall Chrome performance</string>
</resources>
diff --git a/app/src/main/res/xml/oreo_autofill_chrome_compat_fix.xml b/app/src/main/res/xml/oreo_autofill_chrome_compat_fix.xml
new file mode 100644
index 00000000..196c93d5
--- /dev/null
+++ b/app/src/main/res/xml/oreo_autofill_chrome_compat_fix.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ ~ SPDX-License-Identifier: GPL-3.0-only
+ -->
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accessibilityEventTypes="typeWindowContentChanged|typeAnnouncement|typeWindowStateChanged"
+ android:accessibilityFeedbackType="feedbackGeneric"
+ android:accessibilityFlags="flagDefault"
+ android:canRetrieveWindowContent="true"
+ android:description="@string/oreo_autofill_chrome_compat_fix_description"
+ android:notificationTimeout="100"
+ android:packageNames="com.android.chrome,com.chrome.beta,com.chrome.dev,com.chrome.canary"
+ android:summary="@string/oreo_autofill_chrome_compat_fix_summary" />
diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml
index 0d71d6cc..d4ec4139 100644
--- a/app/src/main/res/xml/preference.xml
+++ b/app/src/main/res/xml/preference.xml
@@ -10,6 +10,11 @@
app:defaultValue="true"
app:key="autofill_enable"
app:title="@string/pref_autofill_enable_title" />
+ <SwitchPreferenceCompat
+ app:defaultValue="true"
+ app:key="oreo_autofill_chrome_compat_fix"
+ app:summary="@string/oreo_autofill_chrome_compat_fix_preference_summary"
+ app:title="@string/oreo_autofill_chrome_compat_fix_preference_title" />
<ListPreference
app:defaultValue="file"
app:entries="@array/oreo_autofill_directory_structure_entries"