From 74bb1c43570ad854f1340afc5207090e37c23eb4 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 30 Jan 2020 20:01:06 +0530 Subject: Autofill UX improvements and code cleanup (#626) Co-authored-by: Aditya Wasan Signed-off-by: Harsh Shandilya --- .../com/zeapo/pwdstore/autofill/AutofillService.kt | 127 +++++++++------------ 1 file changed, 56 insertions(+), 71 deletions(-) (limited to 'app/src/main/java/com') diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt index cc9ff2f2..0634cdfe 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt @@ -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(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(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() } -- cgit v1.2.3