summaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'app/src')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java101
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.kt88
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java235
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt211
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java165
-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.java192
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt170
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java606
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt582
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt26
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt5
12 files changed, 1228 insertions, 1312 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java
deleted file mode 100644
index 12d1de74..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java
+++ /dev/null
@@ -1,101 +0,0 @@
-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 android.util.Log;
-import androidx.appcompat.app.AppCompatActivity;
-import com.zeapo.pwdstore.PasswordStore;
-import org.eclipse.jgit.util.StringUtils;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-// blank activity started by service for calling startIntentSenderForResult
-public class AutofillActivity extends AppCompatActivity {
- public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913;
- public static final int REQUEST_CODE_PICK = 777;
- public static final int REQUEST_CODE_PICK_MATCH_WITH = 778;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Bundle extras = getIntent().getExtras();
-
- if (extras != null && extras.containsKey("pending_intent")) {
- try {
- PendingIntent pi = extras.getParcelable("pending_intent");
- if (pi == null) {
- return;
- }
- startIntentSenderForResult(pi.getIntentSender()
- , REQUEST_CODE_DECRYPT_AND_VERIFY, null, 0, 0, 0);
- } catch (IntentSender.SendIntentException e) {
- Log.e(AutofillService.Constants.TAG, "SendIntentException", e);
- }
- } else if (extras != null && extras.containsKey("pick")) {
- Intent intent = new Intent(getApplicationContext(), PasswordStore.class);
- intent.putExtra("matchWith", true);
- startActivityForResult(intent, REQUEST_CODE_PICK);
- } else if (extras != null && extras.containsKey("pickMatchWith")) {
- Intent intent = new Intent(getApplicationContext(), PasswordStore.class);
- intent.putExtra("matchWith", true);
- startActivityForResult(intent, REQUEST_CODE_PICK_MATCH_WITH);
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- finish(); // go back to the password field app
- switch (requestCode) {
- case REQUEST_CODE_DECRYPT_AND_VERIFY:
- if (resultCode == RESULT_OK) {
- AutofillService.getInstance().setResultData(data); // report the result to service
- }
- break;
- case REQUEST_CODE_PICK:
- if (resultCode == RESULT_OK) {
- AutofillService.getInstance().setPickedPassword(data.getStringExtra("path"));
- }
- break;
- case REQUEST_CODE_PICK_MATCH_WITH:
- if (resultCode == RESULT_OK) {
- // need to not only decrypt the picked password, but also
- // update the "match with" preference
- Bundle extras = getIntent().getExtras();
- String packageName = extras.getString("packageName");
- boolean isWeb = extras.getBoolean("isWeb");
-
- String path = data.getStringExtra("path");
- AutofillService.getInstance().setPickedPassword(data.getStringExtra("path"));
-
- SharedPreferences prefs;
- if (!isWeb) {
- prefs = getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
- } else {
- prefs = getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
- }
- SharedPreferences.Editor editor = prefs.edit();
- String preference = prefs.getString(packageName, "");
- switch (preference) {
- case "":
- case "/first":
- case "/never":
- editor.putString(packageName, path);
- break;
- default:
- List<String> matches = new ArrayList<>(Arrays.asList(preference.trim().split("\n")));
- matches.add(path);
- String paths = StringUtils.join(matches, "\n");
- editor.putString(packageName, paths);
- }
- editor.apply();
- }
- break;
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.kt
new file mode 100644
index 00000000..9bf2f085
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.kt
@@ -0,0 +1,88 @@
+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 android.util.Log
+import androidx.appcompat.app.AppCompatActivity
+import com.zeapo.pwdstore.PasswordStore
+import com.zeapo.pwdstore.utils.splitLines
+import org.eclipse.jgit.util.StringUtils
+import java.util.ArrayList
+import java.util.Arrays
+
+// 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) {
+ Log.e(AutofillService.Constants.TAG, "SendIntentException", e)
+ }
+
+ } 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) {
+ AutofillService.instance?.setResultData(data!!) // report the result to service
+ }
+ REQUEST_CODE_PICK -> if (resultCode == RESULT_OK) {
+ AutofillService.instance?.setPickedPassword(data!!.getStringExtra("path"))
+ }
+ REQUEST_CODE_PICK_MATCH_WITH -> if (resultCode == RESULT_OK) {
+ // 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)
+ }
+ val editor = prefs.edit()
+ when (val preference = prefs.getString(packageName, "")) {
+ "", "/first", "/never" -> editor.putString(packageName, path)
+ else -> {
+ val matches = ArrayList(Arrays.asList(*preference!!.trim { it <= ' ' }.splitLines()))
+ matches.add(path)
+ val paths = StringUtils.join(matches, "\n")
+ editor.putString(packageName, paths)
+ }
+ }
+ editor.apply()
+ }
+ }
+ }
+
+ 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.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java
deleted file mode 100644
index d7095dae..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java
+++ /dev/null
@@ -1,235 +0,0 @@
-package com.zeapo.pwdstore.autofill;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ListView;
-import android.widget.RadioButton;
-import android.widget.RadioGroup;
-import android.widget.TextView;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.core.content.ContextCompat;
-import com.zeapo.pwdstore.PasswordStore;
-import com.zeapo.pwdstore.R;
-
-public class AutofillFragment extends DialogFragment {
- private static final int MATCH_WITH = 777;
- private ArrayAdapter<String> adapter;
- private boolean isWeb;
-
- public AutofillFragment() {
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- // this fragment is only created from the settings page (AutofillPreferenceActivity)
- // need to interact with the recyclerAdapter which is a member of activity
- final AutofillPreferenceActivity callingActivity = (AutofillPreferenceActivity) getActivity();
- LayoutInflater inflater = callingActivity.getLayoutInflater();
-
- @SuppressLint("InflateParams") final View view = inflater.inflate(R.layout.fragment_autofill, null);
-
- builder.setView(view);
-
- final String packageName = getArguments().getString("packageName");
- final String appName = getArguments().getString("appName");
- isWeb = getArguments().getBoolean("isWeb");
-
- // set the dialog icon and title or webURL editText
- String iconPackageName;
- if (!isWeb) {
- iconPackageName = packageName;
- builder.setTitle(appName);
- view.findViewById(R.id.webURL).setVisibility(View.GONE);
- } else {
- iconPackageName = "com.android.browser";
- builder.setTitle("Website");
- ((EditText) view.findViewById(R.id.webURL)).setText(packageName);
- }
- try {
- builder.setIcon(callingActivity.getPackageManager().getApplicationIcon(iconPackageName));
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
-
- // set up the listview now for items added by button/from preferences
- adapter = new ArrayAdapter<String>(getActivity().getApplicationContext()
- , android.R.layout.simple_list_item_1, android.R.id.text1) {
- // set text color to black because default is white...
- @NonNull
- @Override
- public View getView(int position, View convertView, @NonNull ViewGroup parent) {
- TextView textView = (TextView) super.getView(position, convertView, parent);
- textView.setTextColor(ContextCompat.getColor(getContext(), R.color.grey_black_1000));
- return textView;
- }
- };
- ((ListView) view.findViewById(R.id.matched)).setAdapter(adapter);
- // delete items by clicking them
- ((ListView) view.findViewById(R.id.matched)).setOnItemClickListener(
- (parent, view1, position, id) -> adapter.remove(adapter.getItem(position)));
-
- // set the existing preference, if any
- SharedPreferences prefs;
- if (!isWeb) {
- prefs = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
- } else {
- prefs = getActivity().getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
- }
- String preference = prefs.getString(packageName, "");
- switch (preference) {
- case "":
- ((RadioButton) view.findViewById(R.id.use_default)).toggle();
- break;
- case "/first":
- ((RadioButton) view.findViewById(R.id.first)).toggle();
- break;
- case "/never":
- ((RadioButton) view.findViewById(R.id.never)).toggle();
- break;
- default:
- ((RadioButton) view.findViewById(R.id.match)).toggle();
- // trim to remove the last blank element
- adapter.addAll(preference.trim().split("\n"));
- }
-
- // add items with the + button
- View.OnClickListener matchPassword = v -> {
- ((RadioButton) view.findViewById(R.id.match)).toggle();
- Intent intent = new Intent(getActivity(), PasswordStore.class);
- intent.putExtra("matchWith", true);
- startActivityForResult(intent, MATCH_WITH);
- };
- view.findViewById(R.id.matchButton).setOnClickListener(matchPassword);
-
- // write to preferences when OK clicked
- builder.setPositiveButton(R.string.dialog_ok, (dialog, which) -> {
-
- });
- builder.setNegativeButton(R.string.dialog_cancel, null);
- final SharedPreferences.Editor editor = prefs.edit();
- if (isWeb) {
- builder.setNeutralButton(R.string.autofill_apps_delete, (dialog, which) -> {
- if (callingActivity.recyclerAdapter != null
- && packageName != null && !packageName.equals("")) {
- editor.remove(packageName);
- callingActivity.recyclerAdapter.removeWebsite(packageName);
- editor.apply();
- }
- });
- }
- return builder.create();
- }
-
- // need to the onClick here for buttons to dismiss dialog only when wanted
- @Override
- public void onStart() {
- super.onStart();
- AlertDialog ad = (AlertDialog) getDialog();
- if (ad != null) {
- Button positiveButton = ad.getButton(Dialog.BUTTON_POSITIVE);
- positiveButton.setOnClickListener(v -> {
- AutofillPreferenceActivity callingActivity = (AutofillPreferenceActivity) getActivity();
- Dialog dialog = getDialog();
-
- SharedPreferences prefs;
- if (!isWeb) {
- prefs = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
- } else {
- prefs = getActivity().getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
- }
- SharedPreferences.Editor editor = prefs.edit();
-
- String packageName = getArguments().getString("packageName", "");
- if (isWeb) {
- // handle some errors and don't dismiss the dialog
- EditText webURL = dialog.findViewById(R.id.webURL);
-
- packageName = webURL.getText().toString();
-
- if (packageName.equals("")) {
- webURL.setError("URL cannot be blank");
- return;
- }
- String oldPackageName = getArguments().getString("packageName", "");
- if (!oldPackageName.equals(packageName) && prefs.getAll().containsKey(packageName)) {
- webURL.setError("URL already exists");
- return;
- }
- }
-
- // write to preferences accordingly
- RadioGroup radioGroup = dialog.findViewById(R.id.autofill_radiogroup);
- switch (radioGroup.getCheckedRadioButtonId()) {
- case R.id.use_default:
- if (!isWeb) {
- editor.remove(packageName);
- } else {
- editor.putString(packageName, "");
- }
- break;
- case R.id.first:
- editor.putString(packageName, "/first");
- break;
- case R.id.never:
- editor.putString(packageName, "/never");
- break;
- default:
- StringBuilder paths = new StringBuilder();
- for (int i = 0; i < adapter.getCount(); i++) {
- paths.append(adapter.getItem(i));
- if (i != adapter.getCount()) {
- paths.append("\n");
- }
- }
- editor.putString(packageName, paths.toString());
- }
- editor.apply();
-
- // notify the recycler adapter if it is loaded
- if (callingActivity.recyclerAdapter != null) {
- int position;
- if (!isWeb) {
- String appName = getArguments().getString("appName", "");
- position = callingActivity.recyclerAdapter.getPosition(appName);
- callingActivity.recyclerAdapter.notifyItemChanged(position);
- } else {
- position = callingActivity.recyclerAdapter.getPosition(packageName);
- String oldPackageName = getArguments().getString("packageName", "");
- if (oldPackageName.equals(packageName)) {
- callingActivity.recyclerAdapter.notifyItemChanged(position);
- } else if (oldPackageName.equals("")) {
- callingActivity.recyclerAdapter.addWebsite(packageName);
- } else {
- editor.remove(oldPackageName);
- callingActivity.recyclerAdapter.updateWebsite(oldPackageName, packageName);
- }
- }
- }
-
- dismiss();
- });
- }
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (resultCode == Activity.RESULT_OK) {
- adapter.add(data.getStringExtra("path"));
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt
new file mode 100644
index 00000000..aa1c329b
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.kt
@@ -0,0 +1,211 @@
+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.os.Bundle
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import android.widget.EditText
+import android.widget.ListView
+import android.widget.RadioButton
+import android.widget.RadioGroup
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.DialogFragment
+import com.zeapo.pwdstore.PasswordStore
+import com.zeapo.pwdstore.R
+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 = AlertDialog.Builder(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 {
+ iconPackageName = "com.android.browser"
+ builder.setTitle("Website")
+ (view.findViewById<View>(R.id.webURL) as EditText).setText(packageName)
+ }
+ try {
+ 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 TextView
+ textView.setTextColor(ContextCompat.getColor(context, R.color.grey_black_1000))
+ 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).setOnItemClickListener { _, _, 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)
+ val editor = prefs.edit()
+ if (isWeb) {
+ builder.setNeutralButton(R.string.autofill_apps_delete) { _, _ ->
+ if (callingActivity.recyclerAdapter != null
+ && packageName != null && packageName != "") {
+ editor.remove(packageName)
+ callingActivity.recyclerAdapter?.removeWebsite(packageName)
+ editor.apply()
+ }
+ }
+ }
+ 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 = dialog
+ 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)
+ }
+ val editor = prefs.edit()
+
+ 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
+ val radioGroup = dialog.findViewById<RadioGroup>(R.id.autofill_radiogroup)
+ when (radioGroup.checkedRadioButtonId) {
+ R.id.use_default -> if (!isWeb) {
+ editor.remove(packageName)
+ } else {
+ editor.putString(packageName, "")
+ }
+ R.id.first -> editor.putString(packageName, "/first")
+ R.id.never -> editor.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")
+ }
+ }
+ editor.putString(packageName, paths.toString())
+ }
+ }
+ editor.apply()
+
+ // 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 -> {
+ editor.remove(oldPackageName)
+ updateWebsite(oldPackageName, packageName)
+ }
+ }
+ }
+ }
+ dismiss()
+ }
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
+ if (resultCode == AppCompatActivity.RESULT_OK) {
+ 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.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java
deleted file mode 100644
index 9fba959a..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java
+++ /dev/null
@@ -1,165 +0,0 @@
-package com.zeapo.pwdstore.autofill;
-
-import android.app.DialogFragment;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-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.core.app.NavUtils;
-import androidx.core.app.TaskStackBuilder;
-import androidx.core.view.MenuItemCompat;
-import androidx.recyclerview.widget.DividerItemDecoration;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.zeapo.pwdstore.R;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-public class AutofillPreferenceActivity extends AppCompatActivity {
-
- AutofillRecyclerAdapter recyclerAdapter; // let fragment have access
- private RecyclerView recyclerView;
- private PackageManager pm;
-
- private boolean recreate; // flag for action on up press; origin autofill dialog? different act
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.autofill_recycler_view);
- recyclerView = findViewById(R.id.autofill_recycler);
-
- RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
- recyclerView.setLayoutManager(layoutManager);
- recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
-
- pm = getPackageManager();
-
- new populateTask().execute();
-
- // if the preference activity was started from the autofill dialog
- recreate = false;
- Bundle extras = getIntent().getExtras();
- if (extras != null) {
- recreate = true;
-
- showDialog(extras.getString("packageName"), extras.getString("appName"), extras.getBoolean("isWeb"));
- }
-
- setTitle("Autofill Apps");
-
- final FloatingActionButton fab = findViewById(R.id.fab);
- fab.setOnClickListener(v -> showDialog("", "", true));
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.autofill_preference, menu);
- MenuItem searchItem = menu.findItem(R.id.action_search);
- SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem);
-
- searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String s) {
- return false;
- }
-
- @Override
- public boolean onQueryTextChange(String s) {
- if (recyclerAdapter != null) {
- recyclerAdapter.filter(s);
- }
- return true;
- }
- });
-
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- // in service, we CLEAR_TASK. then we set the recreate flag.
- // something of a hack, but w/o CLEAR_TASK, behaviour was unpredictable
- case android.R.id.home:
- Intent upIntent = NavUtils.getParentActivityIntent(this);
- if (recreate) {
- TaskStackBuilder.create(this)
- .addNextIntentWithParentStack(upIntent)
- .startActivities();
- } else {
- NavUtils.navigateUpTo(this, upIntent);
- }
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- public void showDialog(String packageName, String appName, boolean isWeb) {
- DialogFragment df = new AutofillFragment();
- Bundle args = new Bundle();
- args.putString("packageName", packageName);
- args.putString("appName", appName);
- args.putBoolean("isWeb", isWeb);
- df.setArguments(args);
- df.show(getFragmentManager(), "autofill_dialog");
- }
-
- private class populateTask extends AsyncTask<Void, Void, Void> {
- @Override
- protected void onPreExecute() {
- runOnUiThread(() -> findViewById(R.id.progress_bar).setVisibility(View.VISIBLE));
- }
-
- @Override
- protected Void doInBackground(Void... params) {
- Intent intent = new Intent(Intent.ACTION_MAIN);
- intent.addCategory(Intent.CATEGORY_LAUNCHER);
- List<ResolveInfo> allAppsResolveInfo = pm.queryIntentActivities(intent, 0);
- List<AutofillRecyclerAdapter.AppInfo> allApps = new ArrayList<>();
-
- for (ResolveInfo app : allAppsResolveInfo) {
- allApps.add(new AutofillRecyclerAdapter.AppInfo(app.activityInfo.packageName
- , app.loadLabel(pm).toString(), false, app.loadIcon(pm)));
- }
-
- SharedPreferences prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
- Map<String, ?> prefsMap = prefs.getAll();
- for (String key : prefsMap.keySet()) {
- try {
- allApps.add(new AutofillRecyclerAdapter.AppInfo(key, key, true, pm.getApplicationIcon("com.android.browser")));
- } catch (PackageManager.NameNotFoundException e) {
- allApps.add(new AutofillRecyclerAdapter.AppInfo(key, key, true, null));
- }
- }
-
- recyclerAdapter = new AutofillRecyclerAdapter(allApps, pm, AutofillPreferenceActivity.this);
- return null;
- }
-
- @Override
- protected void onPostExecute(Void aVoid) {
- runOnUiThread(() -> {
- findViewById(R.id.progress_bar).setVisibility(View.GONE);
- recyclerView.setAdapter(recyclerAdapter);
- Bundle extras = getIntent().getExtras();
- if (extras != null) {
- recyclerView.scrollToPosition(recyclerAdapter.getPosition(extras.getString("appName")));
- }
- });
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt
new file mode 100644
index 00000000..43f2f33f
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.kt
@@ -0,0 +1,159 @@
+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.core.app.NavUtils
+import androidx.core.app.TaskStackBuilder
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.zeapo.pwdstore.R
+import java.lang.ref.WeakReference
+import java.util.ArrayList
+
+class AutofillPreferenceActivity : AppCompatActivity() {
+
+ internal var recyclerAdapter: AutofillRecyclerAdapter? = null // let fragment have access
+ private var recyclerView: RecyclerView? = null
+ 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(R.layout.autofill_recycler_view)
+ recyclerView = findViewById(R.id.autofill_recycler)
+
+ val layoutManager = LinearLayoutManager(this)
+ recyclerView!!.layoutManager = layoutManager
+ recyclerView!!.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
+
+ 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
+ if (item.itemId == android.R.id.home) {
+ val upIntent = NavUtils.getParentActivityIntent(this)
+ if (recreate) {
+ TaskStackBuilder.create(this)
+ .addNextIntentWithParentStack(upIntent!!)
+ .startActivities()
+ } else {
+ NavUtils.navigateUpTo(this, upIntent!!)
+ }
+ return true
+ }
+ return 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<AutofillPreferenceActivity>(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 {
+ findViewById<View>(R.id.progress_bar).visibility = View.GONE
+ recyclerView!!.adapter = recyclerAdapter
+ val extras = intent.extras
+ if (extras != null) {
+ recyclerView!!.scrollToPosition(recyclerAdapter!!.getPosition(extras.getString("appName")!!))
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java
deleted file mode 100644
index bed2aa7e..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java
+++ /dev/null
@@ -1,192 +0,0 @@
-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 android.widget.ImageView;
-import android.widget.TextView;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.SortedList;
-import androidx.recyclerview.widget.SortedListAdapterCallback;
-import com.zeapo.pwdstore.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecyclerAdapter.ViewHolder> {
-
- private SortedList<AppInfo> apps;
- private ArrayList<AppInfo> allApps; // for filtering, maintain a list of all
- private AutofillPreferenceActivity activity;
- private Drawable browserIcon = null;
-
- AutofillRecyclerAdapter(List<AppInfo> allApps, final PackageManager pm
- , AutofillPreferenceActivity activity) {
- SortedList.Callback<AppInfo> callback = new SortedListAdapterCallback<AppInfo>(this) {
- // don't take into account secondary text. This is good enough
- // for the limited add/remove usage for websites
- @Override
- public int compare(AppInfo o1, AppInfo o2) {
- return o1.appName.toLowerCase().compareTo(o2.appName.toLowerCase());
- }
-
- @Override
- public boolean areContentsTheSame(AppInfo oldItem, AppInfo newItem) {
- return oldItem.appName.equals(newItem.appName);
- }
-
- @Override
- public boolean areItemsTheSame(AppInfo item1, AppInfo item2) {
- return item1.appName.equals(item2.appName);
- }
- };
- this.apps = new SortedList<>(AppInfo.class, callback);
- this.apps.addAll(allApps);
- this.allApps = new ArrayList<>(allApps);
- this.activity = activity;
- try {
- browserIcon = activity.getPackageManager().getApplicationIcon("com.android.browser");
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public AutofillRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- View v = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.autofill_row_layout, parent, false);
- return new ViewHolder(v);
- }
-
- @Override
- public void onBindViewHolder(AutofillRecyclerAdapter.ViewHolder holder, int position) {
- AppInfo app = apps.get(position);
- holder.packageName = app.packageName;
- holder.appName = app.appName;
- holder.isWeb = app.isWeb;
-
- holder.icon.setImageDrawable(app.icon);
- holder.name.setText(app.appName);
-
- holder.secondary.setVisibility(View.VISIBLE);
- holder.view.setBackgroundResource(R.color.grey_white_1000);
-
- SharedPreferences prefs;
- if (!app.appName.equals(app.packageName)) {
- prefs = activity.getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
- } else {
- prefs = activity.getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
- }
- String preference = prefs.getString(holder.packageName, "");
- switch (preference) {
- case "":
- holder.secondary.setVisibility(View.GONE);
- holder.view.setBackgroundResource(0);
- break;
- case "/first":
- holder.secondary.setText(R.string.autofill_apps_first);
- break;
- case "/never":
- holder.secondary.setText(R.string.autofill_apps_never);
- break;
- default:
- holder.secondary.setText(R.string.autofill_apps_match);
- holder.secondary.append(" " + preference.split("\n")[0]);
- if ((preference.trim().split("\n").length - 1) > 0) {
- holder.secondary.append(" and "
- + (preference.trim().split("\n").length - 1) + " more");
- }
- break;
- }
- }
-
- @Override
- public int getItemCount() {
- return apps.size();
- }
-
- int getPosition(String appName) {
- return apps.indexOf(new AppInfo(null, appName, false, null));
- }
-
- // for websites, URL = packageName == appName
- void addWebsite(String packageName) {
- apps.add(new AppInfo(packageName, packageName, true, browserIcon));
- allApps.add(new AppInfo(packageName, packageName, true, browserIcon));
- }
-
- void removeWebsite(String packageName) {
- apps.remove(new AppInfo(null, packageName, false, null));
- allApps.remove(new AppInfo(null, packageName, false, null)); // compare with equals
- }
-
- void updateWebsite(String oldPackageName, String packageName) {
- apps.updateItemAt(getPosition(oldPackageName), new AppInfo(packageName, packageName, true, browserIcon));
- allApps.remove(new AppInfo(null, oldPackageName, false, null)); // compare with equals
- allApps.add(new AppInfo(null, packageName, false, null));
- }
-
- void filter(String s) {
- if (s.isEmpty()) {
- apps.addAll(allApps);
- return;
- }
- apps.beginBatchedUpdates();
- for (AppInfo app : allApps) {
- if (app.appName.toLowerCase().contains(s.toLowerCase())) {
- apps.add(app);
- } else {
- apps.remove(app);
- }
- }
- apps.endBatchedUpdates();
- }
-
- static class AppInfo {
- public Drawable icon;
- String packageName;
- String appName;
- boolean isWeb;
-
- AppInfo(String packageName, String appName, boolean isWeb, Drawable icon) {
- this.packageName = packageName;
- this.appName = appName;
- this.isWeb = isWeb;
- this.icon = icon;
- }
-
- @Override
- public boolean equals(Object o) {
- return o instanceof AppInfo && this.appName.equals(((AppInfo) o).appName);
- }
- }
-
- class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
- public View view;
- public TextView name;
- public ImageView icon;
- TextView secondary;
- String packageName;
- String appName;
- Boolean isWeb;
-
- ViewHolder(View view) {
- super(view);
- this.view = view;
- name = view.findViewById(R.id.app_name);
- secondary = view.findViewById(R.id.secondary_text);
- icon = view.findViewById(R.id.app_icon);
- view.setOnClickListener(this);
- }
-
- @Override
- public void onClick(View v) {
- activity.showDialog(packageName, appName, isWeb);
- }
-
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt
new file mode 100644
index 00000000..f4c9357b
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.kt
@@ -0,0 +1,170 @@
+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 android.widget.ImageView
+import android.widget.TextView
+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
+
+internal class AutofillRecyclerAdapter(
+ allApps: List<AppInfo>,
+ private val activity: AutofillPreferenceActivity
+) : RecyclerView.Adapter<AutofillRecyclerAdapter.ViewHolder>() {
+
+ 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 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
+ holder.view.setBackgroundResource(R.color.grey_white_1000)
+
+ 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: TextView = view.findViewById(R.id.app_name)
+ var icon: ImageView = view.findViewById(R.id.app_icon)
+ var secondary: TextView = 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.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
deleted file mode 100644
index 8ad27b4f..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
+++ /dev/null
@@ -1,606 +0,0 @@
-package com.zeapo.pwdstore.autofill;
-
-import android.accessibilityservice.AccessibilityService;
-import android.app.PendingIntent;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.PreferenceManager;
-import android.provider.Settings;
-import android.util.Log;
-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 com.zeapo.pwdstore.PasswordEntry;
-import com.zeapo.pwdstore.R;
-import com.zeapo.pwdstore.utils.PasswordRepository;
-import org.apache.commons.io.FileUtils;
-import org.openintents.openpgp.IOpenPgpService2;
-import org.openintents.openpgp.OpenPgpError;
-import org.openintents.openpgp.util.OpenPgpApi;
-import org.openintents.openpgp.util.OpenPgpServiceConnection;
-
-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.List;
-import java.util.Map;
-
-public class AutofillService extends AccessibilityService {
- private static AutofillService instance;
- private OpenPgpServiceConnection serviceConnection;
- private SharedPreferences settings;
- private AccessibilityNodeInfo info; // the original source of the event (the edittext field)
- private ArrayList<File> items; // password choices
- private int lastWhichItem;
- private AlertDialog dialog;
- private AccessibilityWindowInfo window;
- private Intent resultData = null; // need the intent which contains results from user interaction
- private CharSequence packageName;
- private boolean ignoreActionFocus = false;
- private String webViewTitle = null;
- private String webViewURL = null;
- private PasswordEntry lastPassword;
- private long lastPasswordMaxDate;
-
- public static AutofillService getInstance() {
- return instance;
- }
-
- public void setResultData(Intent data) {
- resultData = data;
- }
-
- public void setPickedPassword(String path) {
- items.add(new File(PasswordRepository.getRepositoryDirectory(getApplicationContext()) + "/" + path + ".gpg"));
- bindDecryptAndVerify();
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
- instance = this;
- }
-
- @Override
- protected void onServiceConnected() {
- super.onServiceConnected();
- serviceConnection = new OpenPgpServiceConnection(AutofillService.this
- , "org.sufficientlysecure.keychain");
- serviceConnection.bindToService();
- settings = PreferenceManager.getDefaultSharedPreferences(this);
- }
-
- @Override
- public void onAccessibilityEvent(AccessibilityEvent event) {
- // 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.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
- && event.getPackageName() != null && event.getPackageName().equals(packageName)
- && resultData != null) {
- bindDecryptAndVerify();
- }
-
- // look for webView and trigger accessibility events if window changes
- // or if page changes in chrome
- if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
- || (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
- && event.getPackageName() != null
- && (event.getPackageName().equals("com.android.chrome")
- || event.getPackageName().equals("com.android.browser")))) {
- // there is a chance for getRootInActiveWindow() to return null at any time. save it.
- try {
- AccessibilityNodeInfo root = getRootInActiveWindow();
- webViewTitle = searchWebView(root);
- webViewURL = null;
- if (webViewTitle != null) {
- List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar");
- if (nodes.isEmpty()) {
- nodes = root.findAccessibilityNodeInfosByViewId("com.android.browser:id/url");
- }
- for (AccessibilityNodeInfo node : nodes)
- if (node.getText() != null) {
- try {
- webViewURL = new URL(node.getText().toString()).getHost();
- } catch (MalformedURLException e) {
- if (e.toString().contains("Protocol not found")) {
- try {
- webViewURL = new URL("http://" + node.getText().toString()).getHost();
- } catch (MalformedURLException ignored) {
- }
- }
- }
- }
- }
- } catch (Exception e) {
- // sadly we were unable to access the data we wanted
- return;
- }
- }
-
- // nothing to do if field is keychain app or system ui
- if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
- || event.getPackageName() != null && event.getPackageName().equals("org.sufficientlysecure.keychain")
- || event.getPackageName() != null && event.getPackageName().equals("com.android.systemui")) {
- dismissDialog(event);
- return;
- }
-
- if (!event.isPassword()) {
- if (lastPassword != null && event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && event.getSource().isEditable()) {
- showPasteUsernameDialog(event.getSource(), lastPassword);
- return;
- } else {
- // nothing to do if not password field focus
- dismissDialog(event);
- 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.getEventType() == 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)) {
- Intent intent = new 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.getSource();
- if (info == null) return;
-
- // save the dialog's corresponding window so we can use getWindows() in dismissDialog
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- window = info.getWindow();
- }
-
- String packageName;
- String appName;
- boolean isWeb;
-
- // 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.equals("") && webViewURL == null)) {
- if (info.getPackageName() == null) return;
- packageName = info.getPackageName().toString();
-
- // get the app name and find a corresponding password
- PackageManager packageManager = getPackageManager();
- ApplicationInfo applicationInfo;
- try {
- applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0);
- } catch (PackageManager.NameNotFoundException e) {
- applicationInfo = null;
- }
- appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").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("autofill_always", false)) {
- return;
- }
- showSelectPasswordDialog(packageName, appName, isWeb);
- }
-
- private String searchWebView(AccessibilityNodeInfo source) {
- return searchWebView(source, 10);
- }
-
- private String searchWebView(AccessibilityNodeInfo source, int depth) {
- if (source == null || depth == 0) {
- return null;
- }
- for (int i = 0; i < source.getChildCount(); i++) {
- AccessibilityNodeInfo u = source.getChild(i);
- if (u == null) {
- continue;
- }
- if (u.getClassName() != null && u.getClassName().equals("android.webkit.WebView")) {
- if (u.getContentDescription() != null) {
- return u.getContentDescription().toString();
- }
- return "";
- }
- String webView = searchWebView(u, depth - 1);
- if (webView != null) {
- return webView;
- }
- u.recycle();
- }
- return null;
- }
-
- // dismiss the dialog if the window has changed
- private void dismissDialog(AccessibilityEvent event) {
- // the default keyboard showing/hiding is a window state changed event
- // on Android 5+ we can use getWindows() to determine when the original window is not visible
- // on Android 4.3 we have to use window state changed events and filter out the keyboard ones
- // there may be other exceptions...
- boolean dismiss;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- dismiss = !getWindows().contains(window);
- } else {
- dismiss = !(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
- event.getPackageName() != null &&
- event.getPackageName().toString().contains("inputmethod"));
- }
- if (dismiss && dialog != null && dialog.isShowing()) {
- dialog.dismiss();
- dialog = null;
- }
- }
-
- private String setWebMatchingPasswords(String webViewTitle, String webViewURL) {
- // Return the URL needed to open the corresponding Settings.
- String settingsURL = webViewURL;
-
- // if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
- String defValue = settings.getBoolean("autofill_default", true) ? "/first" : "/never";
- SharedPreferences prefs;
- String preference;
-
- prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
- preference = defValue;
- if (webViewURL != null) {
- final String webViewUrlLowerCase = webViewURL.toLowerCase();
- Map<String, ?> prefsMap = prefs.getAll();
- for (String key : prefsMap.keySet()) {
- // for websites unlike apps there can be blank preference of "" which
- // means use default, so ignore it.
- final String value = prefs.getString(key, null);
- final String keyLowerCase = key.toLowerCase();
- if (value != null && !value.equals("")
- && (webViewUrlLowerCase.contains(keyLowerCase) || keyLowerCase.contains(webViewUrlLowerCase))) {
- preference = value;
- settingsURL = key;
- }
- }
- }
-
- switch (preference) {
- case "/first":
- if (!PasswordRepository.isInitialized()) {
- PasswordRepository.initialize(this);
- }
- items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), webViewTitle);
- break;
- case "/never":
- items = new ArrayList<>();
- break;
- default:
- getPreferredPasswords(preference);
- }
-
- return settingsURL;
- }
-
- private void setAppMatchingPasswords(String appName, String packageName) {
- // if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
- String defValue = settings.getBoolean("autofill_default", true) ? "/first" : "/never";
- SharedPreferences prefs;
- String preference;
-
- prefs = getSharedPreferences("autofill", Context.MODE_PRIVATE);
- preference = prefs.getString(packageName, defValue);
-
- switch (preference) {
- case "/first":
- if (!PasswordRepository.isInitialized()) {
- PasswordRepository.initialize(this);
- }
- items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), appName);
- break;
- case "/never":
- items = new ArrayList<>();
- break;
- default:
- getPreferredPasswords(preference);
- }
- }
-
- // Put the newline separated list of passwords from the SharedPreferences
- // file into the items list.
- private void getPreferredPasswords(String preference) {
- if (!PasswordRepository.isInitialized()) {
- PasswordRepository.initialize(this);
- }
- String preferredPasswords[] = preference.split("\n");
- items = new ArrayList<>();
- for (String password : preferredPasswords) {
- String path = PasswordRepository.getRepositoryDirectory(getApplicationContext()) + "/" + password + ".gpg";
- if (new File(path).exists()) {
- items.add(new File(path));
- }
- }
- }
-
- private ArrayList<File> searchPasswords(File path, String appName) {
- ArrayList<File> passList = PasswordRepository.getFilesList(path);
-
- if (passList.size() == 0) return new ArrayList<>();
-
- ArrayList<File> items = new ArrayList<>();
-
- for (File file : passList) {
- if (file.isFile()) {
- if (!file.isHidden() && appName.toLowerCase().contains(file.getName().toLowerCase().replace(".gpg", ""))) {
- items.add(file);
- }
- } else {
- if (!file.isHidden()) {
- items.addAll(searchPasswords(file, appName));
- }
- }
- }
- return items;
- }
-
- private void showPasteUsernameDialog(final AccessibilityNodeInfo node, final PasswordEntry password) {
- if (dialog != null) {
- dialog.dismiss();
- dialog = null;
- }
-
- AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
- builder.setNegativeButton(R.string.dialog_cancel, (d, which) -> {
- dialog.dismiss();
- dialog = null;
- });
- builder.setPositiveButton(R.string.autofill_paste, (d, which) -> {
- pasteText(node, password.getUsername());
- dialog.dismiss();
- dialog = null;
- });
- builder.setMessage(getString(R.string.autofill_paste_username, password.getUsername()));
-
- dialog = builder.create();
- this.setDialogType(dialog);
- dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
- dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
- dialog.show();
- }
-
- private void showSelectPasswordDialog(final String packageName, final String appName, final boolean isWeb) {
- if (dialog != null) {
- dialog.dismiss();
- dialog = null;
- }
-
- AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
- builder.setNegativeButton(R.string.dialog_cancel, (d, which) -> {
- dialog.dismiss();
- dialog = null;
- });
- builder.setNeutralButton("Settings", (dialog, which) -> {
- //TODO make icon? gear?
- // the user will have to return to the app themselves.
- Intent intent = new Intent(AutofillService.this, AutofillPreferenceActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 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)
- CharSequence itemNames[] = new CharSequence[items.size() + 2];
- for (int i = 0; i < items.size(); i++) {
- itemNames[i] = items.get(i).getName().replace(".gpg", "");
- }
- itemNames[items.size()] = getString(R.string.autofill_pick);
- itemNames[items.size() + 1] = getString(R.string.autofill_pick_and_match);
- builder.setItems(itemNames, (dialog, which) -> {
- lastWhichItem = which;
- if (which < items.size()) {
- bindDecryptAndVerify();
- } else if (which == items.size()) {
- Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- intent.putExtra("pick", true);
- startActivity(intent);
- } else {
- lastWhichItem--; // will add one element to items, so lastWhichItem=items.size()+1
- Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- intent.putExtra("pickMatchWith", true);
- intent.putExtra("packageName", packageName);
- intent.putExtra("isWeb", isWeb);
- startActivity(intent);
- }
- });
-
- dialog = builder.create();
- this.setDialogType(dialog);
- dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
- dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
- // arbitrary non-annoying size
- int height = 154;
- if (itemNames.length > 1) {
- height += 46;
- }
- dialog.getWindow().setLayout((int) (240 * getApplicationContext().getResources().getDisplayMetrics().density)
- , (int) (height * getApplicationContext().getResources().getDisplayMetrics().density));
- dialog.show();
- }
-
- private void setDialogType(AlertDialog dialog) {
- //noinspection ConstantConditions
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
- dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
- } else {
- dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
- }
- }
-
- @Override
- public void onInterrupt() {
-
- }
-
- private void bindDecryptAndVerify() {
- if (serviceConnection.getService() == null) {
- // the service was disconnected, need to bind again
- // give it a listener and in the callback we will decryptAndVerify
- serviceConnection = new OpenPgpServiceConnection(AutofillService.this
- , "org.sufficientlysecure.keychain", new onBoundListener());
- serviceConnection.bindToService();
- } else {
- decryptAndVerify();
- }
- }
-
- private void decryptAndVerify() {
- packageName = info.getPackageName();
- Intent data;
- if (resultData == null) {
- data = new Intent();
- data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
- } else {
- data = resultData;
- resultData = null;
- }
- InputStream is = null;
- try {
- is = FileUtils.openInputStream(items.get(lastWhichItem));
- } catch (IOException e) {
- e.printStackTrace();
- }
- ByteArrayOutputStream os = new ByteArrayOutputStream();
-
- OpenPgpApi api = new OpenPgpApi(AutofillService.this, serviceConnection.getService());
- // TODO we are dropping frames, (did we before??) find out why and maybe make this async
- Intent result = api.executeApi(data, is, os);
- switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
- case OpenPgpApi.RESULT_CODE_SUCCESS: {
- try {
- final PasswordEntry entry = new PasswordEntry(os);
- pasteText(info, entry.getPassword());
-
- // save password entry for pasting the username as well
- if (entry.hasUsername()) {
- lastPassword = entry;
- final int ttl = Integer.parseInt(settings.getString("general_show_time", "45"));
- Toast.makeText(this, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show();
- lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L;
- }
- } catch (UnsupportedEncodingException e) {
- Log.e(Constants.TAG, "UnsupportedEncodingException", e);
- }
- break;
- }
- case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: {
- Log.i("PgpHandler", "RESULT_CODE_USER_INTERACTION_REQUIRED");
- PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
- // need to start a blank activity to call startIntentSenderForResult
- Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
- intent.putExtra("pending_intent", pi);
- startActivity(intent);
- break;
- }
- case OpenPgpApi.RESULT_CODE_ERROR: {
- OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
- Toast.makeText(AutofillService.this,
- "Error from OpenKeyChain : " + error.getMessage(),
- Toast.LENGTH_LONG).show();
- Log.e(Constants.TAG, "onError getErrorId:" + error.getErrorId());
- Log.e(Constants.TAG, "onError getMessage:" + error.getMessage());
- break;
- }
- }
- }
-
- private void pasteText(final AccessibilityNodeInfo node, final String text) {
- // 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);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- Bundle args = new Bundle();
- args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
- node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
- } else {
- ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText("autofill_pm", text);
- clipboard.setPrimaryClip(clip);
- node.performAction(AccessibilityNodeInfo.ACTION_PASTE);
-
- clip = ClipData.newPlainText("autofill_pm", "");
- clipboard.setPrimaryClip(clip);
- if (settings.getBoolean("clear_clipboard_20x", false)) {
- for (int i = 0; i < 20; i++) {
- clip = ClipData.newPlainText(String.valueOf(i), String.valueOf(i));
- clipboard.setPrimaryClip(clip);
- }
- }
- }
- node.recycle();
- }
-
- final class Constants {
- static final String TAG = "Keychain";
- }
-
- private class onBoundListener implements OpenPgpServiceConnection.OnBound {
- @Override
- public void onBound(IOpenPgpService2 service) {
- decryptAndVerify();
- }
-
- @Override
- public void onError(Exception e) {
- e.printStackTrace();
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
new file mode 100644
index 00000000..6cb3c678
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
@@ -0,0 +1,582 @@
+package com.zeapo.pwdstore.autofill
+
+import android.accessibilityservice.AccessibilityService
+import android.app.PendingIntent
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.preference.PreferenceManager
+import android.provider.Settings
+import android.util.Log
+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 com.zeapo.pwdstore.PasswordEntry
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.utils.PasswordRepository
+import com.zeapo.pwdstore.utils.splitLines
+import org.apache.commons.io.FileUtils
+import org.openintents.openpgp.IOpenPgpService2
+import org.openintents.openpgp.OpenPgpError
+import org.openintents.openpgp.util.OpenPgpApi
+import org.openintents.openpgp.util.OpenPgpServiceConnection
+
+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
+
+class AutofillService : AccessibilityService() {
+ 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(applicationContext)}/$path.gpg"))
+ bindDecryptAndVerify()
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ instance = this
+ }
+
+ 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(event)
+ 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(event)
+ 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
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ 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
+ var applicationInfo: ApplicationInfo?
+ try {
+ applicationInfo = packageManager.getApplicationInfo(event.packageName.toString(), 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ applicationInfo = 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("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(event: AccessibilityEvent) {
+ // the default keyboard showing/hiding is a window state changed event
+ // on Android 5+ we can use getWindows() to determine when the original window is not visible
+ // on Android 4.3 we have to use window state changed events and filter out the keyboard ones
+ // there may be other exceptions...
+ val dismiss: Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ !windows.contains(window)
+ } else {
+ !(event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
+ event.packageName != null &&
+ event.packageName.toString().contains("inputmethod"))
+ }
+ 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("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(this)
+ }
+ items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), 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("autofill_default", true)) "/first" else "/never"
+ val prefs: SharedPreferences = getSharedPreferences("autofill", Context.MODE_PRIVATE)
+ val preference: String?
+
+ preference = prefs.getString(packageName, defValue)
+
+ when (preference) {
+ "/first" -> {
+ if (!PasswordRepository.isInitialized()) {
+ PasswordRepository.initialize(this)
+ }
+ items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), 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(this)
+ }
+ val preferredPasswords = preference.splitLines()
+ items = ArrayList()
+ for (password in preferredPasswords) {
+ val path = PasswordRepository.getRepositoryDirectory(applicationContext).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 = AlertDialog.Builder(this, R.style.Theme_AppCompat_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()
+ this.setDialogType(dialog)
+ dialog!!.window!!.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
+ dialog!!.window!!.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 = AlertDialog.Builder(this, R.style.Theme_AppCompat_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)
+ for (i in items.indices) {
+ 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()
+ setDialogType(dialog)
+ dialog?.window?.apply {
+ val height = 200
+ val density = context.resources.displayMetrics.density
+ addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
+ clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ // arbitrary non-annoying size
+ setLayout((240 * density).toInt(), (height * density).toInt())
+ }
+ dialog?.show()
+ }
+
+ private fun setDialogType(dialog: AlertDialog?) {
+ dialog?.window?.apply {
+ setType(
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
+ WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
+ else
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+ )
+ }
+ }
+
+ 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() {
+ packageName = info!!.packageName
+ val data: Intent
+ if (resultData == null) {
+ data = Intent()
+ data.action = OpenPgpApi.ACTION_DECRYPT_VERIFY
+ } else {
+ data = resultData!!
+ resultData = null
+ }
+ var `is`: InputStream? = null
+ try {
+ `is` = FileUtils.openInputStream(items[lastWhichItem])
+ } catch (e: IOException) {
+ e.printStackTrace()
+ }
+
+ val os = ByteArrayOutputStream()
+
+ val api = OpenPgpApi(this@AutofillService, serviceConnection!!.service)
+ // TODO we are dropping frames, (did we before??) find out why and maybe make this async
+ val result = api.executeApi(data, `is`, os)
+ when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+ OpenPgpApi.RESULT_CODE_SUCCESS -> {
+ try {
+ val entry = PasswordEntry(os)
+ pasteText(info!!, entry.password)
+
+ // save password entry for pasting the username as well
+ if (entry.hasUsername()) {
+ lastPassword = entry
+ val ttl = Integer.parseInt(settings!!.getString("general_show_time", "45")!!)
+ Toast.makeText(this, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show()
+ lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L
+ }
+ } catch (e: UnsupportedEncodingException) {
+ Log.e(Constants.TAG, "UnsupportedEncodingException", e)
+ }
+
+ }
+ OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
+ Log.i("PgpHandler", "RESULT_CODE_USER_INTERACTION_REQUIRED")
+ val pi = result.getParcelableExtra<PendingIntent>(OpenPgpApi.RESULT_INTENT)
+ // need to start a blank activity to call startIntentSenderForResult
+ val intent = Intent(this@AutofillService, AutofillActivity::class.java)
+ 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)
+ Toast.makeText(this@AutofillService,
+ "Error from OpenKeyChain : " + error.message,
+ Toast.LENGTH_LONG).show()
+ Log.e(Constants.TAG, "onError getErrorId:" + error.errorId)
+ Log.e(Constants.TAG, "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)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ val args = Bundle()
+ args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
+ node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
+ } else {
+ val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ var clip = ClipData.newPlainText("autofill_pm", text)
+ clipboard.primaryClip = clip
+ node.performAction(AccessibilityNodeInfo.ACTION_PASTE)
+
+ clip = ClipData.newPlainText("autofill_pm", "")
+ clipboard.primaryClip = clip
+ if (settings!!.getBoolean("clear_clipboard_20x", false)) {
+ for (i in 0..19) {
+ clip = ClipData.newPlainText(i.toString(), i.toString())
+ clipboard.primaryClip = clip
+ }
+ }
+ }
+ 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/crypto/PgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt
index f6f0b9a8..7d782a05 100644
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt
@@ -435,8 +435,8 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
val path = if (intent.getBooleanExtra("fromDecrypt", false)) fullPath else "$fullPath/$editName.gpg"
api?.executeApiAsync(data, iStream, oStream) { result: Intent? ->
- when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
- OpenPgpApi.RESULT_CODE_SUCCESS -> {
+ when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) {
+ RESULT_CODE_SUCCESS -> {
try {
// TODO This might fail, we should check that the write is successful
val outputStream = FileUtils.openOutputStream(File(path))
@@ -459,7 +459,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
Log.e(TAG, "An Exception occurred", e)
}
}
- OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
+ RESULT_CODE_ERROR -> handleError(result)
}
}
@@ -516,7 +516,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
private fun calculateHotp(entry: PasswordEntry) {
copyOtpToClipBoard(Otp.calculateCode(entry.hotpSecret, entry.hotpCounter!! + 1, "sha1", entry.digits))
- crypto_otp_show.text = Otp.calculateCode(entry.hotpSecret, entry.hotpCounter!! + 1, "sha1", entry.digits)
+ crypto_otp_show.text = Otp.calculateCode(entry.hotpSecret, entry.hotpCounter + 1, "sha1", entry.digits)
crypto_extra_show.text = entry.extraContent
}
@@ -539,8 +539,8 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
val data = receivedIntent ?: Intent()
data.action = OpenPgpApi.ACTION_GET_KEY_IDS
api?.executeApiAsync(data, null, null) { result: Intent? ->
- when (result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
- OpenPgpApi.RESULT_CODE_SUCCESS -> {
+ when (result?.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR)) {
+ RESULT_CODE_SUCCESS -> {
try {
val ids = result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)
val keys = ids.map { it.toString() }.toSet()
@@ -557,7 +557,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
}
}
RESULT_CODE_USER_INTERACTION_REQUIRED -> handleUserInteractionRequest(result, REQUEST_KEY_ID)
- OpenPgpApi.RESULT_CODE_ERROR -> handleError(result)
+ RESULT_CODE_ERROR -> handleError(result)
}
}
}
@@ -580,23 +580,23 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
Log.d(TAG, "onActivityResult resultCode: $resultCode")
if (data == null) {
- setResult(AppCompatActivity.RESULT_CANCELED, null)
+ setResult(RESULT_CANCELED, null)
finish()
return
}
// try again after user interaction
- if (resultCode == AppCompatActivity.RESULT_OK) {
+ if (resultCode == RESULT_OK) {
when (requestCode) {
REQUEST_DECRYPT -> decryptAndVerify(data)
REQUEST_KEY_ID -> getKeyIds(data)
else -> {
- setResult(AppCompatActivity.RESULT_OK)
+ setResult(RESULT_OK)
finish()
}
}
- } else if (resultCode == AppCompatActivity.RESULT_CANCELED) {
- setResult(AppCompatActivity.RESULT_CANCELED, data)
+ } else if (resultCode == RESULT_CANCELED) {
+ setResult(RESULT_CANCELED, data)
finish()
}
}
@@ -786,7 +786,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
if (crypto_password_show != null) {
// clear password; if decrypt changed to encrypt layout via edit button, no need
if (passwordEntry?.hotpIsIncremented() == false) {
- setResult(AppCompatActivity.RESULT_CANCELED)
+ setResult(RESULT_CANCELED)
}
passwordEntry = null
crypto_password_show.text = ""
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
new file mode 100644
index 00000000..a2be3ddc
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
@@ -0,0 +1,5 @@
+package com.zeapo.pwdstore.utils
+
+fun String.splitLines(): Array<String> {
+ return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+}