summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java6
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordStore.java15
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/UserPreference.java63
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java40
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java121
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java146
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java148
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java274
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java105
9 files changed, 916 insertions, 2 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java
index 72636043..1daa4f90 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java
@@ -112,7 +112,11 @@ public class PasswordFragment extends Fragment{
((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} else {
- ((PasswordStore) getActivity()).decryptPassword(item);
+ if (getArguments().getBoolean("matchWith", false)) {
+ ((PasswordStore) getActivity()).matchPasswordWithApp(item);
+ } else {
+ ((PasswordStore) getActivity()).decryptPassword(item);
+ }
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java
index e51ef201..39d918c8 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java
@@ -289,6 +289,12 @@ public class PasswordStore extends AppCompatActivity {
Bundle args = new Bundle();
args.putString("Path", PasswordRepository.getWorkTree().getAbsolutePath());
+ // if the activity was started from the autofill settings, the
+ // intent is to match a clicked pwd with app. pass this to fragment
+ if (getIntent().getBooleanExtra("matchWith", false)) {
+ args.putBoolean("matchWith", true);
+ }
+
plist.setArguments(args);
fragmentTransaction.addToBackStack("passlist");
@@ -531,4 +537,13 @@ public class PasswordStore extends AppCompatActivity {
})
.show();
}
+
+ public void matchPasswordWithApp(PasswordItem item) {
+ String path = item.getFile().getAbsolutePath();
+ path = path.replace(PasswordRepository.getWorkTree() + "/", "").replace(".gpg", "");
+ Intent data = new Intent();
+ data.putExtra("path", path);
+ setResult(RESULT_OK, data);
+ finish();
+ }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java
index 9a5c83d4..8f8e8d66 100644
--- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java
+++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java
@@ -1,22 +1,29 @@
package com.zeapo.pwdstore;
-import android.app.AlertDialog;
+import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.DialogFragment;
+import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
+import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
+import android.text.SpannableStringBuilder;
import android.view.MenuItem;
+import android.view.accessibility.AccessibilityManager;
import android.widget.Toast;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
+import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity;
import com.zeapo.pwdstore.crypto.PgpHandler;
import com.zeapo.pwdstore.git.GitActivity;
import com.zeapo.pwdstore.utils.PasswordRepository;
@@ -32,6 +39,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
public class UserPreference extends AppCompatActivity {
@@ -182,6 +190,40 @@ public class UserPreference extends AppCompatActivity {
findPreference("pref_select_external").setOnPreferenceChangeListener(resetRepo);
findPreference("git_external").setOnPreferenceChangeListener(resetRepo);
+
+ findPreference("autofill_apps").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent(callingActivity, AutofillPreferenceActivity.class);
+ startActivity(intent);
+ return true;
+ }
+ });
+
+ findPreference("autofill_enable").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ new AlertDialog.Builder(callingActivity).
+ setTitle(R.string.pref_autofill_enable_title).
+ setView(R.layout.autofill_instructions).
+ setPositiveButton(R.string.dialog_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
+ startActivity(intent);
+ }
+ }).
+ setNegativeButton(R.string.dialog_cancel, null).
+ setOnDismissListener(new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ ((CheckBoxPreference) findPreference("autofill_enable"))
+ .setChecked(((UserPreference) getActivity()).isServiceEnabled());
+ }
+ }).show();
+ return true;
+ }
+ });
}
@Override
@@ -189,6 +231,10 @@ public class UserPreference extends AppCompatActivity {
super.onStart();
final SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences();
findPreference("ssh_see_key").setEnabled(sharedPreferences.getBoolean("use_generated_key", false));
+
+ // see if the autofill service is enabled and check the preference accordingly
+ ((CheckBoxPreference) findPreference("autofill_enable"))
+ .setChecked(((UserPreference) getActivity()).isServiceEnabled());
}
}
@@ -268,6 +314,21 @@ public class UserPreference extends AppCompatActivity {
sshKey.close();
}
+ // Returns whether the autofill service is enabled
+ private boolean isServiceEnabled() {
+ AccessibilityManager am = (AccessibilityManager) this
+ .getSystemService(Context.ACCESSIBILITY_SERVICE);
+ List<AccessibilityServiceInfo> runningServices = am
+ .getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC);
+ for (AccessibilityServiceInfo service : runningServices) {
+ if ("com.zeapo.pwdstore/.autofill.AutofillService".equals(service.getId())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (resultCode == RESULT_OK) {
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java
new file mode 100644
index 00000000..d8e50ebd
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java
@@ -0,0 +1,40 @@
+package com.zeapo.pwdstore.autofill;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+
+// blank activity started by service for calling startIntentSenderForResult
+public class AutofillActivity extends AppCompatActivity {
+ public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle extras = getIntent().getExtras();
+
+ if (extras != null) {
+ 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);
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ finish(); // go back to the password field app
+ if (resultCode == RESULT_OK) {
+ AutofillService.setUnlockOK(); // report the result to service
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java
new file mode 100644
index 00000000..e708c19d
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java
@@ -0,0 +1,121 @@
+package com.zeapo.pwdstore.autofill;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.support.v7.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+
+import com.zeapo.pwdstore.PasswordStore;
+import com.zeapo.pwdstore.R;
+
+public class AutofillFragment extends DialogFragment {
+ private static final int MATCH_WITH = 777;
+
+ 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();
+ final View view = inflater.inflate(R.layout.fragment_autofill, null);
+
+ builder.setView(view);
+
+ final String packageName = getArguments().getString("packageName");
+ String appName = getArguments().getString("appName");
+
+ builder.setTitle(appName);
+ try {
+ // since we can't (easily?) pass the drawable as an argument
+ builder.setIcon(callingActivity.getPackageManager().getApplicationIcon(packageName));
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+
+ SharedPreferences prefs
+ = getActivity().getApplicationContext().getSharedPreferences("autofill", 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();
+ ((EditText) view.findViewById(R.id.matched)).setText(preference);
+ }
+
+ View.OnClickListener matchPassword = new View.OnClickListener() {
+ @Override
+ public void onClick(View 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.match).setOnClickListener(matchPassword);
+ view.findViewById(R.id.matched).setOnClickListener(matchPassword);
+
+ final SharedPreferences.Editor editor = prefs.edit();
+ builder.setPositiveButton(R.string.dialog_ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.autofill_radiogroup);
+ switch (radioGroup.getCheckedRadioButtonId()) {
+ case R.id.use_default:
+ editor.remove(packageName);
+ break;
+ case R.id.first:
+ editor.putString(packageName, "/first");
+ break;
+ case R.id.never:
+ editor.putString(packageName, "/never");
+ break;
+ default:
+ EditText matched = (EditText) view.findViewById(R.id.matched);
+ String path = matched.getText().toString();
+ editor.putString(packageName, path);
+ }
+ editor.apply();
+ int position = getArguments().getInt("position");
+ callingActivity.recyclerAdapter.notifyItemChanged(position);
+
+ if (getArguments().getBoolean("finish")) {
+ callingActivity.finish();
+ }
+ }
+ });
+ builder.setNegativeButton(R.string.dialog_cancel, null);
+ return builder.create();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_OK) {
+ ((EditText) getDialog().findViewById(R.id.matched)).setText(data.getStringExtra("path"));
+ } else {
+ ((RadioButton) getDialog().findViewById(R.id.use_default)).toggle();
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java
new file mode 100644
index 00000000..16867bcd
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java
@@ -0,0 +1,146 @@
+package com.zeapo.pwdstore.autofill;
+
+import android.app.DialogFragment;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.NavUtils;
+import android.support.v4.app.TaskStackBuilder;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.SearchView;
+import android.util.Pair;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.zeapo.pwdstore.R;
+
+import java.util.HashMap;
+import java.util.List;
+
+public class AutofillPreferenceActivity extends AppCompatActivity {
+
+ private RecyclerView recyclerView;
+ AutofillRecyclerAdapter recyclerAdapter; // let fragment have access
+ private RecyclerView.LayoutManager layoutManager;
+
+ private PackageManager pm;
+
+ private boolean recreate;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.autofill_recycler_view);
+ recyclerView = (RecyclerView) findViewById(R.id.autofill_recycler);
+
+ layoutManager = new LinearLayoutManager(this);
+ recyclerView.setLayoutManager(layoutManager);
+ recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
+
+ pm = getPackageManager();
+
+ new populateTask().execute();
+
+ setTitle("Autofill Apps");
+ }
+
+ private class populateTask extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected void onPreExecute() {
+ 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> allApps = pm.queryIntentActivities(intent, 0);
+
+ HashMap<String, Pair<Drawable, String>> iconMap = new HashMap<>(allApps.size());
+ for (ResolveInfo app : allApps) {
+ iconMap.put(app.activityInfo.packageName
+ , Pair.create(app.loadIcon(pm), app.loadLabel(pm).toString()));
+ }
+
+ recyclerAdapter = new AutofillRecyclerAdapter(allApps, iconMap, pm, AutofillPreferenceActivity.this);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ findViewById(R.id.progress_bar).setVisibility(View.GONE);
+
+ recyclerView.setAdapter(recyclerAdapter);
+
+ recreate = false;
+ Bundle extras = getIntent().getExtras();
+ if (extras != null) {
+ recreate = true;
+ recyclerView.scrollToPosition(recyclerAdapter.getPosition(extras.getString("packageName")));
+ showDialog(extras.getString("packageName"), extras.getString("appName"));
+ }
+ }
+ }
+
+ @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) {
+ DialogFragment df = new AutofillFragment();
+ Bundle args = new Bundle();
+ args.putString("packageName", packageName);
+ args.putString("appName", appName);
+ args.putInt("position", recyclerAdapter.getPosition(packageName));
+ df.setArguments(args);
+ df.show(getFragmentManager(), "autofill_dialog");
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java
new file mode 100644
index 00000000..a131598a
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java
@@ -0,0 +1,148 @@
+package com.zeapo.pwdstore.autofill;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.support.v7.util.SortedList;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.util.SortedListAdapterCallback;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.zeapo.pwdstore.R;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecyclerAdapter.ViewHolder> {
+ private SortedList<ResolveInfo> apps;
+ private ArrayList<ResolveInfo> allApps;
+ private HashMap<String, Pair<Drawable, String>> iconMap;
+ private PackageManager pm;
+ private AutofillPreferenceActivity activity;
+
+ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ public View view;
+ public TextView name;
+ public TextView secondary;
+ public ImageView icon;
+ public String packageName;
+
+ public ViewHolder(View view) {
+ super(view);
+ this.view = view;
+ name = (TextView) view.findViewById(R.id.app_name);
+ secondary = (TextView) view.findViewById(R.id.secondary_text);
+ icon = (ImageView) view.findViewById(R.id.app_icon);
+ view.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ activity.showDialog(packageName, name.getText().toString());
+ }
+
+ }
+
+ public AutofillRecyclerAdapter(List<ResolveInfo> allApps, HashMap<String, Pair<Drawable, String>> iconMap
+ , final PackageManager pm, AutofillPreferenceActivity activity) {
+ SortedList.Callback<ResolveInfo> callback = new SortedListAdapterCallback<ResolveInfo>(this) {
+ @Override
+ public int compare(ResolveInfo o1, ResolveInfo o2) {
+ return o1.loadLabel(pm).toString().toLowerCase().compareTo(o2.loadLabel(pm).toString().toLowerCase());
+ }
+
+ @Override
+ public boolean areContentsTheSame(ResolveInfo oldItem, ResolveInfo newItem) {
+ return oldItem.loadLabel(pm).toString().equals(newItem.loadLabel(pm).toString());
+ }
+
+ @Override
+ public boolean areItemsTheSame(ResolveInfo item1, ResolveInfo item2) {
+ return item1.loadLabel(pm).toString().equals(item2.loadLabel(pm).toString());
+ }
+ };
+ this.apps = new SortedList<>(ResolveInfo.class, callback);
+ this.apps.addAll(allApps);
+ this.allApps = new ArrayList<>(allApps);
+ this.iconMap = new HashMap<>(iconMap);
+ this.pm = pm;
+ this.activity = activity;
+ }
+
+ @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) {
+ ResolveInfo app = apps.get(position);
+ holder.packageName = app.activityInfo.packageName;
+
+ holder.icon.setImageDrawable(iconMap.get(holder.packageName).first);
+ holder.name.setText(iconMap.get(holder.packageName).second);
+
+ holder.secondary.setVisibility(View.VISIBLE);
+ holder.view.setBackgroundResource(R.color.grey_white_1000);
+
+ SharedPreferences prefs
+ = activity.getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
+ String preference = prefs.getString(holder.packageName, "");
+ switch (preference) {
+ case "":
+ holder.secondary.setVisibility(View.GONE);
+ // "android:windowBackground"
+ holder.view.setBackgroundResource(R.color.indigo_50);
+ 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("Match with " + preference);
+ break;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return apps.size();
+ }
+
+ public int getPosition(String packageName) {
+ for (int i = 0; i < apps.size(); i++) {
+ if (apps.get(i).activityInfo.packageName.equals(packageName)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public void filter(String s) {
+ if (s.isEmpty()) {
+ apps.addAll(allApps);
+ return;
+ }
+ apps.beginBatchedUpdates();
+ for (ResolveInfo app : allApps) {
+ if (app.loadLabel(pm).toString().toLowerCase().contains(s.toLowerCase())) {
+ apps.add(app);
+ } else {
+ apps.remove(app);
+ }
+ }
+ apps.endBatchedUpdates();
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
new file mode 100644
index 00000000..ea09c863
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
@@ -0,0 +1,274 @@
+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.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v7.app.AlertDialog;
+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 com.zeapo.pwdstore.R;
+import com.zeapo.pwdstore.utils.PasswordItem;
+import com.zeapo.pwdstore.utils.PasswordRepository;
+
+import org.apache.commons.io.FileUtils;
+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.util.ArrayList;
+
+public class AutofillService extends AccessibilityService {
+ private OpenPgpServiceConnection serviceConnection;
+ private SharedPreferences settings;
+ private AccessibilityNodeInfo info; // the original source of the event (the edittext field)
+ private ArrayList<PasswordItem> items; // password choices
+ private AlertDialog dialog;
+ private AccessibilityWindowInfo window;
+ private static boolean unlockOK = false; // if openkeychain user interaction was successful
+ private CharSequence packageName;
+ private boolean ignoreActionFocus = false;
+
+ public final class Constants {
+ public static final String TAG = "Keychain";
+ }
+
+ public static void setUnlockOK() { unlockOK = true; }
+
+ @Override
+ protected void onServiceConnected() {
+ super.onServiceConnected();
+ serviceConnection = new OpenPgpServiceConnection(AutofillService.this, "org.sufficientlysecure.keychain");
+ serviceConnection.bindToService();
+ settings = PreferenceManager.getDefaultSharedPreferences(this);
+ }
+ // TODO change search/search results (just use first result)
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ // if returning to the source app from a successful AutofillActivity
+ if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ && event.getPackageName().equals(packageName) && unlockOK) {
+ decryptAndVerify();
+ }
+
+ // nothing to do if not password field focus, android version, or field is keychain app
+ if (!event.isPassword()
+ || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
+ || event.getPackageName().equals("org.sufficientlysecure.keychain")) {
+ // 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().toString().contains("inputmethod"));
+ }
+ if (dismiss && dialog != null && dialog.isShowing()) {
+ dialog.dismiss();
+ }
+ return;
+ }
+
+ if (dialog != null && dialog.isShowing()) {
+ // if the view was clicked, the click event follows the focus event
+ // since the focus event was already handled, ignore click event
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
+ return;
+ }
+ // if past this point, a new dialog will be created, so dismiss the existing
+ dialog.dismiss();
+ }
+
+ // ignore the ACTION_FOCUS from decryptAndVerify otherwise dialog will appear after Fill
+ if (ignoreActionFocus) {
+ ignoreActionFocus = false;
+ return;
+ }
+
+ info = event.getSource();
+
+ // save the dialog's corresponding window so we can use getWindows() above to check whether dismiss
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ window = info.getWindow();
+ }
+
+ // 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;
+ }
+ final String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString();
+
+ // 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 = getSharedPreferences("autofill", Context.MODE_PRIVATE);
+ String preference = prefs.getString(event.getPackageName().toString(), defValue);
+ switch (preference) {
+ case "/first":
+ if (!PasswordRepository.isInitialized()) {
+ PasswordRepository.initialize(this);
+ }
+ items = recursiveFilter(appName, null);
+ break;
+ case "/never":
+ return;
+ default:
+ if (!PasswordRepository.isInitialized()) {
+ PasswordRepository.initialize(this);
+ }
+ String path = PasswordRepository.getWorkTree() + "/" + preference + ".gpg";
+ File file = new File(path);
+ items = new ArrayList<>();
+ items.add(PasswordItem.newPassword(file.getName(), file, PasswordRepository.getRepositoryDirectory(this)));
+ }
+ if (items.isEmpty()) {
+ return;
+ }
+
+ if (dialog == null) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
+ builder.setNegativeButton(R.string.dialog_cancel, null);
+ builder.setPositiveButton(R.string.autofill_fill, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ decryptAndVerify();
+ }
+ });
+ builder.setNeutralButton("Settings", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int 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", info.getPackageName());
+ intent.putExtra("appName", appName);
+ startActivity(intent);
+ }
+ });
+ dialog = builder.create();
+ dialog.setIcon(R.drawable.ic_launcher);
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
+ }
+ dialog.setTitle(items.get(0).toString());
+ dialog.show();
+ }
+
+ private ArrayList<PasswordItem> recursiveFilter(String filter, File dir) {
+ ArrayList<PasswordItem> items = new ArrayList<>();
+ ArrayList<PasswordItem> passwordItems = dir == null ?
+ PasswordRepository.getPasswords(PasswordRepository.getRepositoryDirectory(this)) :
+ PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(this));
+ for (PasswordItem item : passwordItems) {
+ if (item.getType() == PasswordItem.TYPE_CATEGORY) {
+ items.addAll(recursiveFilter(filter, item.getFile()));
+ }
+ if (item.toString().toLowerCase().contains(filter.toLowerCase())) {
+ items.add(item);
+ }
+ }
+ return items;
+ }
+
+ @Override
+ public void onInterrupt() {
+
+ }
+
+ public void decryptAndVerify() {
+ unlockOK = false;
+ packageName = info.getPackageName();
+ Intent data = new Intent();
+ data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
+ InputStream is = null;
+ try {
+ is = FileUtils.openInputStream(items.get(0).getFile());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+ OpenPgpApi api = new OpenPgpApi(AutofillService.this, serviceConnection.getService());
+ Intent result = api.executeApi(data, is, os);
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS: {
+ try {
+ String[] passContent = os.toString("UTF-8").split("\n");
+
+ // if the user focused on something else, take focus back
+ // but this will open another dialog...hack to ignore this
+ ignoreActionFocus = info.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,
+ passContent[0]);
+ info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
+ } else {
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("autofill_pm", passContent[0]);
+ clipboard.setPrimaryClip(clip);
+ info.performAction(AccessibilityNodeInfo.ACTION_PASTE);
+
+ clip = ClipData.newPlainText("autofill_pm", "MyPasswordIsDaBest!");
+ clipboard.setPrimaryClip(clip);
+ if (settings.getBoolean("clear_clipboard_20x", false)) {
+ for (int i = 0; i < 19; i++) {
+ clip = ClipData.newPlainText(String.valueOf(i), String.valueOf(i));
+ clipboard.setPrimaryClip(clip);
+ }
+ }
+ }
+ } 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;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java b/app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java
new file mode 100644
index 00000000..bd5ec9de
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java
@@ -0,0 +1,105 @@
+package com.zeapo.pwdstore.autofill;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class DividerItemDecoration extends RecyclerView.ItemDecoration {
+
+ private static final int[] ATTRS = new int[]{
+ android.R.attr.listDivider
+ };
+
+ public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
+
+ public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
+
+ private Drawable mDivider;
+
+ private int mOrientation;
+
+ public DividerItemDecoration(Context context, int orientation) {
+ final TypedArray a = context.obtainStyledAttributes(ATTRS);
+ mDivider = a.getDrawable(0);
+ a.recycle();
+ setOrientation(orientation);
+ }
+
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
+ throw new IllegalArgumentException("invalid orientation");
+ }
+ mOrientation = orientation;
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ if (mOrientation == VERTICAL_LIST) {
+ drawVertical(c, parent);
+ } else {
+ drawHorizontal(c, parent);
+ }
+ }
+
+ public void drawVertical(Canvas c, RecyclerView parent) {
+ final int left = parent.getPaddingLeft();
+ final int right = parent.getWidth() - parent.getPaddingRight();
+
+ final int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
+ .getLayoutParams();
+ final int top = child.getBottom() + params.bottomMargin;
+ final int bottom = top + mDivider.getIntrinsicHeight();
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(c);
+ }
+ }
+
+ public void drawHorizontal(Canvas c, RecyclerView parent) {
+ final int top = parent.getPaddingTop();
+ final int bottom = parent.getHeight() - parent.getPaddingBottom();
+
+ final int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
+ .getLayoutParams();
+ final int left = child.getRight() + params.rightMargin;
+ final int right = left + mDivider.getIntrinsicHeight();
+ mDivider.setBounds(left, top, right, bottom);
+ mDivider.draw(c);
+ }
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ if (mOrientation == VERTICAL_LIST) {
+ outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
+ } else {
+ outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
+ }
+ }
+}
+