diff options
Diffstat (limited to 'app/src/main/java')
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" |