Skip to content
This repository has been archived by the owner on Oct 15, 2024. It is now read-only.

Work around Chrome Autofill issue #921

Merged
merged 7 commits into from
Jul 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
57 changes: 48 additions & 9 deletions app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values-v28/bools.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="enable_chrome_compat_fix">true</bool>
</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values/bools.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
12 changes: 12 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
13 changes: 13 additions & 0 deletions app/src/main/res/xml/oreo_autofill_chrome_compat_fix.xml
Original file line number Diff line number Diff line change
@@ -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" />
5 changes: 5 additions & 0 deletions app/src/main/res/xml/preference.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down