diff options
author | wongma7 <wongma7@users.noreply.github.com> | 2016-01-05 19:40:44 -0500 |
---|---|---|
committer | wongma7 <wongma7@users.noreply.github.com> | 2016-01-05 19:40:44 -0500 |
commit | 2064c2eaac88216e6cfb6819831c3afe39158047 (patch) | |
tree | 031d0cb46a2e473bef302642c56cdd684a460cfe /app/src/main/java/com | |
parent | 9e930ae92351c4044e3efb1c715c99c87fc7edd7 (diff) | |
parent | 5cf345eaa3e37cfaca82aa8f7d66328361ca27c3 (diff) |
Merge pull request #146 from zeapo/webview
Autofill: multiple associations per app & WebView/Chrome
Diffstat (limited to 'app/src/main/java/com')
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); } |