Convert autofill package to Kotlin (#515)
Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com>
This commit is contained in:
parent
321035c319
commit
53b42905f1
12 changed files with 1228 additions and 1312 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
582
app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
Normal file
582
app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 = ""
|
||||
|
|
5
app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
Normal file
5
app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
Normal file
|
@ -0,0 +1,5 @@
|
|||
package com.zeapo.pwdstore.utils
|
||||
|
||||
fun String.splitLines(): Array<String> {
|
||||
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
}
|
Loading…
Reference in a new issue