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.kotlin.coroutines.android
implementation deps.kotlin.coroutines.core
implementation deps.third_party.commons_io
implementation deps.third_party.commons_codec
implementation deps.third_party.fastscroll

View file

@ -6,8 +6,6 @@ package com.zeapo.pwdstore.autofill
import android.accessibilityservice.AccessibilityService
import android.app.PendingIntent
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
@ -15,15 +13,16 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.Window
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.PasswordEntry
@ -39,6 +38,11 @@ import java.net.MalformedURLException
import java.net.URL
import java.util.ArrayList
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.OpenPgpServiceConnection
import org.apache.commons.io.FileUtils
@ -46,7 +50,7 @@ import org.openintents.openpgp.IOpenPgpService2
import org.openintents.openpgp.OpenPgpError
import timber.log.Timber
class AutofillService : AccessibilityService() {
class AutofillService : AccessibilityService(), CoroutineScope by CoroutineScope(Dispatchers.Default) {
private var serviceConnection: OpenPgpServiceConnection? = null
private var settings: SharedPreferences? = null
private var info: AccessibilityNodeInfo? = null // the original source of the event (the edittext field)
@ -76,6 +80,12 @@ class AutofillService : AccessibilityService() {
instance = this
}
override fun onDestroy() {
super.onDestroy()
instance = null
cancel()
}
override fun onServiceConnected() {
super.onServiceConnected()
serviceConnection = OpenPgpServiceConnection(this@AutofillService, "org.sufficientlysecure.keychain")
@ -135,7 +145,7 @@ class AutofillService : AccessibilityService() {
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED ||
event.packageName != null && event.packageName == "org.sufficientlysecure.keychain" ||
event.packageName != null && event.packageName == "com.android.systemui") {
dismissDialog(event)
dismissDialog()
return
}
@ -145,7 +155,7 @@ class AutofillService : AccessibilityService() {
return
} else {
// nothing to do if not password field focus
dismissDialog(event)
dismissDialog()
return
}
}
@ -182,9 +192,7 @@ class AutofillService : AccessibilityService() {
if (info == null) return
// 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 appName: String
@ -246,18 +254,8 @@ class AutofillService : AccessibilityService() {
}
// dismiss the dialog if the window has changed
private fun dismissDialog(event: AccessibilityEvent) {
// the default keyboard showing/hiding is a window state changed event
// 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"))
}
private fun dismissDialog() {
val dismiss = !windows.contains(window)
if (dismiss && dialog != null && dialog!!.isShowing) {
dialog!!.dismiss()
dialog = null
@ -380,9 +378,12 @@ class AutofillService : AccessibilityService() {
builder.setMessage(getString(R.string.autofill_paste_username, password.username))
dialog = builder.create()
this.setDialogType(dialog)
dialog!!.window!!.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
dialog!!.window!!.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
require(dialog != null) { "Dialog should not be null at this stage" }
dialog!!.window!!.apply {
setDialogType(this)
addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
}
dialog!!.show()
}
@ -446,8 +447,8 @@ class AutofillService : AccessibilityService() {
}
dialog = builder.create()
setDialogType(dialog)
dialog?.window?.apply {
setDialogType(this)
val density = context.resources.displayMetrics.density
addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
@ -458,15 +459,13 @@ class AutofillService : AccessibilityService() {
dialog?.show()
}
private fun setDialogType(dialog: AlertDialog?) {
dialog?.window?.apply {
setType(
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
else
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
)
}
@Suppress("DEPRECATION")
private fun setDialogType(window: Window) {
window.setType(if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
else
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
)
}
override fun onInterrupt() {}
@ -482,7 +481,7 @@ class AutofillService : AccessibilityService() {
}
}
private fun decryptAndVerify() {
private fun decryptAndVerify() = launch {
packageName = info!!.packageName
val data: Intent
if (resultData == null) {
@ -492,40 +491,45 @@ class AutofillService : AccessibilityService() {
data = resultData!!
resultData = null
}
var `is`: InputStream? = null
try {
`is` = FileUtils.openInputStream(items[lastWhichItem])
} catch (e: IOException) {
e.printStackTrace()
var inputStream: InputStream? = null
withContext(Dispatchers.IO) {
try {
inputStream = FileUtils.openInputStream(items[lastWhichItem])
} catch (e: IOException) {
e.printStackTrace()
cancel("", e)
}
}
val os = ByteArrayOutputStream()
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, `is`, os)
val result = api.executeApi(data, inputStream, os)
when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
OpenPgpApi.RESULT_CODE_SUCCESS -> {
try {
val entry = PasswordEntry(os)
pasteText(info!!, entry.password)
var entry: PasswordEntry? = null
withContext(Dispatchers.IO) {
entry = PasswordEntry(os)
}
withContext(Dispatchers.Main) { pasteText(info!!, entry?.password) }
// save password entry for pasting the username as well
if (entry.hasUsername()) {
if (entry?.hasUsername() == true) {
lastPassword = entry
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
}
} catch (e: UnsupportedEncodingException) {
Timber.tag(Constants.TAG).e(e, "UnsupportedEncodingException")
Timber.tag(Constants.TAG).e(e)
}
}
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
Timber.tag("PgpHandler").i("RESULT_CODE_USER_INTERACTION_REQUIRED")
val pi = result.getParcelableExtra<PendingIntent>(OpenPgpApi.RESULT_INTENT)
// 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.putExtra("pending_intent", pi)
startActivity(intent)
@ -533,9 +537,7 @@ class AutofillService : AccessibilityService() {
OpenPgpApi.RESULT_CODE_ERROR -> {
val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR)
if (error != null) {
Toast.makeText(this@AutofillService,
"Error from OpenKeyChain : " + error.message,
Toast.LENGTH_LONG).show()
Toast.makeText(applicationContext, "Error from OpenKeyChain : ${error.message}", Toast.LENGTH_LONG).show()
Timber.tag(Constants.TAG).e("onError getErrorId: ${error.errorId}")
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
// & need to ensure performAction correct (i.e. what is info now?)
ignoreActionFocus = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val args = Bundle()
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)
}
}
}
val args = bundleOf(Pair(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text))
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
node.recycle()
}

View file

@ -20,6 +20,10 @@ ext.deps = [
],
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'
],