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:
parent
d350a27611
commit
74bb1c4357
3 changed files with 64 additions and 71 deletions
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue