aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java/com
diff options
context:
space:
mode:
authorwongma7 <wongma7@users.noreply.github.com>2016-01-05 19:40:44 -0500
committerwongma7 <wongma7@users.noreply.github.com>2016-01-05 19:40:44 -0500
commit2064c2eaac88216e6cfb6819831c3afe39158047 (patch)
tree031d0cb46a2e473bef302642c56cdd684a460cfe /app/src/main/java/com
parent9e930ae92351c4044e3efb1c715c99c87fc7edd7 (diff)
parent5cf345eaa3e37cfaca82aa8f7d66328361ca27c3 (diff)
Merge pull request #146 from zeapo/webview
Autofill: multiple associations per app & WebView/Chrome
Diffstat (limited to 'app/src/main/java/com')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java69
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java200
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java46
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java116
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java316
5 files changed, 596 insertions, 151 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java
index 6d11936b..d40c5123 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java
@@ -1,22 +1,34 @@
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.support.v7.app.AppCompatActivity;
import android.util.Log;
+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) {
+ if (extras != null && extras.containsKey("pending_intent")) {
try {
PendingIntent pi = extras.getParcelable("pending_intent");
if (pi == null) {
@@ -27,14 +39,65 @@ public class AutofillActivity extends AppCompatActivity {
} 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
- if (resultCode == RESULT_OK) {
- AutofillService.setResultData(data); // report the result to service
+ switch (requestCode) {
+ case REQUEST_CODE_DECRYPT_AND_VERIFY:
+ if (resultCode == RESULT_OK) {
+ AutofillService.getInstance().setResultData(data); // report the result to service
+ }
+ break;
+ case REQUEST_CODE_PICK:
+ if (resultCode == RESULT_OK) {
+ AutofillService.getInstance().setPickedPassword(data.getStringExtra("path"));
+ }
+ break;
+ case REQUEST_CODE_PICK_MATCH_WITH:
+ if (resultCode == RESULT_OK) {
+ // need to not only decrypt the picked password, but also
+ // update the "match with" preference
+ Bundle extras = getIntent().getExtras();
+ String packageName = extras.getString("packageName");
+ boolean isWeb = extras.getBoolean("isWeb");
+
+ String path = data.getStringExtra("path");
+ AutofillService.getInstance().setPickedPassword(data.getStringExtra("path"));
+
+ SharedPreferences prefs;
+ if (!isWeb) {
+ prefs = getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
+ } else {
+ prefs = getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
+ }
+ SharedPreferences.Editor editor = prefs.edit();
+ String preference = prefs.getString(packageName, "");
+ switch (preference) {
+ case "":
+ case "/first":
+ case "/never":
+ editor.putString(packageName, path);
+ break;
+ default:
+ List<String> matches = new ArrayList<>(Arrays.asList(preference.trim().split("\n")));
+ matches.add(path);
+ String paths = StringUtils.join(matches, "\n");
+ editor.putString(packageName, paths);
+ }
+ editor.apply();
+ }
+ break;
}
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java
index 5d342f4a..583b5064 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java
@@ -4,6 +4,7 @@ import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
@@ -12,15 +13,23 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+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 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() {
}
@@ -32,23 +41,60 @@ public class AutofillFragment extends DialogFragment {
// 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");
+ final String appName = getArguments().getString("appName");
+ isWeb = getArguments().getBoolean("isWeb");
- builder.setTitle(appName);
+ // 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 {
- // since we can't (easily?) pass the drawable as an argument
- builder.setIcon(callingActivity.getPackageManager().getApplicationIcon(packageName));
+ builder.setIcon(callingActivity.getPackageManager().getApplicationIcon(iconPackageName));
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
- SharedPreferences prefs
- = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
+ // 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...
+ @Override
+ public View getView(int position, View convertView, 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(
+ new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long 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 "":
@@ -62,9 +108,11 @@ public class AutofillFragment extends DialogFragment {
break;
default:
((RadioButton) view.findViewById(R.id.match)).toggle();
- ((EditText) view.findViewById(R.id.matched)).setText(preference);
+ // trim to remove the last blank element
+ adapter.addAll(preference.trim().split("\n"));
}
+ // add items with the + button
View.OnClickListener matchPassword = new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -74,48 +122,130 @@ public class AutofillFragment extends DialogFragment {
startActivityForResult(intent, MATCH_WITH);
}
};
- view.findViewById(R.id.match).setOnClickListener(matchPassword);
- view.findViewById(R.id.matched).setOnClickListener(matchPassword);
+ view.findViewById(R.id.matchButton).setOnClickListener(matchPassword);
- final SharedPreferences.Editor editor = prefs.edit();
+ // write to preferences when OK clicked
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();
- // if recyclerAdapter has not loaded yet, there is no need to notifyItemChanged
- if (callingActivity.recyclerAdapter != null) {
- int position = callingActivity.recyclerAdapter.getPosition(packageName);
- callingActivity.recyclerAdapter.notifyItemChanged(position);
- }
}
});
builder.setNegativeButton(R.string.dialog_cancel, null);
+ final SharedPreferences.Editor editor = prefs.edit();
+ if (isWeb) {
+ builder.setNeutralButton(R.string.autofill_apps_delete, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int 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(new View.OnClickListener() {
+ @Override
+ public void onClick(View 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) {
+ packageName = ((EditText) dialog.findViewById(R.id.webURL)).getText().toString();
+
+ // handle some errors and don't dismiss the dialog
+ EditText webURL = (EditText) dialog.findViewById(R.id.webURL);
+ 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 = (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) {
- ((EditText) getDialog().findViewById(R.id.matched)).setText(data.getStringExtra("path"));
- } else {
- ((RadioButton) getDialog().findViewById(R.id.use_default)).toggle();
+ adapter.add(data.getStringExtra("path"));
}
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java
index adfce0b1..79f6565b 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java
@@ -1,12 +1,14 @@
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.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.NavUtils;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.view.MenuItemCompat;
@@ -14,15 +16,15 @@ 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.ArrayList;
import java.util.List;
+import java.util.Map;
public class AutofillPreferenceActivity extends AppCompatActivity {
@@ -49,15 +51,24 @@ public class AutofillPreferenceActivity extends AppCompatActivity {
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"));
+ showDialog(extras.getString("packageName"), extras.getString("appName"), extras.getBoolean("isWeb"));
}
setTitle("Autofill Apps");
+
+ final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+ fab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ showDialog("", "", true);
+ }
+ });
}
private class populateTask extends AsyncTask<Void, Void, Void> {
@@ -70,15 +81,25 @@ public class AutofillPreferenceActivity extends AppCompatActivity {
protected Void doInBackground(Void... params) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
- List<ResolveInfo> allApps = pm.queryIntentActivities(intent, 0);
+ List<ResolveInfo> allAppsResolveInfo = pm.queryIntentActivities(intent, 0);
+ List<AutofillRecyclerAdapter.AppInfo> allApps = new ArrayList<>();
- 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()));
+ 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, iconMap, pm, AutofillPreferenceActivity.this);
+ recyclerAdapter = new AutofillRecyclerAdapter(allApps, pm, AutofillPreferenceActivity.this);
return null;
}
@@ -89,7 +110,7 @@ public class AutofillPreferenceActivity extends AppCompatActivity {
recyclerView.setAdapter(recyclerAdapter);
Bundle extras = getIntent().getExtras();
if (extras != null) {
- recyclerView.scrollToPosition(recyclerAdapter.getPosition(extras.getString("packageName")));
+ recyclerView.scrollToPosition(recyclerAdapter.getPosition(extras.getString("appName")));
}
}
}
@@ -138,11 +159,12 @@ public class AutofillPreferenceActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item);
}
- public void showDialog(String packageName, String appName) {
+ 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");
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java
index 15f114c6..901ff903 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java
@@ -3,12 +3,10 @@ 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;
@@ -18,15 +16,15 @@ 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 SortedList<AppInfo> apps;
+ private ArrayList<AppInfo> allApps; // for filtering, maintain a list of all
private PackageManager pm;
private AutofillPreferenceActivity activity;
+ Drawable browserIcon = null;
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public View view;
@@ -34,6 +32,8 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
public TextView secondary;
public ImageView icon;
public String packageName;
+ public String appName;
+ public Boolean isWeb;
public ViewHolder(View view) {
super(view);
@@ -46,35 +46,60 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
@Override
public void onClick(View v) {
- activity.showDialog(packageName, name.getText().toString());
+ activity.showDialog(packageName, appName, isWeb);
}
}
- public AutofillRecyclerAdapter(List<ResolveInfo> allApps, HashMap<String, Pair<Drawable, String>> iconMap
- , final PackageManager pm, AutofillPreferenceActivity activity) {
- SortedList.Callback<ResolveInfo> callback = new SortedListAdapterCallback<ResolveInfo>(this) {
+ public static class AppInfo {
+ public String packageName;
+ public String appName;
+ public boolean isWeb;
+ public Drawable icon;
+
+ public 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 != null && o instanceof AppInfo && this.appName.equals(((AppInfo) o).appName);
+ }
+ }
+
+ public 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(ResolveInfo o1, ResolveInfo o2) {
- return o1.loadLabel(pm).toString().toLowerCase().compareTo(o2.loadLabel(pm).toString().toLowerCase());
+ public int compare(AppInfo o1, AppInfo o2) {
+ return o1.appName.toLowerCase().compareTo(o2.appName.toLowerCase());
}
@Override
- public boolean areContentsTheSame(ResolveInfo oldItem, ResolveInfo newItem) {
- return oldItem.loadLabel(pm).toString().equals(newItem.loadLabel(pm).toString());
+ public boolean areContentsTheSame(AppInfo oldItem, AppInfo newItem) {
+ return oldItem.appName.equals(newItem.appName);
}
@Override
- public boolean areItemsTheSame(ResolveInfo item1, ResolveInfo item2) {
- return item1.loadLabel(pm).toString().equals(item2.loadLabel(pm).toString());
+ public boolean areItemsTheSame(AppInfo item1, AppInfo item2) {
+ return item1.appName.equals(item2.appName);
}
};
- this.apps = new SortedList<>(ResolveInfo.class, callback);
+ this.apps = new SortedList<>(AppInfo.class, callback);
this.apps.addAll(allApps);
this.allApps = new ArrayList<>(allApps);
- this.iconMap = new HashMap<>(iconMap);
this.pm = pm;
this.activity = activity;
+ try {
+ browserIcon = activity.getPackageManager().getApplicationIcon("com.android.browser");
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
}
@Override
@@ -86,17 +111,23 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
@Override
public void onBindViewHolder(AutofillRecyclerAdapter.ViewHolder holder, int position) {
- ResolveInfo app = apps.get(position);
- holder.packageName = app.activityInfo.packageName;
+ AppInfo app = apps.get(position);
+ holder.packageName = app.packageName;
+ holder.appName = app.appName;
+ holder.isWeb = app.isWeb;
- holder.icon.setImageDrawable(iconMap.get(holder.packageName).first);
- holder.name.setText(iconMap.get(holder.packageName).second);
+ 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
- = activity.getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
+ 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 "":
@@ -110,7 +141,12 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
holder.secondary.setText(R.string.autofill_apps_never);
break;
default:
- holder.secondary.setText("Match with " + preference);
+ 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;
}
}
@@ -120,13 +156,25 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
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 int getPosition(String appName) {
+ return apps.indexOf(new AppInfo(null, appName, false, null));
+ }
+
+ // for websites, URL = packageName == appName
+ public void addWebsite(String packageName) {
+ apps.add(new AppInfo(packageName, packageName, true, browserIcon));
+ allApps.add(new AppInfo(packageName, packageName, true, browserIcon));
+ }
+
+ public void removeWebsite(String packageName) {
+ apps.remove(new AppInfo(null, packageName, false, null));
+ allApps.remove(new AppInfo(null, packageName, false, null)); // compare with equals
+ }
+
+ public 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));
}
public void filter(String s) {
@@ -135,8 +183,8 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
return;
}
apps.beginBatchedUpdates();
- for (ResolveInfo app : allApps) {
- if (app.loadLabel(pm).toString().toLowerCase().contains(s.toLowerCase())) {
+ for (AppInfo app : allApps) {
+ if (app.appName.toLowerCase().contains(s.toLowerCase())) {
apps.add(app);
} else {
apps.remove(app);
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
index 2b32a809..526d6f98 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
@@ -24,7 +24,6 @@ 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;
@@ -38,24 +37,47 @@ 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<PasswordItem> items; // password choices
+ private ArrayList<File> items; // password choices
+ private int lastWhichItem;
private AlertDialog dialog;
private AccessibilityWindowInfo window;
- private static Intent resultData = null; // need the intent which contains results from user interaction
+ 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;
public final class Constants {
public static final String TAG = "Keychain";
}
- public static void setResultData(Intent data) { resultData = data; }
+ public static AutofillService getInstance() {
+ return instance;
+ }
+
+ public void setResultData(Intent data) { resultData = data; }
+
+ public void setPickedPassword(String path) {
+ items.add(new File(PasswordRepository.getWorkTree() + "/" + path + ".gpg"));
+ bindDecryptAndVerify();
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ instance = this;
+ }
@Override
protected void onServiceConnected() {
@@ -66,18 +88,54 @@ public class AutofillService extends AccessibilityService {
settings = PreferenceManager.getDefaultSharedPreferences(this);
}
- // TODO change search/search results (just use first result)
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
+ // TODO there should be a better way of disabling service
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ return;
+ }
+
// if returning to the source app from a successful AutofillActivity
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
&& event.getPackageName().equals(packageName) && resultData != null) {
bindDecryptAndVerify();
}
- // nothing to do if not password field focus, android version, or field is keychain app
+ // 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.getSource() != null
+ && (event.getSource().getPackageName().equals("com.android.chrome")
+ || event.getSource().getPackageName().equals("com.android.browser")))) {
+ // there is a chance for getRootInActiveWindow() to return null at any time. save it.
+ 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) {
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // nothing to do if not password field focus, field is keychain app
if (!event.isPassword()
- || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
+ || event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|| event.getPackageName().equals("org.sufficientlysecure.keychain")) {
dismissDialog(event);
return;
@@ -91,6 +149,7 @@ public class AutofillService extends AccessibilityService {
}
// 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
@@ -103,12 +162,14 @@ public class AutofillService extends AccessibilityService {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !Settings.canDrawOverlays(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
- Uri.parse("package:" + getApplicationContext().getPackageName()));
+ 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();
// save the dialog's corresponding window so we can use getWindows() in dismissDialog
@@ -116,22 +177,63 @@ public class AutofillService extends AccessibilityService {
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;
+ 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)) {
+ 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;
+
+ setMatchingPasswords(appName, packageName, false);
+ } else {
+ packageName = setMatchingPasswords(webViewTitle, webViewURL, true);
+ appName = packageName;
+ isWeb = true;
}
- final String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString();
- getMatchingPassword(appName, info.getPackageName().toString());
- if (items.isEmpty()) {
+ // if autofill_always checked, show dialog even if no matches (automatic
+ // or otherwise)
+ if (items.isEmpty() && !settings.getBoolean("autofill_always", false)) {
return;
}
+ showDialog(packageName, appName, isWeb);
+ }
- showDialog(appName);
+ private String searchWebView(AccessibilityNodeInfo source) {
+ if (source == null) {
+ 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 "";
+ }
+ if (searchWebView(u) != null) {
+ return searchWebView(u);
+ }
+ u.recycle();
+ }
+ return null;
}
// dismiss the dialog if the window has changed
@@ -149,80 +251,157 @@ public class AutofillService extends AccessibilityService {
}
if (dismiss && dialog != null && dialog.isShowing()) {
dialog.dismiss();
+ dialog = null;
}
}
- private void getMatchingPassword(String appName, String packageName) {
+ private String setMatchingPasswords(String appName, String packageName, boolean isWeb) {
+ // Return the URL needed to open the corresponding Settings.
+ String settingsURL = 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 = getSharedPreferences("autofill", Context.MODE_PRIVATE);
- String preference = prefs.getString(packageName, defValue);
+ SharedPreferences prefs;
+ String preference;
+
+ // for websites unlike apps there can be blank preference of "" which
+ // means use default, so ignore it.
+ if (!isWeb) {
+ prefs = getSharedPreferences("autofill", Context.MODE_PRIVATE);
+ preference = prefs.getString(packageName, defValue);
+ } else {
+ prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
+ preference = defValue;
+ Map<String, ?> prefsMap = prefs.getAll();
+ for (String key : prefsMap.keySet()) {
+ if ((webViewURL.toLowerCase().contains(key.toLowerCase()) || key.toLowerCase().contains(webViewURL.toLowerCase()))
+ && !prefs.getString(key, null).equals("")) {
+ preference = prefs.getString(key, null);
+ settingsURL = key;
+ }
+ }
+ }
+
switch (preference) {
case "/first":
if (!PasswordRepository.isInitialized()) {
PasswordRepository.initialize(this);
}
- items = recursiveFilter(appName, null);
+ items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), appName);
break;
case "/never":
- items.clear();
- 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)));
+ break;
+ default:
+ getPreferredPasswords(preference);
}
+
+ return settingsURL;
}
- 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()));
+ // 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.getWorkTree() + "/" + password + ".gpg";
+ if (new File(path).exists()) {
+ items.add(new File(path));
}
- if (item.toString().toLowerCase().contains(filter.toLowerCase())) {
- items.add(item);
+ }
+ }
+
+ 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 (appName.toLowerCase().contains(file.getName().toLowerCase().replace(".gpg", ""))) {
+ items.add(file);
+ }
+ } else {
+ // ignore .git directory
+ if (file.getName().equals(".git"))
+ continue;
+ items.addAll(searchPasswords(file, appName));
}
}
return items;
}
- private void showDialog(final String appName) {
- 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) {
+ private void showDialog(final String packageName, final String appName, final boolean isWeb) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
+ builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface d, int which) {
+ dialog.dismiss();
+ dialog = null;
+ }
+ });
+ 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", 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()] = "Pick...";
+ itemNames[items.size() + 1] = "Pick and match...";
+ builder.setItems(itemNames, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ lastWhichItem = which;
+ if (which < items.size()) {
bindDecryptAndVerify();
- }
- });
- 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);
+ } 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("packageName", info.getPackageName());
- intent.putExtra("appName", appName);
+ 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();
- 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 = builder.create();
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ 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.setTitle(items.get(0).toString());
+ dialog.getWindow().setLayout((int) (240 * getApplicationContext().getResources().getDisplayMetrics().density)
+ , (int) (height * getApplicationContext().getResources().getDisplayMetrics().density));
dialog.show();
}
@@ -266,13 +445,14 @@ public class AutofillService extends AccessibilityService {
}
InputStream is = null;
try {
- is = FileUtils.openInputStream(items.get(0).getFile());
+ 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: {
@@ -281,6 +461,7 @@ public class AutofillService extends AccessibilityService {
// 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 = info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Bundle args = new Bundle();
@@ -302,6 +483,7 @@ public class AutofillService extends AccessibilityService {
}
}
}
+ info.recycle();
} catch (UnsupportedEncodingException e) {
Log.e(Constants.TAG, "UnsupportedEncodingException", e);
}