summaryrefslogtreecommitdiff
path: root/app/src/main/java
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 /app/src/main/java
parent5d170249cdd0050349f40d3a5852a8ba996663bc (diff)
Work around Chrome Autofill issue (#921)
Diffstat (limited to 'app/src/main/java')
-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
3 files changed, 142 insertions, 9 deletions
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"