diff options
author | Fabian Henneke <FabianHenneke@users.noreply.github.com> | 2020-07-07 17:02:57 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-07 20:32:57 +0530 |
commit | d192ab2d9a6f45fb23e3d3f709c144ce1be3a850 (patch) | |
tree | 7e946de1f3e15db63be9c184d74b49d610c49d09 | |
parent | 5d170249cdd0050349f40d3a5852a8ba996663bc (diff) |
Work around Chrome Autofill issue (#921)
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | app/src/main/AndroidManifest.xml | 13 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/UserPreference.kt | 57 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ChromeCompatFix.kt | 93 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt | 1 | ||||
-rw-r--r-- | app/src/main/res/values-v28/bools.xml | 4 | ||||
-rw-r--r-- | app/src/main/res/values/bools.xml | 1 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 12 | ||||
-rw-r--r-- | app/src/main/res/xml/oreo_autofill_chrome_compat_fix.xml | 13 | ||||
-rw-r--r-- | app/src/main/res/xml/preference.xml | 5 |
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" |