diff options
author | Matthew Wong <wongma@protonmail.ch> | 2015-08-09 16:04:09 -0400 |
---|---|---|
committer | Matthew Wong <wongma@protonmail.ch> | 2015-08-14 17:36:48 -0400 |
commit | ad173d9d05c29790d6f647df641e5de28e32fd58 (patch) | |
tree | 4cbf4579d39392c2d647459eec10ff7f95466a7e | |
parent | 51a05087e5a349afa3f4e0febdc3a339179e9bbd (diff) |
Autofill per-app settings dialog opens on click on suggestion or on existing app
7 files changed, 245 insertions, 14 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 5c42ab8d..77a44c7a 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java @@ -1,5 +1,6 @@ package com.zeapo.pwdstore.autofill; +import android.app.DialogFragment; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -33,7 +34,7 @@ public class AutofillActivity extends AppCompatActivity { private boolean bound = false; private RecyclerView recyclerView; - private AutofillRecyclerAdapter recyclerAdapter; + AutofillRecyclerAdapter recyclerAdapter; // let fragment have access private RecyclerView.LayoutManager layoutManager; @Override @@ -41,6 +42,7 @@ public class AutofillActivity extends AppCompatActivity { super.onCreate(savedInstanceState); Intent intent = getIntent(); Bundle extras = intent.getExtras(); + // if called by service just for startIntentSenderForResult if (extras != null) { try { @@ -64,21 +66,26 @@ public class AutofillActivity extends AppCompatActivity { // apps for which the user has custom settings should be in the recycler final PackageManager pm = getPackageManager(); - final List<ApplicationInfo> allApps = pm.getInstalledApplications(0); SharedPreferences prefs = getSharedPreferences("autofill", Context.MODE_PRIVATE); Map<String, ?> prefApps = prefs.getAll(); ArrayList<ApplicationInfo> apps = new ArrayList<>(); - for (ApplicationInfo applicationInfo : allApps) { - if (prefApps.containsKey(applicationInfo.packageName)) { - apps.add(applicationInfo); + for (String packageName : prefApps.keySet()) { + try { + apps.add(pm.getApplicationInfo(packageName, 0)); + } catch (PackageManager.NameNotFoundException e) { + // remove invalid entries (from uninstalled apps?) + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(packageName).apply(); } } - recyclerAdapter = new AutofillRecyclerAdapter(apps, pm); + recyclerAdapter = new AutofillRecyclerAdapter(apps, pm, this); recyclerView.setAdapter(recyclerAdapter); + // show the search bar by default but don't open the keyboard getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); final SearchView searchView = (SearchView) findViewById(R.id.app_search); + searchView.clearFocus(); // create search suggestions of apps with icons & names final SimpleCursorAdapter.ViewBinder viewBinder = new SimpleCursorAdapter.ViewBinder() { @@ -97,6 +104,8 @@ public class AutofillActivity extends AppCompatActivity { return true; } }; + + final List<ApplicationInfo> allApps = pm.getInstalledApplications(0); searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { @@ -108,7 +117,9 @@ public class AutofillActivity extends AppCompatActivity { // should be a better/faster way to do this? MatrixCursor matrixCursor = new MatrixCursor(new String[]{"_id", "package", "label"}); for (ApplicationInfo applicationInfo : allApps) { - if (applicationInfo.loadLabel(pm).toString().toLowerCase().contains(newText.toLowerCase())) { + // exclude apps that already have settings; the search is just for adding + if (applicationInfo.loadLabel(pm).toString().toLowerCase().contains(newText.toLowerCase()) + && !recyclerAdapter.contains(applicationInfo.packageName)) { matrixCursor.addRow(new Object[]{0, applicationInfo.packageName, applicationInfo.loadLabel(pm)}); } } @@ -121,6 +132,30 @@ public class AutofillActivity extends AppCompatActivity { } }); + searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { + @Override + public boolean onSuggestionSelect(int position) { + return false; + } + + @Override + public boolean onSuggestionClick(int position) { + Cursor cursor = searchView.getSuggestionsAdapter().getCursor(); + String packageName = cursor.getString(1); + String appName = cursor.getString(2); + + // similar to what happens in ViewHolder.onClick but position -1 + DialogFragment df = new AutofillFragment(); + Bundle args = new Bundle(); + args.putString("packageName", packageName); + args.putString("appName", appName); + args.putInt("position", -1); + df.setArguments(args); + df.show(getFragmentManager(), "autofill_dialog"); + return false; + } + }); + setTitle("Autofill Apps"); } diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java new file mode 100644 index 00000000..e9aa86f0 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java @@ -0,0 +1,82 @@ +package com.zeapo.pwdstore.autofill; + +import android.content.DialogInterface; +import android.support.v7.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import com.zeapo.pwdstore.R; + +public class AutofillFragment extends DialogFragment { + + public AutofillFragment() { + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + // this fragment is only created from the settings page (AutofillActivity) + // need to interact with the recyclerAdapter which is a member of activity + final AutofillActivity callingActivity = (AutofillActivity) getActivity(); + LayoutInflater inflater = callingActivity.getLayoutInflater(); + final View view = inflater.inflate(R.layout.fragment_autofill, null); + + builder.setView(view); + + final String packageName = getArguments().getString("packageName"); + String appName = getArguments().getString("appName"); + + builder.setTitle(appName); + SharedPreferences prefs + = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE); + String preference = prefs.getString(packageName, "first"); + switch (preference) { + case "first": + ((RadioButton) view.findViewById(R.id.first)).toggle(); + break; + case "never": + ((RadioButton) view.findViewById(R.id.never)).toggle(); + break; + default: + ((RadioButton) view.findViewById(R.id.match)).toggle(); + ((EditText) view.findViewById(R.id.matched)).setText(preference); + } + + final SharedPreferences.Editor editor = prefs.edit(); + builder.setPositiveButton("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.first: + editor.putString(packageName, "first"); + break; + case R.id.never: + editor.putString(packageName, "never"); + break; + default: + EditText matched = (EditText) view.findViewById(R.id.matched); + String path = matched.getText().toString(); + editor.putString(packageName, path); + } + editor.apply(); + int position = getArguments().getInt("position"); + if (position == -1) { + callingActivity.recyclerAdapter.add(packageName); + } else { + callingActivity.recyclerAdapter.notifyItemChanged(position); + } + } + }); + builder.setNegativeButton("Cancel", null); + return builder.create(); + } +} 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 b59223bd..eff94c51 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java @@ -1,7 +1,11 @@ package com.zeapo.pwdstore.autofill; +import android.app.DialogFragment; +import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.os.Bundle; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; @@ -16,29 +20,40 @@ import java.util.ArrayList; public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecyclerAdapter.ViewHolder> { private ArrayList<ApplicationInfo> apps; private PackageManager pm; + private AutofillActivity activity; public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { public View view; public TextView name; + public TextView secondary; public ImageView icon; + public String packageName; public ViewHolder(View view) { super(view); this.view = view; name = (TextView) view.findViewById(R.id.app_name); + secondary = (TextView) view.findViewById(R.id.secondary_text); icon = (ImageView) view.findViewById(R.id.app_icon); view.setOnClickListener(this); } @Override public void onClick(View v) { - + DialogFragment df = new AutofillFragment(); + Bundle args = new Bundle(); + args.putString("packageName", packageName); + args.putString("appName", name.getText().toString()); + args.putInt("position", getAdapterPosition()); + df.setArguments(args); + df.show(activity.getFragmentManager(), "autofill_dialog"); } } - public AutofillRecyclerAdapter(ArrayList<ApplicationInfo> apps, PackageManager pm) { + public AutofillRecyclerAdapter(ArrayList<ApplicationInfo> apps, PackageManager pm, AutofillActivity activity) { this.apps = apps; this.pm = pm; + this.activity = activity; } @Override @@ -52,11 +67,45 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl public void onBindViewHolder(AutofillRecyclerAdapter.ViewHolder holder, int position) { ApplicationInfo app = apps.get(position); holder.name.setText(pm.getApplicationLabel(app)); + SharedPreferences prefs + = activity.getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE); + String preference = prefs.getString(app.packageName, "first"); + switch (preference) { + case "first": + holder.secondary.setText("Automatically match with password"); + break; + case "never": + holder.secondary.setText("Never autofill"); + break; + default: + holder.secondary.setText("Match with " + preference); + break; + } holder.icon.setImageDrawable(pm.getApplicationIcon(app)); + holder.packageName = app.packageName; } @Override public int getItemCount() { return apps.size(); } + + public boolean contains(String packageName) { + for (ApplicationInfo app : apps) { + if (app.packageName.equals(packageName)) { + return true; + } + } + return false; + } + + public void add(String packageName) { + try { + ApplicationInfo app = pm.getApplicationInfo(packageName, 0); + this.apps.add(app); + notifyItemInserted(apps.size()); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + } } 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 f3108733..dbd018b6 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java @@ -68,9 +68,13 @@ public class AutofillService extends AccessibilityService { && event.getPackageName().equals(packageName) && unlockOK) { decryptAndVerify(); } + + // nothing to do if not password field focus, android version, or field is keychain app if (!event.isPassword() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 || event.getPackageName().equals("org.sufficientlysecure.keychain")) { + // dismiss dialog if WINDOW_STATE_CHANGED unless the keyboard caused it + // there may be other exceptions... if (!(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.getPackageName().toString().contains("com.android.inputmethod")) && dialog != null && dialog.isShowing()) { @@ -78,14 +82,19 @@ public class AutofillService extends AccessibilityService { } return; } + + // if past this point, a new dialog will be created, so dismiss the existing if (dialog != null && dialog.isShowing()) { dialog.dismiss(); } + // ignore the ACTION_FOCUS from decryptAndVerify if (ignoreActionFocus) { ignoreActionFocus = false; return; } + + // get the app name and find a corresponding password info = event.getSource(); PackageManager packageManager = getPackageManager(); ApplicationInfo applicationInfo; @@ -109,7 +118,7 @@ public class AutofillService extends AccessibilityService { decryptAndVerify(); } }); - builder.setNeutralButton("Match", new DialogInterface.OnClickListener() { + builder.setNeutralButton("Settings", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -121,7 +130,7 @@ public class AutofillService extends AccessibilityService { dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); } - dialog.setTitle(items.get(0).getName()); + dialog.setTitle(items.get(0).toString()); dialog.show(); dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); } @@ -202,6 +211,7 @@ public class AutofillService extends AccessibilityService { case OpenPgpApi.RESULT_CODE_SUCCESS: { try { String[] passContent = os.toString("UTF-8").split("\n"); + // if the user focused on something else, take focus back // but this will open another dialog...hack to ignore this ignoreActionFocus = info.performAction(AccessibilityNodeInfo.ACTION_FOCUS); @@ -215,6 +225,7 @@ public class AutofillService extends AccessibilityService { ClipData clip = ClipData.newPlainText("autofill_pm", passContent[0]); clipboard.setPrimaryClip(clip); info.performAction(AccessibilityNodeInfo.ACTION_PASTE); + clip = ClipData.newPlainText("autofill_pm", "MyPasswordIsDaBest!"); clipboard.setPrimaryClip(clip); if (settings.getBoolean("clear_clipboard_20x", false)) { diff --git a/app/src/main/res/layout/autofill_recycler_view.xml b/app/src/main/res/layout/autofill_recycler_view.xml index 98ff779d..6130fd1a 100644 --- a/app/src/main/res/layout/autofill_recycler_view.xml +++ b/app/src/main/res/layout/autofill_recycler_view.xml @@ -2,13 +2,15 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:focusable="true" + android:focusableInTouchMode="true"> <SearchView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/app_search" - android:queryHint="Add an app to configure" + android:queryHint="Add an app to change its autofill setting" android:iconifiedByDefault="false"/> <android.support.v7.widget.RecyclerView diff --git a/app/src/main/res/layout/fragment_autofill.xml b/app/src/main/res/layout/fragment_autofill.xml new file mode 100644 index 00000000..6ad44a0d --- /dev/null +++ b/app/src/main/res/layout/fragment_autofill.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="@dimen/activity_vertical_margin" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin"> + + <RadioGroup + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/autofill_radiogroup" + > + + <RadioButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Automatically match with password" + android:id="@+id/first" + android:layout_gravity="center_vertical" + android:checked="false"/> + + <RadioButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Match with..." + android:id="@+id/match" + android:layout_gravity="center_vertical" + android:checked="false" + android:layout_marginTop="8dp"/> + + <EditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/matched" + android:layout_gravity="center_horizontal" + android:editable="false"/> + + <RadioButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Never autofill" + android:id="@+id/never" + android:layout_gravity="center_vertical" + android:checked="false" + android:layout_marginTop="8dp"/> + + </RadioGroup> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml index 3a8c4fdf..538f0387 100644 --- a/app/src/main/res/xml/preference.xml +++ b/app/src/main/res/xml/preference.xml @@ -74,7 +74,7 @@ <PreferenceCategory android:title="Autofill"> <Preference android:key="autofill_apps" - android:summary="Configure behaviour of autofill for specific apps." + android:summary="Customize autofill settings for specific apps." android:title="Autofill Apps"/> </PreferenceCategory> |