From d192ab2d9a6f45fb23e3d3f709c144ce1be3a850 Mon Sep 17 00:00:00 2001 From: Fabian Henneke Date: Tue, 7 Jul 2020 17:02:57 +0200 Subject: Work around Chrome Autofill issue (#921) --- app/src/main/AndroidManifest.xml | 13 +++ .../main/java/com/zeapo/pwdstore/UserPreference.kt | 57 ++++++++++--- .../pwdstore/autofill/oreo/ChromeCompatFix.kt | 93 ++++++++++++++++++++++ .../com/zeapo/pwdstore/utils/PreferenceKeys.kt | 1 + app/src/main/res/values-v28/bools.xml | 4 + app/src/main/res/values/bools.xml | 1 + app/src/main/res/values/strings.xml | 12 +++ .../res/xml/oreo_autofill_chrome_compat_fix.xml | 13 +++ app/src/main/res/xml/preference.xml | 5 ++ 9 files changed, 190 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ChromeCompatFix.kt create mode 100644 app/src/main/res/values-v28/bools.xml create mode 100644 app/src/main/res/xml/oreo_autofill_chrome_compat_fix.xml (limited to 'app') 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" /> + + + + + + + + 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 private lateinit var oreoAutofillDependencies: List @@ -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(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE) val oreoAutofillDefaultUsername = findPreference(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME) val oreoAutofillCustomPublixSuffixes = findPreference(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(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() ?: 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() ?: 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 @@ + + + true + 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 @@ true true true + false 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 @@ Password Store can offer to fill login forms and even save credentials you enter in apps or on websites. 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. Autofill support with installed browsers: + Make Autofill more reliable in Chrome + 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. + 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. @@ -388,4 +398,6 @@ Add OTP Successfully imported TOTP configuration Failed to import TOTP configuration + Improve reliability in Chrome + Requires activating an accessibility service and may affect overall Chrome performance 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 @@ + + 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" /> +