Autofill UX improvements and code cleanup (#626)

Co-authored-by: Aditya Wasan <adityawasan55@gmail.com>
Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com>
This commit is contained in:
Harsh Shandilya 2020-01-30 20:01:06 +05:30 committed by GitHub
parent d350a27611
commit 74bb1c4357
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 64 additions and 71 deletions

View file

@ -90,6 +90,10 @@ dependencies {
} }
} }
implementation deps.androidx.material implementation deps.androidx.material
implementation deps.kotlin.coroutines.android
implementation deps.kotlin.coroutines.core
implementation deps.third_party.commons_io implementation deps.third_party.commons_io
implementation deps.third_party.commons_codec implementation deps.third_party.commons_codec
implementation deps.third_party.fastscroll implementation deps.third_party.fastscroll

View file

@ -6,8 +6,6 @@ package com.zeapo.pwdstore.autofill
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@ -15,15 +13,16 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo import android.view.accessibility.AccessibilityWindowInfo
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.PasswordEntry import com.zeapo.pwdstore.PasswordEntry
@ -39,6 +38,11 @@ import java.net.MalformedURLException
import java.net.URL import java.net.URL
import java.util.ArrayList import java.util.ArrayList
import java.util.Locale import java.util.Locale
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.msfjarvis.openpgpktx.util.OpenPgpApi import me.msfjarvis.openpgpktx.util.OpenPgpApi
import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
@ -46,7 +50,7 @@ import org.openintents.openpgp.IOpenPgpService2
import org.openintents.openpgp.OpenPgpError import org.openintents.openpgp.OpenPgpError
import timber.log.Timber import timber.log.Timber
class AutofillService : AccessibilityService() { class AutofillService : AccessibilityService(), CoroutineScope by CoroutineScope(Dispatchers.Default) {
private var serviceConnection: OpenPgpServiceConnection? = null private var serviceConnection: OpenPgpServiceConnection? = null
private var settings: SharedPreferences? = null private var settings: SharedPreferences? = null
private var info: AccessibilityNodeInfo? = null // the original source of the event (the edittext field) private var info: AccessibilityNodeInfo? = null // the original source of the event (the edittext field)
@ -76,6 +80,12 @@ class AutofillService : AccessibilityService() {
instance = this instance = this
} }
override fun onDestroy() {
super.onDestroy()
instance = null
cancel()
}
override fun onServiceConnected() { override fun onServiceConnected() {
super.onServiceConnected() super.onServiceConnected()
serviceConnection = OpenPgpServiceConnection(this@AutofillService, "org.sufficientlysecure.keychain") serviceConnection = OpenPgpServiceConnection(this@AutofillService, "org.sufficientlysecure.keychain")
@ -135,7 +145,7 @@ class AutofillService : AccessibilityService() {
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED || if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED ||
event.packageName != null && event.packageName == "org.sufficientlysecure.keychain" || event.packageName != null && event.packageName == "org.sufficientlysecure.keychain" ||
event.packageName != null && event.packageName == "com.android.systemui") { event.packageName != null && event.packageName == "com.android.systemui") {
dismissDialog(event) dismissDialog()
return return
} }
@ -145,7 +155,7 @@ class AutofillService : AccessibilityService() {
return return
} else { } else {
// nothing to do if not password field focus // nothing to do if not password field focus
dismissDialog(event) dismissDialog()
return return
} }
} }
@ -182,9 +192,7 @@ class AutofillService : AccessibilityService() {
if (info == null) return if (info == null) return
// save the dialog's corresponding window so we can use getWindows() in dismissDialog // save the dialog's corresponding window so we can use getWindows() in dismissDialog
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { window = info!!.window
window = info!!.window
}
val packageName: String val packageName: String
val appName: String val appName: String
@ -246,18 +254,8 @@ class AutofillService : AccessibilityService() {
} }
// dismiss the dialog if the window has changed // dismiss the dialog if the window has changed
private fun dismissDialog(event: AccessibilityEvent) { private fun dismissDialog() {
// the default keyboard showing/hiding is a window state changed event val dismiss = !windows.contains(window)
// on Android 5+ we can use getWindows() to determine when the original window is not visible
// on Android 4.3 we have to use window state changed events and filter out the keyboard ones
// there may be other exceptions...
val dismiss: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
!windows.contains(window)
} else {
!(event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
event.packageName != null &&
event.packageName.toString().contains("inputmethod"))
}
if (dismiss && dialog != null && dialog!!.isShowing) { if (dismiss && dialog != null && dialog!!.isShowing) {
dialog!!.dismiss() dialog!!.dismiss()
dialog = null dialog = null
@ -380,9 +378,12 @@ class AutofillService : AccessibilityService() {
builder.setMessage(getString(R.string.autofill_paste_username, password.username)) builder.setMessage(getString(R.string.autofill_paste_username, password.username))
dialog = builder.create() dialog = builder.create()
this.setDialogType(dialog) require(dialog != null) { "Dialog should not be null at this stage" }
dialog!!.window!!.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) dialog!!.window!!.apply {
dialog!!.window!!.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) setDialogType(this)
addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
}
dialog!!.show() dialog!!.show()
} }
@ -446,8 +447,8 @@ class AutofillService : AccessibilityService() {
} }
dialog = builder.create() dialog = builder.create()
setDialogType(dialog)
dialog?.window?.apply { dialog?.window?.apply {
setDialogType(this)
val density = context.resources.displayMetrics.density val density = context.resources.displayMetrics.density
addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
@ -458,15 +459,13 @@ class AutofillService : AccessibilityService() {
dialog?.show() dialog?.show()
} }
private fun setDialogType(dialog: AlertDialog?) { @Suppress("DEPRECATION")
dialog?.window?.apply { private fun setDialogType(window: Window) {
setType( window.setType(if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT else
else WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY )
)
}
} }
override fun onInterrupt() {} override fun onInterrupt() {}
@ -482,7 +481,7 @@ class AutofillService : AccessibilityService() {
} }
} }
private fun decryptAndVerify() { private fun decryptAndVerify() = launch {
packageName = info!!.packageName packageName = info!!.packageName
val data: Intent val data: Intent
if (resultData == null) { if (resultData == null) {
@ -492,40 +491,45 @@ class AutofillService : AccessibilityService() {
data = resultData!! data = resultData!!
resultData = null resultData = null
} }
var `is`: InputStream? = null
try { var inputStream: InputStream? = null
`is` = FileUtils.openInputStream(items[lastWhichItem]) withContext(Dispatchers.IO) {
} catch (e: IOException) { try {
e.printStackTrace() inputStream = FileUtils.openInputStream(items[lastWhichItem])
} catch (e: IOException) {
e.printStackTrace()
cancel("", e)
}
} }
val os = ByteArrayOutputStream() val os = ByteArrayOutputStream()
val api = OpenPgpApi(this@AutofillService, serviceConnection!!.service!!) val api = OpenPgpApi(this@AutofillService, serviceConnection!!.service!!)
// TODO we are dropping frames, (did we before??) find out why and maybe make this async val result = api.executeApi(data, inputStream, os)
val result = api.executeApi(data, `is`, os)
when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
OpenPgpApi.RESULT_CODE_SUCCESS -> { OpenPgpApi.RESULT_CODE_SUCCESS -> {
try { try {
val entry = PasswordEntry(os) var entry: PasswordEntry? = null
pasteText(info!!, entry.password) withContext(Dispatchers.IO) {
entry = PasswordEntry(os)
}
withContext(Dispatchers.Main) { pasteText(info!!, entry?.password) }
// save password entry for pasting the username as well // save password entry for pasting the username as well
if (entry.hasUsername()) { if (entry?.hasUsername() == true) {
lastPassword = entry lastPassword = entry
val ttl = Integer.parseInt(settings!!.getString("general_show_time", "45")!!) val ttl = Integer.parseInt(settings!!.getString("general_show_time", "45")!!)
Toast.makeText(this, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show() Toast.makeText(applicationContext, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show()
lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L
} }
} catch (e: UnsupportedEncodingException) { } catch (e: UnsupportedEncodingException) {
Timber.tag(Constants.TAG).e(e, "UnsupportedEncodingException") Timber.tag(Constants.TAG).e(e)
} }
} }
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
Timber.tag("PgpHandler").i("RESULT_CODE_USER_INTERACTION_REQUIRED") Timber.tag("PgpHandler").i("RESULT_CODE_USER_INTERACTION_REQUIRED")
val pi = result.getParcelableExtra<PendingIntent>(OpenPgpApi.RESULT_INTENT) val pi = result.getParcelableExtra<PendingIntent>(OpenPgpApi.RESULT_INTENT)
// need to start a blank activity to call startIntentSenderForResult // need to start a blank activity to call startIntentSenderForResult
val intent = Intent(this@AutofillService, AutofillActivity::class.java) val intent = Intent(applicationContext, AutofillActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.putExtra("pending_intent", pi) intent.putExtra("pending_intent", pi)
startActivity(intent) startActivity(intent)
@ -533,9 +537,7 @@ class AutofillService : AccessibilityService() {
OpenPgpApi.RESULT_CODE_ERROR -> { OpenPgpApi.RESULT_CODE_ERROR -> {
val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR) val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR)
if (error != null) { if (error != null) {
Toast.makeText(this@AutofillService, Toast.makeText(applicationContext, "Error from OpenKeyChain : ${error.message}", Toast.LENGTH_LONG).show()
"Error from OpenKeyChain : " + error.message,
Toast.LENGTH_LONG).show()
Timber.tag(Constants.TAG).e("onError getErrorId: ${error.errorId}") Timber.tag(Constants.TAG).e("onError getErrorId: ${error.errorId}")
Timber.tag(Constants.TAG).e("onError getMessage: ${error.message}") Timber.tag(Constants.TAG).e("onError getMessage: ${error.message}")
} }
@ -548,25 +550,8 @@ class AutofillService : AccessibilityService() {
// but this will open another dialog...hack to ignore this // but this will open another dialog...hack to ignore this
// & need to ensure performAction correct (i.e. what is info now?) // & need to ensure performAction correct (i.e. what is info now?)
ignoreActionFocus = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS) ignoreActionFocus = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val args = bundleOf(Pair(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text))
val args = Bundle() node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
} else {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
var clip = ClipData.newPlainText("autofill_pm", text)
clipboard.setPrimaryClip(clip)
node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
clip = ClipData.newPlainText("autofill_pm", "")
clipboard.setPrimaryClip(clip)
if (settings!!.getBoolean("clear_clipboard_20x", false)) {
for (i in 0..19) {
clip = ClipData.newPlainText(i.toString(), i.toString())
clipboard.setPrimaryClip(clip)
}
}
}
node.recycle() node.recycle()
} }

View file

@ -20,6 +20,10 @@ ext.deps = [
], ],
kotlin: [ kotlin: [
coroutines: [
android: 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3',
core: 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3',
],
stdlib8: 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61' stdlib8: 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.61'
], ],