Merge pull request #146 from zeapo/webview

Autofill: multiple associations per app & WebView/Chrome
This commit is contained in:
wongma7 2016-01-05 19:40:44 -05:00
commit 2064c2eaac
12 changed files with 661 additions and 171 deletions

View file

@ -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
switch (requestCode) {
case REQUEST_CODE_DECRYPT_AND_VERIFY:
if (resultCode == RESULT_OK) {
AutofillService.setResultData(data); // report the result to service
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

@ -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");
// 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,17 +122,80 @@ 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);
}
});
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");
@ -93,29 +204,48 @@ public class AutofillFragment extends DialogFragment {
editor.putString(packageName, "/never");
break;
default:
EditText matched = (EditText) view.findViewById(R.id.matched);
String path = matched.getText().toString();
editor.putString(packageName, path);
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();
// if recyclerAdapter has not loaded yet, there is no need to notifyItemChanged
// notify the recycler adapter if it is loaded
if (callingActivity.recyclerAdapter != null) {
int position = callingActivity.recyclerAdapter.getPosition(packageName);
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();
}
});
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();
adapter.add(data.getStringExtra("path"));
}
}
}

View file

@ -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)));
}
recyclerAdapter = new AutofillRecyclerAdapter(allApps, iconMap, pm, AutofillPreferenceActivity.this);
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;
}
@ -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");
}

View file

@ -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) {
@Override
public int compare(ResolveInfo o1, ResolveInfo o2) {
return o1.loadLabel(pm).toString().toLowerCase().compareTo(o2.loadLabel(pm).toString().toLowerCase());
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 areContentsTheSame(ResolveInfo oldItem, ResolveInfo newItem) {
return oldItem.loadLabel(pm).toString().equals(newItem.loadLabel(pm).toString());
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(AppInfo o1, AppInfo o2) {
return o1.appName.toLowerCase().compareTo(o2.appName.toLowerCase());
}
@Override
public boolean areItemsTheSame(ResolveInfo item1, ResolveInfo item2) {
return item1.loadLabel(pm).toString().equals(item2.loadLabel(pm).toString());
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<>(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;
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));
}
return -1;
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);

View file

@ -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,6 +177,15 @@ public class AutofillService extends AccessibilityService {
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)) {
packageName = info.getPackageName().toString();
// get the app name and find a corresponding password
PackageManager packageManager = getPackageManager();
ApplicationInfo applicationInfo;
@ -124,14 +194,46 @@ public class AutofillService extends AccessibilityService {
} catch (PackageManager.NameNotFoundException e) {
applicationInfo = null;
}
final String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString();
appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString();
getMatchingPassword(appName, info.getPackageName().toString());
if (items.isEmpty()) {
return;
isWeb = false;
setMatchingPasswords(appName, packageName, false);
} else {
packageName = setMatchingPasswords(webViewTitle, webViewURL, true);
appName = packageName;
isWeb = true;
}
showDialog(appName);
// 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);
}
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,59 +251,100 @@ 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;
items = new ArrayList<>();
break;
default:
getPreferredPasswords(preference);
}
return settingsURL;
}
// 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 path = PasswordRepository.getWorkTree() + "/" + preference + ".gpg";
File file = new File(path);
String preferredPasswords[] = preference.split("\n");
items = new ArrayList<>();
items.add(PasswordItem.newPassword(file.getName(), file, PasswordRepository.getRepositoryDirectory(this)));
for (String password : preferredPasswords) {
String path = PasswordRepository.getWorkTree() + "/" + password + ".gpg";
if (new File(path).exists()) {
items.add(new File(path));
}
}
}
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()));
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);
}
if (item.toString().toLowerCase().contains(filter.toLowerCase())) {
items.add(item);
} 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) {
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, null);
builder.setPositiveButton(R.string.autofill_fill, new DialogInterface.OnClickListener() {
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
bindDecryptAndVerify();
public void onClick(DialogInterface d, int which) {
dialog.dismiss();
dialog = null;
}
});
builder.setNeutralButton("Settings", new DialogInterface.OnClickListener() {
@ -210,19 +353,55 @@ public class AutofillService extends AccessibilityService {
// 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("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();
} 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();
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);
// 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);
}

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.RecyclerView
android:id="@+id/autofill_recycler"
@ -17,4 +18,19 @@
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:src="@drawable/ic_action_new"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
app:elevation="6dp"
app:pressedTranslationZ="12dp"
app:backgroundTint="@color/blue_grey_500"
app:rippleColor="@color/blue_grey_50"
app:borderWidth="0dp"
android:layout_margin="@dimen/fab_compat_margin"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"/>
</RelativeLayout>

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -8,6 +9,19 @@
android:paddingRight="24dp"
android:paddingTop="20dp">
<android.support.design.widget.TextInputLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hintTextAppearance="@style/TextAppearance.AppCompat">
<EditText
android:id="@+id/webURL"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="URL"
android:inputType="textUri"/>
</android.support.design.widget.TextInputLayout>
<RadioGroup
android:id="@+id/autofill_radiogroup"
android:layout_width="match_parent"
@ -37,12 +51,20 @@
android:checked="false"
android:text="@string/autofill_apps_match_ellipsis" />
<EditText
<ListView
android:id="@+id/matched"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:editable="false" />
android:layout_weight="1"/>
<Button
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"
android:id="@+id/matchButton"
android:layout_gravity="center_horizontal"/>
<RadioButton
android:id="@+id/never"

View file

@ -168,5 +168,6 @@
<string name="autofill_apps_default">Použít výchozí nastavení</string>
<string name="autofill_apps_first">Automaticky spárovat</string>
<string name="autofill_apps_match_ellipsis">Spárovat s…</string>
<string name="autofill_apps_match">Spárovat s</string>
<string name="autofill_apps_never">Nikdy nepárovat</string>
</resources>

View file

@ -112,7 +112,7 @@
<string name="pref_password_dialog_title">Set the time you want the password to be in clipboard</string>
<string name="pref_copy_title">Automatically Copy Password</string>
<string name="pref_copy_dialog_title">Automatically copy the password to the clipboard after decryption was successful.</string>
<string name="ssh_key_success_dialog_title" translatable="false">SSH-key imported</string>
<string name="ssh_key_success_dialog_title">SSH-key imported</string>
<string name="ssh_key_error_dialog_title">Error while trying to import the ssh-key</string>
<string name="ssh_key_error_dialog_text">Message : \n</string>
<string name="pref_recursive_filter">Recursive filtering</string>
@ -121,10 +121,11 @@
<string name="pref_autofill_enable_msg">Tap OK to go to Accessibility settings. There, tap Password Store under Services then tap the switch in the top right to turn it on or off.</string>
<string name="pref_autofill_enable_msg2">Once the service is on, a dialog will appear when you click on a password field in an app if a matching password for the app exists.</string>
<string name="pref_autofill_enable_msg3">Password Store attempts to match apps with passwords automatically. You can change this default setting and also matching settings per-app.</string>
<string name="pref_autofill_apps_title">Per-app settings</string>
<string name="pref_autofill_apps_title">App and website settings</string>
<string name="pref_autofill_apps_hint">Customize autofill settings for specific apps.</string>
<string name="pref_autofill_default_title">Automatically match by default</string>
<string name="pref_autofill_default_hint">Default to \'Automatically match\' for apps without custom settings. Otherwise, \'Never match.\'</string>
<string name="pref_autofill_always_title">Always show dialog</string>
<string name="pref_clear_clipboard_title">Clear clipboard 20 times</string>
<string name="pref_clear_clipboard_hint">Store nonsense in the clipboard 20 times instead of just once. Useful on Samsung phones that feature clipboard history.</string>
@ -168,5 +169,7 @@
<string name="autofill_apps_default">Use default setting</string>
<string name="autofill_apps_first">Automatically match</string>
<string name="autofill_apps_match_ellipsis">Match with…</string>
<string name="autofill_apps_match">Match with</string>
<string name="autofill_apps_never">Never match</string>
<string name="autofill_apps_delete">Delete</string>
</resources>

View file

@ -1,9 +1,8 @@
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/autofill_description"
android:accessibilityEventTypes="typeViewFocused|typeViewClicked|typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagReportViewIds"
android:accessibilityFeedbackType="feedbackGeneric"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:canRequestEnhancedWebAccessibility="true"
/>

View file

@ -79,7 +79,6 @@
<Preference
android:dependency="autofill_enable"
android:key="autofill_apps"
android:summary="@string/pref_autofill_apps_hint"
android:title="@string/pref_autofill_apps_title"/>
<CheckBoxPreference
android:dependency="autofill_enable"
@ -87,6 +86,11 @@
android:key="autofill_default"
android:summary="@string/pref_autofill_default_hint"
android:title="@string/pref_autofill_default_title"/>
<CheckBoxPreference
android:dependency="autofill_enable"
android:defaultValue="false"
android:key="autofill_always"
android:title="@string/pref_autofill_always_title"/>
</PreferenceCategory>
<PreferenceCategory android:title="Misc">