summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/UserPreference.kt108
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.kt98
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt232
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt159
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt180
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt584
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt4
7 files changed, 35 insertions, 1330 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
index 6edd9ceb..53a49653 100644
--- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
@@ -4,7 +4,6 @@
*/
package com.zeapo.pwdstore
-import android.accessibilityservice.AccessibilityServiceInfo
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
@@ -18,10 +17,10 @@ import android.provider.DocumentsContract
import android.provider.Settings
import android.text.TextUtils
import android.view.MenuItem
-import android.view.accessibility.AccessibilityManager
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree
+import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.edit
@@ -42,7 +41,6 @@ import com.github.michaelbull.result.getOr
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
import com.zeapo.pwdstore.crypto.BasePgpActivity
import com.zeapo.pwdstore.git.GitConfigActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
@@ -173,7 +171,6 @@ class UserPreference : AppCompatActivity() {
private var autoFillEnablePreference: SwitchPreferenceCompat? = null
private var clearSavedPassPreference: Preference? = null
private var viewSshKeyPreference: Preference? = null
- private lateinit var autofillDependencies: List<Preference>
private lateinit var oreoAutofillDependencies: List<Preference>
private lateinit var prefsActivity: UserPreference
private lateinit var sharedPreferences: SharedPreferences
@@ -220,16 +217,6 @@ class UserPreference : AppCompatActivity() {
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)
- val autoFillAppsPreference = findPreference<Preference>(PreferenceKeys.AUTOFILL_APPS)
- val autoFillDefaultPreference = findPreference<CheckBoxPreference>(PreferenceKeys.AUTOFILL_DEFAULT)
- val autoFillAlwaysShowDialogPreference = findPreference<CheckBoxPreference>(PreferenceKeys.AUTOFILL_ALWAYS)
- val autoFillShowFullNamePreference = findPreference<CheckBoxPreference>(PreferenceKeys.AUTOFILL_FULL_PATH)
- autofillDependencies = listOfNotNull(
- autoFillAppsPreference,
- autoFillDefaultPreference,
- autoFillAlwaysShowDialogPreference,
- autoFillShowFullNamePreference,
- )
oreoAutofillDependencies = listOfNotNull(
oreoAutofillDirectoryStructurePreference,
oreoAutofillDefaultUsername,
@@ -347,15 +334,11 @@ class UserPreference : AppCompatActivity() {
selectExternalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo
externalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo
- autoFillAppsPreference?.onPreferenceClickListener = ClickListener {
- val intent = Intent(prefsActivity, AutofillPreferenceActivity::class.java)
- startActivity(intent)
- true
- }
-
- autoFillEnablePreference?.onPreferenceClickListener = ClickListener {
- onEnableAutofillClick()
- true
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ autoFillEnablePreference?.onPreferenceClickListener = ClickListener {
+ onEnableAutofillClick()
+ true
+ }
}
findPreference<Preference>(PreferenceKeys.EXPORT_PASSWORDS)?.apply {
@@ -474,12 +457,12 @@ class UserPreference : AppCompatActivity() {
}
private fun updateAutofillSettings() {
- val isAccessibilityServiceEnabled = prefsActivity.isAccessibilityServiceEnabled
val isAutofillServiceEnabled = prefsActivity.isAutofillServiceEnabled
- autoFillEnablePreference?.isChecked =
- isAccessibilityServiceEnabled || isAutofillServiceEnabled
- autofillDependencies.forEach {
- it.isVisible = isAccessibilityServiceEnabled
+ val isAutofillSupported = prefsActivity.isAutofillServiceSupported
+ if (!isAutofillSupported) {
+ autoFillEnablePreference?.isVisible = false
+ } else {
+ autoFillEnablePreference?.isChecked = isAutofillServiceEnabled
}
oreoAutofillDependencies.forEach {
it.isVisible = isAutofillServiceEnabled
@@ -507,51 +490,40 @@ class UserPreference : AppCompatActivity() {
viewSshKeyPreference?.isVisible = SshKey.canShowSshPublicKey
}
+ @RequiresApi(Build.VERSION_CODES.O)
private fun onEnableAutofillClick() {
- if (prefsActivity.isAccessibilityServiceEnabled) {
- startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
- } else if (prefsActivity.isAutofillServiceEnabled) {
+ if (prefsActivity.isAutofillServiceEnabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
prefsActivity.autofillManager!!.disableAutofillServices()
else
throw IllegalStateException("isAutofillServiceEnabled == true, but Build.VERSION.SDK_INT < Build.VERSION_CODES.O")
} else {
- val enableOreoAutofill = prefsActivity.isAutofillServiceSupported
MaterialAlertDialogBuilder(prefsActivity).run {
setTitle(R.string.pref_autofill_enable_title)
- if (enableOreoAutofill && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- @SuppressLint("InflateParams")
- val layout =
- layoutInflater.inflate(R.layout.oreo_autofill_instructions, null)
- val supportedBrowsersTextView =
- layout.findViewById<AppCompatTextView>(R.id.supportedBrowsers)
- supportedBrowsersTextView.text =
- getInstalledBrowsersWithAutofillSupportLevel(context).joinToString(
- separator = "\n"
- ) {
- val appLabel = it.first
- val supportDescription = when (it.second) {
- BrowserAutofillSupportLevel.None -> getString(R.string.oreo_autofill_no_support)
- BrowserAutofillSupportLevel.FlakyFill -> getString(R.string.oreo_autofill_flaky_fill_support)
- BrowserAutofillSupportLevel.PasswordFill -> getString(R.string.oreo_autofill_password_fill_support)
- BrowserAutofillSupportLevel.GeneralFill -> getString(R.string.oreo_autofill_general_fill_support)
- BrowserAutofillSupportLevel.GeneralFillAndSave -> getString(R.string.oreo_autofill_general_fill_and_save_support)
- }
- "$appLabel: $supportDescription"
+ @SuppressLint("InflateParams")
+ val layout =
+ layoutInflater.inflate(R.layout.oreo_autofill_instructions, null)
+ val supportedBrowsersTextView =
+ layout.findViewById<AppCompatTextView>(R.id.supportedBrowsers)
+ supportedBrowsersTextView.text =
+ getInstalledBrowsersWithAutofillSupportLevel(context).joinToString(
+ separator = "\n"
+ ) {
+ val appLabel = it.first
+ val supportDescription = when (it.second) {
+ BrowserAutofillSupportLevel.None -> getString(R.string.oreo_autofill_no_support)
+ BrowserAutofillSupportLevel.FlakyFill -> getString(R.string.oreo_autofill_flaky_fill_support)
+ BrowserAutofillSupportLevel.PasswordFill -> getString(R.string.oreo_autofill_password_fill_support)
+ BrowserAutofillSupportLevel.GeneralFill -> getString(R.string.oreo_autofill_general_fill_support)
+ BrowserAutofillSupportLevel.GeneralFillAndSave -> getString(R.string.oreo_autofill_general_fill_and_save_support)
}
- setView(layout)
- } else {
- setView(R.layout.autofill_instructions)
- }
+ "$appLabel: $supportDescription"
+ }
+ setView(layout)
setPositiveButton(R.string.dialog_ok) { _, _ ->
- val intent =
- if (enableOreoAutofill && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply {
- data = Uri.parse("package:${BuildConfig.APPLICATION_ID}")
- }
- } else {
- Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
- }
+ val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply {
+ data = Uri.parse("package:${BuildConfig.APPLICATION_ID}")
+ }
startActivity(intent)
}
setNegativeButton(R.string.dialog_cancel, null)
@@ -670,16 +642,6 @@ class UserPreference : AppCompatActivity() {
storeCustomXkpwdDictionaryAction.launch(arrayOf("*/*"))
}
- private val isAccessibilityServiceEnabled: 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 }
- }
-
private val isAutofillServiceSupported: Boolean
get() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.kt
deleted file mode 100644
index 3d040f13..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-@file:Suppress("Deprecation")
-
-package com.zeapo.pwdstore.autofill
-
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.content.IntentSender
-import android.content.SharedPreferences
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.content.edit
-import com.github.ajalt.timberkt.Timber.tag
-import com.github.ajalt.timberkt.e
-import com.zeapo.pwdstore.PasswordStore
-import com.zeapo.pwdstore.utils.splitLines
-import org.eclipse.jgit.util.StringUtils
-
-// blank activity started by service for calling startIntentSenderForResult
-class AutofillActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val extras = intent.extras
-
- if (extras != null && extras.containsKey("pending_intent")) {
- try {
- val pi = extras.getParcelable<PendingIntent>("pending_intent") ?: return
- startIntentSenderForResult(pi.intentSender, REQUEST_CODE_DECRYPT_AND_VERIFY, null, 0, 0, 0)
- } catch (e: IntentSender.SendIntentException) {
- tag(AutofillService.Constants.TAG).e(e) { "SendIntentException" }
- }
- } else if (extras != null && extras.containsKey("pick")) {
- val intent = Intent(applicationContext, PasswordStore::class.java)
- intent.putExtra("matchWith", true)
- startActivityForResult(intent, REQUEST_CODE_PICK)
- } else if (extras != null && extras.containsKey("pickMatchWith")) {
- val intent = Intent(applicationContext, PasswordStore::class.java)
- intent.putExtra("matchWith", true)
- startActivityForResult(intent, REQUEST_CODE_PICK_MATCH_WITH)
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- finish() // go back to the password field app
- when (requestCode) {
- REQUEST_CODE_DECRYPT_AND_VERIFY -> if (resultCode == RESULT_OK) {
- require(data != null)
- AutofillService.instance?.setResultData(data) // report the result to service
- }
- REQUEST_CODE_PICK -> if (resultCode == RESULT_OK) {
- require(data != null)
- AutofillService.instance?.setPickedPassword(data.getStringExtra("path")!!)
- }
- REQUEST_CODE_PICK_MATCH_WITH -> if (resultCode == RESULT_OK) {
- require(data != null)
- // need to not only decrypt the picked password, but also
- // update the "match with" preference
- val extras = intent.extras ?: return
- val packageName = extras.getString("packageName")
- val isWeb = extras.getBoolean("isWeb")
-
- val path = data.getStringExtra("path")
- AutofillService.instance?.setPickedPassword(data.getStringExtra("path")!!)
-
- val prefs: SharedPreferences
- prefs = if (!isWeb) {
- applicationContext.getSharedPreferences("autofill", Context.MODE_PRIVATE)
- } else {
- applicationContext.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
- }
- prefs.edit {
- when (val preference = prefs.getString(packageName, "")) {
- "", "/first", "/never" -> putString(packageName, path)
- else -> {
- val matches = arrayListOf(*preference!!.trim { it <= ' ' }.splitLines())
- matches.add(path)
- val paths = StringUtils.join(matches, "\n")
- putString(packageName, paths)
- }
- }
- }
- }
- }
- super.onActivityResult(requestCode, resultCode, data)
- }
-
- companion object {
-
- const val REQUEST_CODE_DECRYPT_AND_VERIFY = 9913
- const val REQUEST_CODE_PICK = 777
- const val REQUEST_CODE_PICK_MATCH_WITH = 778
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt
deleted file mode 100644
index 87721ad2..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-@file:Suppress("Deprecation")
-
-package com.zeapo.pwdstore.autofill
-
-import android.annotation.SuppressLint
-import android.app.Dialog
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.os.Bundle
-import android.view.View
-import android.view.ViewGroup
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
-import android.widget.EditText
-import android.widget.ListView
-import android.widget.RadioButton
-import android.widget.RadioGroup
-import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.AppCompatTextView
-import androidx.core.content.edit
-import androidx.fragment.app.DialogFragment
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.zeapo.pwdstore.PasswordStore
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.utils.resolveAttribute
-import com.zeapo.pwdstore.utils.splitLines
-
-class AutofillFragment : DialogFragment() {
-
- private var adapter: ArrayAdapter<String>? = null
- private var isWeb: Boolean = false
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val builder = MaterialAlertDialogBuilder(requireContext())
- // this fragment is only created from the settings page (AutofillPreferenceActivity)
- // need to interact with the recyclerAdapter which is a member of activity
- val callingActivity = requireActivity() as AutofillPreferenceActivity
- val inflater = callingActivity.layoutInflater
- val args = requireNotNull(arguments)
-
- @SuppressLint("InflateParams") val view = inflater.inflate(R.layout.fragment_autofill, null)
-
- builder.setView(view)
-
- val packageName = args.getString("packageName")
- val appName = args.getString("appName")
- isWeb = args.getBoolean("isWeb")
-
- // set the dialog icon and title or webURL editText
- val iconPackageName: String?
- if (!isWeb) {
- iconPackageName = packageName
- builder.setTitle(appName)
- view.findViewById<View>(R.id.webURL).visibility = View.GONE
- } else {
- val browserIntent = Intent("android.intent.action.VIEW", Uri.parse("http://"))
- val resolveInfo = requireContext().packageManager
- .resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
- iconPackageName = resolveInfo?.activityInfo?.packageName
- builder.setTitle("Website")
- (view.findViewById<View>(R.id.webURL) as EditText).setText(packageName
- ?: "com.android.browser")
- }
- try {
- if (iconPackageName != null) {
- builder.setIcon(callingActivity.packageManager.getApplicationIcon(iconPackageName))
- }
- } catch (e: PackageManager.NameNotFoundException) {
- e.printStackTrace()
- }
-
- // set up the listview now for items added by button/from preferences
- adapter = object : ArrayAdapter<String>(requireContext(), android.R.layout.simple_list_item_1, android.R.id.text1) {
- // set text color to black because default is white...
- override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
- val textView = super.getView(position, convertView, parent) as AppCompatTextView
- textView.setTextColor(requireContext().resolveAttribute(android.R.attr.textColor))
- return textView
- }
- }
- (view.findViewById<View>(R.id.matched) as ListView).adapter = adapter
- // delete items by clicking them
- (view.findViewById<View>(R.id.matched) as ListView).onItemClickListener =
- AdapterView.OnItemClickListener { _, _, position, _ ->
- adapter!!.remove(adapter!!.getItem(position))
- }
-
- // set the existing preference, if any
- val prefs: SharedPreferences = if (!isWeb) {
- callingActivity.applicationContext.getSharedPreferences("autofill", Context.MODE_PRIVATE)
- } else {
- callingActivity.applicationContext.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
- }
- when (val preference = prefs.getString(packageName, "")) {
- "" -> (view.findViewById<View>(R.id.use_default) as RadioButton).toggle()
- "/first" -> (view.findViewById<View>(R.id.first) as RadioButton).toggle()
- "/never" -> (view.findViewById<View>(R.id.never) as RadioButton).toggle()
- else -> {
- (view.findViewById<View>(R.id.match) as RadioButton).toggle()
- // trim to remove the last blank element
- adapter!!.addAll(*preference!!.trim { it <= ' ' }.splitLines())
- }
- }
-
- // add items with the + button
- val matchPassword = { _: View ->
- (view.findViewById<View>(R.id.match) as RadioButton).toggle()
- val intent = Intent(activity, PasswordStore::class.java)
- intent.putExtra("matchWith", true)
- startActivityForResult(intent, MATCH_WITH)
- }
- view.findViewById<View>(R.id.matchButton).setOnClickListener(matchPassword)
-
- // write to preferences when OK clicked
- builder.setPositiveButton(R.string.dialog_ok) { _, _ -> }
- builder.setNegativeButton(R.string.dialog_cancel, null)
- if (isWeb) {
- builder.setNeutralButton(R.string.autofill_apps_delete) { _, _ ->
- if (callingActivity.recyclerAdapter != null &&
- packageName != null && packageName != "") {
- prefs.edit {
- remove(packageName)
- callingActivity.recyclerAdapter?.removeWebsite(packageName)
- }
- }
- }
- }
- return builder.create()
- }
-
- // need to the onClick here for buttons to dismiss dialog only when wanted
- override fun onStart() {
- super.onStart()
- val ad = dialog as? AlertDialog
- if (ad != null) {
- val positiveButton = ad.getButton(Dialog.BUTTON_POSITIVE)
- positiveButton.setOnClickListener {
- val callingActivity = requireActivity() as AutofillPreferenceActivity
- val dialog = requireDialog()
- val args = requireNotNull(arguments)
-
- val prefs: SharedPreferences = if (!isWeb) {
- callingActivity.applicationContext.getSharedPreferences("autofill", Context.MODE_PRIVATE)
- } else {
- callingActivity.applicationContext.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
- }
-
- var packageName = args.getString("packageName", "")
- if (isWeb) {
- // handle some errors and don't dismiss the dialog
- val webURL = dialog.findViewById<EditText>(R.id.webURL)
-
- packageName = webURL.text.toString()
-
- if (packageName == "") {
- webURL.error = "URL cannot be blank"
- return@setOnClickListener
- }
- val oldPackageName = args.getString("packageName", "")
- if (oldPackageName != packageName && prefs.all.containsKey(packageName)) {
- webURL.error = "URL already exists"
- return@setOnClickListener
- }
- }
-
- // write to preferences accordingly
- prefs.edit {
- val radioGroup = dialog.findViewById<RadioGroup>(R.id.autofill_radiogroup)
- when (radioGroup.checkedRadioButtonId) {
- R.id.use_default -> if (!isWeb) {
- remove(packageName)
- } else {
- putString(packageName, "")
- }
- R.id.first -> putString(packageName, "/first")
- R.id.never -> putString(packageName, "/never")
- else -> {
- val paths = StringBuilder()
- for (i in 0 until adapter!!.count) {
- paths.append(adapter!!.getItem(i))
- if (i != adapter!!.count) {
- paths.append("\n")
- }
- }
- putString(packageName, paths.toString())
- }
- }
- }
-
- // notify the recycler adapter if it is loaded
- callingActivity.recyclerAdapter?.apply {
- val position: Int
- if (!isWeb) {
- val appName = args.getString("appName", "")
- position = getPosition(appName)
- notifyItemChanged(position)
- } else {
- position = getPosition(packageName)
- when (val oldPackageName = args.getString("packageName", "")) {
- packageName -> notifyItemChanged(position)
- "" -> addWebsite(packageName)
- else -> {
- prefs.edit { remove(oldPackageName) }
- updateWebsite(oldPackageName, packageName)
- }
- }
- }
- }
- dismiss()
- }
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (resultCode == AppCompatActivity.RESULT_OK && data != null) {
- adapter!!.add(data.getStringExtra("path"))
- }
- }
-
- companion object {
-
- private const val MATCH_WITH = 777
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt
deleted file mode 100644
index feef77a5..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-@file:Suppress("Deprecation")
-
-package com.zeapo.pwdstore.autofill
-
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.os.AsyncTask
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.widget.SearchView
-import androidx.recyclerview.widget.DividerItemDecoration
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.google.android.material.floatingactionbutton.FloatingActionButton
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.databinding.AutofillRecyclerViewBinding
-import com.zeapo.pwdstore.utils.viewBinding
-import java.lang.ref.WeakReference
-import java.util.ArrayList
-import me.zhanghai.android.fastscroll.FastScrollerBuilder
-
-class AutofillPreferenceActivity : AppCompatActivity() {
-
- private val binding by viewBinding(AutofillRecyclerViewBinding::inflate)
- internal var recyclerAdapter: AutofillRecyclerAdapter? = null // let fragment have access
- private var pm: PackageManager? = null
-
- private var recreate: Boolean = false // flag for action on up press; origin autofill dialog? different act
-
- public override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(binding.root)
-
- val layoutManager = LinearLayoutManager(this)
- with(binding) {
- autofillRecycler.layoutManager = layoutManager
- autofillRecycler.addItemDecoration(DividerItemDecoration(this@AutofillPreferenceActivity, DividerItemDecoration.VERTICAL))
- FastScrollerBuilder(autofillRecycler).build()
- }
-
- pm = packageManager
-
- PopulateTask(this).execute()
-
- // if the preference activity was started from the autofill dialog
- recreate = false
- val extras = intent.extras
- if (extras != null) {
- recreate = true
-
- showDialog(extras.getString("packageName"), extras.getString("appName"), extras.getBoolean("isWeb"))
- }
-
- title = "Autofill Apps"
-
- val fab = findViewById<FloatingActionButton>(R.id.fab)
- fab.setOnClickListener { showDialog("", "", true) }
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- // Inflate the menu; this adds items to the action bar if it is present.
- menuInflater.inflate(R.menu.autofill_preference, menu)
- val searchItem = menu.findItem(R.id.action_search)
- val searchView = searchItem.actionView as SearchView
-
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(s: String): Boolean {
- return false
- }
-
- override fun onQueryTextChange(s: String): Boolean {
- if (recyclerAdapter != null) {
- recyclerAdapter!!.filter(s)
- }
- return true
- }
- })
-
- return super.onCreateOptionsMenu(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- // in service, we CLEAR_TASK. then we set the recreate flag.
- // something of a hack, but w/o CLEAR_TASK, behaviour was unpredictable
- return if (item.itemId == android.R.id.home) {
- onBackPressed()
- true
- } else super.onOptionsItemSelected(item)
- }
-
- fun showDialog(packageName: String?, appName: String?, isWeb: Boolean) {
- val df = AutofillFragment()
- val args = Bundle()
- args.putString("packageName", packageName)
- args.putString("appName", appName)
- args.putBoolean("isWeb", isWeb)
- df.arguments = args
- df.show(supportFragmentManager, "autofill_dialog")
- }
-
- companion object {
- private class PopulateTask(activity: AutofillPreferenceActivity) : AsyncTask<Void, Void, Void>() {
-
- val weakReference = WeakReference(activity)
-
- override fun onPreExecute() {
- weakReference.get()?.apply {
- runOnUiThread { findViewById<View>(R.id.progress_bar).visibility = View.VISIBLE }
- }
- }
-
- override fun doInBackground(vararg params: Void): Void? {
- val pm = weakReference.get()?.pm ?: return null
- val intent = Intent(Intent.ACTION_MAIN)
- intent.addCategory(Intent.CATEGORY_LAUNCHER)
- val allAppsResolveInfo = pm.queryIntentActivities(intent, 0)
- val allApps = ArrayList<AutofillRecyclerAdapter.AppInfo>()
-
- for (app in allAppsResolveInfo) {
- allApps.add(AutofillRecyclerAdapter.AppInfo(app.activityInfo.packageName, app.loadLabel(pm).toString(), false, app.loadIcon(pm)))
- }
-
- val prefs = weakReference.get()?.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
- val prefsMap = prefs!!.all
- for (key in prefsMap.keys) {
- try {
- allApps.add(AutofillRecyclerAdapter.AppInfo(key, key, true, pm.getApplicationIcon("com.android.browser")))
- } catch (e: PackageManager.NameNotFoundException) {
- allApps.add(AutofillRecyclerAdapter.AppInfo(key, key, true, null))
- }
- }
- weakReference.get()?.recyclerAdapter = AutofillRecyclerAdapter(allApps, weakReference.get()!!)
- return null
- }
-
- override fun onPostExecute(ignored: Void?) {
- weakReference.get()?.apply {
- runOnUiThread {
- with(binding) {
- progressBar.visibility = View.GONE
- autofillRecycler.adapter = recyclerAdapter
- val extras = intent.extras
- if (extras != null) {
- autofillRecycler.scrollToPosition(recyclerAdapter!!.getPosition(extras.getString("appName")!!))
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt
deleted file mode 100644
index ac53d32a..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-@file:Suppress("Deprecation")
-
-package com.zeapo.pwdstore.autofill
-
-import android.content.Context
-import android.content.SharedPreferences
-import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.appcompat.widget.AppCompatTextView
-import androidx.recyclerview.widget.RecyclerView
-import androidx.recyclerview.widget.SortedList
-import androidx.recyclerview.widget.SortedListAdapterCallback
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.utils.splitLines
-import java.util.ArrayList
-import java.util.Locale
-import me.zhanghai.android.fastscroll.PopupTextProvider
-
-internal class AutofillRecyclerAdapter(
- allApps: List<AppInfo>,
- private val activity: AutofillPreferenceActivity
-) : RecyclerView.Adapter<AutofillRecyclerAdapter.ViewHolder>(), PopupTextProvider {
-
- private val apps: SortedList<AppInfo>
- private val allApps: ArrayList<AppInfo> // for filtering, maintain a list of all
- private var browserIcon: Drawable? = null
-
- init {
- val callback = object : SortedListAdapterCallback<AppInfo>(this) {
- // don't take into account secondary text. This is good enough
- // for the limited add/remove usage for websites
- override fun compare(o1: AppInfo, o2: AppInfo): Int {
- return o1.appName.toLowerCase(Locale.ROOT).compareTo(o2.appName.toLowerCase(Locale.ROOT))
- }
-
- override fun areContentsTheSame(oldItem: AppInfo, newItem: AppInfo): Boolean {
- return oldItem.appName == newItem.appName
- }
-
- override fun areItemsTheSame(item1: AppInfo, item2: AppInfo): Boolean {
- return item1.appName == item2.appName
- }
- }
- apps = SortedList(AppInfo::class.java, callback)
- apps.addAll(allApps)
- this.allApps = ArrayList(allApps)
- try {
- browserIcon = activity.packageManager.getApplicationIcon("com.android.browser")
- } catch (e: PackageManager.NameNotFoundException) {
- e.printStackTrace()
- }
- }
-
- override fun getPopupText(position: Int): String {
- return allApps[position].appName[0].toString().toUpperCase(Locale.getDefault())
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- val v = LayoutInflater.from(parent.context)
- .inflate(R.layout.autofill_row_layout, parent, false)
- return ViewHolder(v)
- }
-
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- val app = apps.get(position)
- holder.packageName = app.packageName
- holder.appName = app.appName
- holder.isWeb = app.isWeb
-
- holder.icon.setImageDrawable(app.icon)
- holder.name.text = app.appName
-
- holder.secondary.visibility = View.VISIBLE
-
- val prefs: SharedPreferences
- prefs = if (app.appName != app.packageName) {
- activity.applicationContext.getSharedPreferences("autofill", Context.MODE_PRIVATE)
- } else {
- activity.applicationContext.getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
- }
- when (val preference = prefs.getString(holder.packageName, "")) {
- "" -> {
- holder.secondary.visibility = View.GONE
- holder.view.setBackgroundResource(0)
- }
- "/first" -> holder.secondary.setText(R.string.autofill_apps_first)
- "/never" -> holder.secondary.setText(R.string.autofill_apps_never)
- else -> {
- holder.secondary.setText(R.string.autofill_apps_match)
- holder.secondary.append(" " + preference!!.splitLines()[0])
- if (preference.trim { it <= ' ' }.splitLines().size - 1 > 0) {
- holder.secondary.append(" and " +
- (preference.trim { it <= ' ' }.splitLines().size - 1) + " more")
- }
- }
- }
- }
-
- override fun getItemCount(): Int {
- return apps.size()
- }
-
- fun getPosition(appName: String): Int {
- return apps.indexOf(AppInfo(null, appName, false, null))
- }
-
- // for websites, URL = packageName == appName
- fun addWebsite(packageName: String) {
- apps.add(AppInfo(packageName, packageName, true, browserIcon))
- allApps.add(AppInfo(packageName, packageName, true, browserIcon))
- }
-
- fun removeWebsite(packageName: String) {
- apps.remove(AppInfo(null, packageName, false, null))
- allApps.remove(AppInfo(null, packageName, false, null)) // compare with equals
- }
-
- fun updateWebsite(oldPackageName: String, packageName: String) {
- apps.updateItemAt(getPosition(oldPackageName), AppInfo(packageName, packageName, true, browserIcon))
- allApps.remove(AppInfo(null, oldPackageName, false, null)) // compare with equals
- allApps.add(AppInfo(null, packageName, false, null))
- }
-
- fun filter(s: String) {
- if (s.isEmpty()) {
- apps.addAll(allApps)
- return
- }
- apps.beginBatchedUpdates()
- for (app in allApps) {
- if (app.appName.toLowerCase(Locale.ROOT).contains(s.toLowerCase(Locale.ROOT))) {
- apps.add(app)
- } else {
- apps.remove(app)
- }
- }
- apps.endBatchedUpdates()
- }
-
- internal class AppInfo(var packageName: String?, var appName: String, var isWeb: Boolean, var icon: Drawable?) {
-
- override fun equals(other: Any?): Boolean {
- return other is AppInfo && this.appName == other.appName
- }
-
- override fun hashCode(): Int {
- var result = packageName?.hashCode() ?: 0
- result = 31 * result + appName.hashCode()
- result = 31 * result + isWeb.hashCode()
- result = 31 * result + (icon?.hashCode() ?: 0)
- return result
- }
- }
-
- internal inner class ViewHolder(var view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
-
- var name: AppCompatTextView = view.findViewById(R.id.app_name)
- var icon: AppCompatImageView = view.findViewById(R.id.app_icon)
- var secondary: AppCompatTextView = view.findViewById(R.id.secondary_text)
- var packageName: String? = null
- var appName: String? = null
- var isWeb: Boolean = false
-
- init {
- view.setOnClickListener(this)
- }
-
- override fun onClick(v: View) {
- activity.showDialog(packageName, appName, isWeb)
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
deleted file mode 100644
index 4b40d8a4..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
+++ /dev/null
@@ -1,584 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-@file:Suppress("Deprecation")
-
-package com.zeapo.pwdstore.autofill
-
-import android.accessibilityservice.AccessibilityService
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.os.Build
-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.github.ajalt.timberkt.Timber.tag
-import com.github.ajalt.timberkt.e
-import com.github.ajalt.timberkt.i
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.model.PasswordEntry
-import com.zeapo.pwdstore.utils.PasswordRepository
-import com.zeapo.pwdstore.utils.PreferenceKeys
-import com.zeapo.pwdstore.utils.splitLines
-import java.io.ByteArrayOutputStream
-import java.io.File
-import java.io.IOException
-import java.io.InputStream
-import java.io.UnsupportedEncodingException
-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.openintents.openpgp.IOpenPgpService2
-import org.openintents.openpgp.OpenPgpError
-
-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)
- private var items: ArrayList<File> = arrayListOf() // password choices
- private var lastWhichItem: Int = 0
- private var dialog: AlertDialog? = null
- private var window: AccessibilityWindowInfo? = null
- private var resultData: Intent? = null // need the intent which contains results from user interaction
- private var packageName: CharSequence? = null
- private var ignoreActionFocus = false
- private var webViewTitle: String? = null
- private var webViewURL: String? = null
- private var lastPassword: PasswordEntry? = null
- private var lastPasswordMaxDate: Long = 0
-
- fun setResultData(data: Intent) {
- resultData = data
- }
-
- fun setPickedPassword(path: String) {
- items.add(File("${PasswordRepository.getRepositoryDirectory()}/$path.gpg"))
- bindDecryptAndVerify()
- }
-
- override fun onCreate() {
- super.onCreate()
- instance = this
- }
-
- override fun onDestroy() {
- super.onDestroy()
- instance = null
- cancel()
- }
-
- override fun onServiceConnected() {
- super.onServiceConnected()
- serviceConnection = OpenPgpServiceConnection(this@AutofillService, "org.sufficientlysecure.keychain")
- serviceConnection!!.bindToService()
- settings = PreferenceManager.getDefaultSharedPreferences(this)
- }
-
- override fun onAccessibilityEvent(event: AccessibilityEvent) {
- // remove stored password from cache
- if (lastPassword != null && System.currentTimeMillis() > lastPasswordMaxDate) {
- lastPassword = null
- }
-
- // if returning to the source app from a successful AutofillActivity
- if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
- event.packageName != null && event.packageName == packageName &&
- resultData != null) {
- bindDecryptAndVerify()
- }
-
- // look for webView and trigger accessibility events if window changes
- // or if page changes in chrome
- if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED &&
- event.packageName != null &&
- (event.packageName == "com.android.chrome" || event.packageName == "com.android.browser"))) {
- // there is a chance for getRootInActiveWindow() to return null at any time. save it.
- try {
- val root = rootInActiveWindow
- webViewTitle = searchWebView(root)
- webViewURL = null
- if (webViewTitle != null) {
- var nodes = root.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
- if (nodes.isEmpty()) {
- nodes = root.findAccessibilityNodeInfosByViewId("com.android.browser:id/url")
- }
- for (node in nodes)
- if (node.text != null) {
- try {
- webViewURL = URL(node.text.toString()).host
- } catch (e: MalformedURLException) {
- if (e.toString().contains("Protocol not found")) {
- try {
- webViewURL = URL("http://" + node.text.toString()).host
- } catch (ignored: MalformedURLException) {
- }
- }
- }
- }
- }
- } catch (e: Exception) {
- // sadly we were unable to access the data we wanted
- return
- }
- }
-
- // nothing to do if field is keychain app or system ui
- 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()
- return
- }
-
- if (!event.isPassword) {
- if (lastPassword != null && event.eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED && event.source.isEditable) {
- showPasteUsernameDialog(event.source, lastPassword!!)
- return
- } else {
- // nothing to do if not password field focus
- dismissDialog()
- return
- }
- }
-
- if (dialog != null && dialog!!.isShowing) {
- // the current dialog must belong to this window; ignore clicks on this password field
- // why handle clicks at all then? some cases e.g. Paypal there is no initial focus event
- if (event.eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) {
- return
- }
- // if it was not a click, the field was refocused or another field was focused; recreate
- dialog!!.dismiss()
- dialog = null
- }
-
- // ignore the ACTION_FOCUS from decryptAndVerify otherwise dialog will appear after Fill
- if (ignoreActionFocus) {
- ignoreActionFocus = false
- return
- }
-
- // need to request permission before attempting to draw dialog
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
- val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
- Uri.parse("package:" + getPackageName()))
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
- return
- }
-
- // we are now going to attempt to fill, save AccessibilityNodeInfo for later in decryptAndVerify
- // (there should be a proper way to do this, although this seems to work 90% of the time)
- info = event.source
- if (info == null) return
-
- // save the dialog's corresponding window so we can use getWindows() in dismissDialog
- window = info!!.window
-
- val packageName: String
- val appName: String
- val isWeb: Boolean
-
- // Match with the app if a webview was not found or one was found but
- // there's no title or url to go by
- if (webViewTitle == null || webViewTitle == "" && webViewURL == null) {
- if (info!!.packageName == null) return
- packageName = info!!.packageName.toString()
-
- // get the app name and find a corresponding password
- val packageManager = packageManager
- val applicationInfo: ApplicationInfo? = try {
- packageManager.getApplicationInfo(event.packageName.toString(), 0)
- } catch (e: PackageManager.NameNotFoundException) {
- null
- }
-
- appName = (if (applicationInfo != null) packageManager.getApplicationLabel(applicationInfo) else "").toString()
-
- isWeb = false
-
- setAppMatchingPasswords(appName, packageName)
- } else {
- // now we may have found a title but webViewURL could be null
- // we set packagename so that we can find the website setting entry
- packageName = setWebMatchingPasswords(webViewTitle!!, webViewURL)
- appName = packageName
- isWeb = true
- }
-
- // if autofill_always checked, show dialog even if no matches (automatic
- // or otherwise)
- if (items.isEmpty() && !settings!!.getBoolean(PreferenceKeys.AUTOFILL_ALWAYS, false)) {
- return
- }
- showSelectPasswordDialog(packageName, appName, isWeb)
- }
-
- private fun searchWebView(source: AccessibilityNodeInfo?, depth: Int = 10): String? {
- if (source == null || depth == 0) {
- return null
- }
- for (i in 0 until source.childCount) {
- val u = source.getChild(i) ?: continue
- if (u.className != null && u.className == "android.webkit.WebView") {
- return if (u.contentDescription != null) {
- u.contentDescription.toString()
- } else ""
- }
- val webView = searchWebView(u, depth - 1)
- if (webView != null) {
- return webView
- }
- u.recycle()
- }
- return null
- }
-
- // dismiss the dialog if the window has changed
- private fun dismissDialog() {
- val dismiss = !windows.contains(window)
- if (dismiss && dialog != null && dialog!!.isShowing) {
- dialog!!.dismiss()
- dialog = null
- }
- }
-
- private fun setWebMatchingPasswords(webViewTitle: String, webViewURL: String?): String {
- // Return the URL needed to open the corresponding Settings.
- var settingsURL = webViewURL
-
- // if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
- val defValue = if (settings!!.getBoolean(PreferenceKeys.AUTOFILL_DEFAULT, true)) "/first" else "/never"
- val prefs: SharedPreferences = getSharedPreferences("autofill_web", Context.MODE_PRIVATE)
- var preference: String
-
- preference = defValue
- if (webViewURL != null) {
- val webViewUrlLowerCase = webViewURL.toLowerCase(Locale.ROOT)
- val prefsMap = prefs.all
- for (key in prefsMap.keys) {
- // for websites unlike apps there can be blank preference of "" which
- // means use default, so ignore it.
- val value = prefs.getString(key, null)
- val keyLowerCase = key.toLowerCase(Locale.ROOT)
- if (value != null && value != "" &&
- (webViewUrlLowerCase.contains(keyLowerCase) || keyLowerCase.contains(webViewUrlLowerCase))) {
- preference = value
- settingsURL = key
- }
- }
- }
-
- when (preference) {
- "/first" -> {
- if (!PasswordRepository.isInitialized) {
- PasswordRepository.initialize()
- }
- items = searchPasswords(PasswordRepository.getRepositoryDirectory(), webViewTitle)
- }
- "/never" -> items = ArrayList()
- else -> getPreferredPasswords(preference)
- }
-
- return settingsURL!!
- }
-
- private fun setAppMatchingPasswords(appName: String, packageName: String) {
- // if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
- val defValue = if (settings!!.getBoolean(PreferenceKeys.AUTOFILL_DEFAULT, true)) "/first" else "/never"
- val prefs: SharedPreferences = getSharedPreferences("autofill", Context.MODE_PRIVATE)
- val preference: String?
-
- preference = prefs.getString(packageName, defValue) ?: defValue
-
- when (preference) {
- "/first" -> {
- if (!PasswordRepository.isInitialized) {
- PasswordRepository.initialize()
- }
- items = searchPasswords(PasswordRepository.getRepositoryDirectory(), appName)
- }
- "/never" -> items = ArrayList()
- else -> getPreferredPasswords(preference)
- }
- }
-
- // Put the newline separated list of passwords from the SharedPreferences
- // file into the items list.
- private fun getPreferredPasswords(preference: String) {
- if (!PasswordRepository.isInitialized) {
- PasswordRepository.initialize()
- }
- val preferredPasswords = preference.splitLines()
- items = ArrayList()
- for (password in preferredPasswords) {
- val path = PasswordRepository.getRepositoryDirectory().toString() + "/" + password + ".gpg"
- if (File(path).exists()) {
- items.add(File(path))
- }
- }
- }
-
- private fun searchPasswords(path: File?, appName: String): ArrayList<File> {
- val passList = PasswordRepository.getFilesList(path)
-
- if (passList.size == 0) return ArrayList()
-
- val items = ArrayList<File>()
-
- for (file in passList) {
- if (file.isFile) {
- if (!file.isHidden && appName.toLowerCase(Locale.ROOT).contains(file.name.toLowerCase(Locale.ROOT).replace(".gpg", ""))) {
- items.add(file)
- }
- } else {
- if (!file.isHidden) {
- items.addAll(searchPasswords(file, appName))
- }
- }
- }
- return items
- }
-
- private fun showPasteUsernameDialog(node: AccessibilityNodeInfo, password: PasswordEntry) {
- if (dialog != null) {
- dialog!!.dismiss()
- dialog = null
- }
-
- val builder = MaterialAlertDialogBuilder(this, R.style.AppTheme_Dialog)
- builder.setNegativeButton(R.string.dialog_cancel) { _, _ ->
- dialog!!.dismiss()
- dialog = null
- }
- builder.setPositiveButton(R.string.autofill_paste) { _, _ ->
- pasteText(node, password.username)
- dialog!!.dismiss()
- dialog = null
- }
- builder.setMessage(getString(R.string.autofill_paste_username, password.username))
-
- dialog = builder.create()
- 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()
- }
-
- private fun showSelectPasswordDialog(packageName: String, appName: String, isWeb: Boolean) {
- if (dialog != null) {
- dialog!!.dismiss()
- dialog = null
- }
-
- val builder = MaterialAlertDialogBuilder(this, R.style.AppTheme_Dialog)
- builder.setNegativeButton(R.string.dialog_cancel) { _, _ ->
- dialog!!.dismiss()
- dialog = null
- }
- builder.setNeutralButton("Settings") { _, _ ->
- // TODO make icon? gear?
- // the user will have to return to the app themselves.
- val intent = Intent(this@AutofillService, AutofillPreferenceActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
- intent.putExtra("packageName", packageName)
- intent.putExtra("appName", appName)
- intent.putExtra("isWeb", isWeb)
- startActivity(intent)
- }
-
- // populate the dialog items, always with pick + pick and match. Could
- // make it optional (or make height a setting for the same effect)
- val itemNames = arrayOfNulls<CharSequence>(items.size + 2)
- val passwordDirectory = PasswordRepository.getRepositoryDirectory().toString()
- val autofillFullPath = settings!!.getBoolean(PreferenceKeys.AUTOFILL_FULL_PATH, false)
- for (i in items.indices) {
- if (autofillFullPath) {
- itemNames[i] = items[i].path.replace(".gpg", "")
- .replace("$passwordDirectory/", "")
- } else {
- itemNames[i] = items[i].name.replace(".gpg", "")
- }
- }
- itemNames[items.size] = getString(R.string.autofill_pick)
- itemNames[items.size + 1] = getString(R.string.autofill_pick_and_match)
- builder.setItems(itemNames) { _, which ->
- lastWhichItem = which
- when {
- which < items.size -> bindDecryptAndVerify()
- which == items.size -> {
- val intent = Intent(this@AutofillService, AutofillActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
- intent.putExtra("pick", true)
- startActivity(intent)
- }
- else -> {
- lastWhichItem-- // will add one element to items, so lastWhichItem=items.size()+1
- val intent = Intent(this@AutofillService, AutofillActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
- intent.putExtra("pickMatchWith", true)
- intent.putExtra("packageName", packageName)
- intent.putExtra("isWeb", isWeb)
- startActivity(intent)
- }
- }
- }
-
- dialog = builder.create()
- dialog?.window?.apply {
- setDialogType(this)
- val density = context.resources.displayMetrics.density
- addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
- setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
- clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
- // arbitrary non-annoying size
- setLayout((340 * density).toInt(), WRAP_CONTENT)
- }
- dialog?.show()
- }
-
- @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() {}
-
- private fun bindDecryptAndVerify() {
- if (serviceConnection!!.service == null) {
- // the service was disconnected, need to bind again
- // give it a listener and in the callback we will decryptAndVerify
- serviceConnection = OpenPgpServiceConnection(this@AutofillService, "org.sufficientlysecure.keychain", OnBoundListener())
- serviceConnection!!.bindToService()
- } else {
- decryptAndVerify()
- }
- }
-
- private fun decryptAndVerify() = launch {
- packageName = info!!.packageName
- val data: Intent
- if (resultData == null) {
- data = Intent()
- data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY
- } else {
- data = resultData!!
- resultData = null
- }
-
- var inputStream: InputStream? = null
- withContext(Dispatchers.IO) {
- try {
- inputStream = items[lastWhichItem].inputStream()
- } catch (e: IOException) {
- e.printStackTrace()
- cancel("", e)
- }
- }
-
- val os = ByteArrayOutputStream()
-
- val api = OpenPgpApi(this@AutofillService, serviceConnection!!.service!!)
- val result = api.executeApi(data, inputStream, os)
- when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
- OpenPgpApi.RESULT_CODE_SUCCESS -> {
- try {
- 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() == true) {
- lastPassword = entry
- val ttl = Integer.parseInt(settings!!.getString(PreferenceKeys.GENERAL_SHOW_TIME, "45")!!)
- withContext(Dispatchers.Main) { Toast.makeText(applicationContext, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show() }
- lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L
- }
- } catch (e: UnsupportedEncodingException) {
- tag(Constants.TAG).e(e)
- }
- }
- OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
- 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(applicationContext, AutofillActivity::class.java)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
- intent.putExtra("pending_intent", pi)
- startActivity(intent)
- }
- OpenPgpApi.RESULT_CODE_ERROR -> {
- val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR)
- if (error != null) {
- withContext(Dispatchers.Main) { Toast.makeText(applicationContext, "Error from OpenKeyChain : ${error.message}", Toast.LENGTH_LONG).show() }
- tag(Constants.TAG).e { "onError getErrorId: ${error.errorId}" }
- tag(Constants.TAG).e { "onError getMessage: ${error.message}" }
- }
- }
- }
- }
-
- private fun pasteText(node: AccessibilityNodeInfo, text: String?) {
- // if the user focused on something else, take focus back
- // 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)
- val args = bundleOf(Pair(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text))
- node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
- node.recycle()
- }
-
- internal object Constants {
-
- const val TAG = "Keychain"
- }
-
- private inner class OnBoundListener : OpenPgpServiceConnection.OnBound {
-
- override fun onBound(service: IOpenPgpService2) {
- decryptAndVerify()
- }
-
- override fun onError(e: Exception) {
- e.printStackTrace()
- }
- }
-
- companion object {
-
- var instance: AutofillService? = null
- private set
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
index 33130f78..536a0c19 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
@@ -9,11 +9,7 @@ object PreferenceKeys {
const val APP_THEME = "app_theme"
const val APP_VERSION = "app_version"
- const val AUTOFILL_APPS = "autofill_apps"
- const val AUTOFILL_ALWAYS = "autofill_always"
- const val AUTOFILL_DEFAULT = "autofill_default"
const val AUTOFILL_ENABLE = "autofill_enable"
- const val AUTOFILL_FULL_PATH = "autofill_full_path"
const val BIOMETRIC_AUTH = "biometric_auth"
const val CLEAR_CLIPBOARD_20X = "clear_clipboard_20x"
const val CLEAR_SAVED_PASS = "clear_saved_pass"