Convert autofill package to Kotlin (#515)

Signed-off-by: Harsh Shandilya <msfjarvis@gmail.com>
This commit is contained in:
Harsh Shandilya 2019-05-29 00:42:09 +05:30 committed by GitHub
parent 321035c319
commit 53b42905f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1228 additions and 1312 deletions

View file

@ -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;
}
}
}

View file

@ -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
}
}

View file

@ -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"));
}
}
}

View file

@ -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
}
}

View file

@ -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")));
}
});
}
}
}

View file

@ -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")!!))
}
}
}
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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)
}
}
}

View file

@ -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();
}
}
}

View 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
}
}

View file

@ -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 = ""

View file

@ -0,0 +1,5 @@
package com.zeapo.pwdstore.utils
fun String.splitLines(): Array<String> {
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}