diff options
Diffstat (limited to 'app/src')
12 files changed, 579 insertions, 159 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bd4c3704..fed637d9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,7 +46,7 @@ </activity> <service android:name=".autofill.AutofillService" - android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> 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..0f184bb7 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java @@ -7,16 +7,19 @@ import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; +import com.zeapo.pwdstore.PasswordStore; + // 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_MATCH_WITH = 777; @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 +30,26 @@ public class AutofillActivity extends AppCompatActivity { } catch (IntentSender.SendIntentException e) { Log.e(AutofillService.Constants.TAG, "SendIntentException", e); } + } else if (extras != null && extras.containsKey("matchWith")) { + Intent intent = new Intent(getApplicationContext(), PasswordStore.class); + intent.putExtra("matchWith", true); + startActivityForResult(intent, REQUEST_CODE_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_MATCH_WITH: + if (resultCode == RESULT_OK) { + AutofillService.getInstance().setPickedPassword(data.getStringExtra("path")); + } } } } 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..1ebf4828 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() { } @@ -30,25 +39,62 @@ public class AutofillFragment extends DialogFragment { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); // this fragment is only created from the settings page (AutofillPreferenceActivity) // need to interact with the recyclerAdapter which is a member of activity - final AutofillPreferenceActivity callingActivity = (AutofillPreferenceActivity) getActivity(); + 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 webName 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,128 @@ 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 (packageName != null && !packageName.equals("")) { + editor.remove(packageName); + ((AutofillPreferenceActivity) getActivity()).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 + EditText webURL = (EditText) dialog.findViewById(R.id.webURL); + if (packageName.equals("")) { + webURL.setError("URL cannot be blank"); + return; + } + String oldPackageName = getArguments().getString("packageName", ""); + int position = callingActivity.recyclerAdapter.getPosition(packageName); + if (!oldPackageName.equals(packageName) && position != -1) { + webURL.setError("URL already exists"); + return; + } + } + 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(); + + // if recyclerAdapter has not loaded yet, there is no need to notify + 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..81d0ddde 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,19 +16,19 @@ 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 { - private RecyclerView recyclerView; + RecyclerView recyclerView; AutofillRecyclerAdapter recyclerAdapter; // let fragment have access private RecyclerView.LayoutManager layoutManager; @@ -54,10 +56,18 @@ public class AutofillPreferenceActivity extends AppCompatActivity { 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 +80,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 +109,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 +158,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..39849181 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,7 +88,6 @@ public class AutofillService extends AccessibilityService { settings = PreferenceManager.getDefaultSharedPreferences(this); } - // TODO change search/search results (just use first result) @Override public void onAccessibilityEvent(AccessibilityEvent event) { // if returning to the source app from a successful AutofillActivity @@ -75,8 +96,36 @@ public class AutofillService extends AccessibilityService { bindDecryptAndVerify(); } + // 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().getPackageName().equals("com.android.chrome"))) + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + webViewTitle = searchWebView(getRootInActiveWindow()); + webViewURL = null; + if (webViewTitle != null && getRootInActiveWindow() != null) { + List<AccessibilityNodeInfo> nodes = getRootInActiveWindow() + .findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar"); + 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, android version, or field is keychain app if (!event.isPassword() + || event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 || event.getPackageName().equals("org.sufficientlysecure.keychain")) { dismissDialog(event); @@ -91,6 +140,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 @@ -109,6 +159,8 @@ public class AutofillService extends AccessibilityService { 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 +168,57 @@ 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; + if (webViewTitle == 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(); + + setMatchingPasswords(appName, info.getPackageName().toString()); + } else { + packageName = setMatchingPasswordsWeb(webViewTitle, webViewURL); + + appName = packageName; } - 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); + } - 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,10 +236,11 @@ public class AutofillService extends AccessibilityService { } if (dismiss && dialog != null && dialog.isShowing()) { dialog.dismiss(); + dialog = null; } } - private void getMatchingPassword(String appName, String packageName) { + private void setMatchingPasswords(String appName, String 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); @@ -162,67 +250,139 @@ public class AutofillService extends AccessibilityService { if (!PasswordRepository.isInitialized()) { PasswordRepository.initialize(this); } - items = recursiveFilter(appName, null); + items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), appName); break; case "/never": - items.clear(); + items = new ArrayList<>(); 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))); + getPreferredPasswords(preference); + } + } + + // Return the the matched preference's key, which isn't necessarily equal to + // the URL, if a preference is matched so it can be accessed with Settings. + private String setMatchingPasswordsWeb(String webViewTitle, String webViewURL) { + SharedPreferences prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE); + Map<String, ?> prefsMap = prefs.getAll(); + for (String key : prefsMap.keySet()) { + if (webViewURL.toLowerCase().contains(key.toLowerCase())) { + getPreferredPasswords(prefs.getString(key, "")); + return key; + } + } + + // no user-defined match found, maybe auto match using title, not URL + if (settings.getBoolean("autofill_default", true)) { + if (!PasswordRepository.isInitialized()) { + PasswordRepository.initialize(this); + } + items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), webViewTitle); + } else { + items = new ArrayList<>(); } + return webViewURL; } - 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() { + private void showDialog(final String packageName, final String appName) { + 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); + if (webViewTitle != null) { + intent.putExtra("isWeb", true); + } + startActivity(intent); + } + }); + + if (!items.isEmpty()) { + CharSequence itemNames[] = new CharSequence[items.size()]; + for (int i = 0; i < items.size(); i++) { + itemNames[i] = items.get(i).getName().replace(".gpg", ""); + } + builder.setItems(itemNames, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { + lastWhichItem = which; bindDecryptAndVerify(); } }); - builder.setNeutralButton("Settings", new DialogInterface.OnClickListener() { + } else { + builder.setItems(new CharSequence[]{"Pick a password..."}, 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); + public void onClick(DialogInterface dialog, int which) { + lastWhichItem = which; // always 0 + // TODO option to remember a pick for the future when possible? or option to have this always visible? + 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("matchWith", true); startActivity(intent); } }); - dialog = builder.create(); - dialog.setIcon(R.drawable.ic_launcher); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); - dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); - dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); } - dialog.setTitle(items.get(0).toString()); + dialog = 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 (items.size() > 1) { + height += 33; + } + dialog.getWindow().setLayout((int) (240 * getApplicationContext().getResources().getDisplayMetrics().density) + , (int) (height * getApplicationContext().getResources().getDisplayMetrics().density)); dialog.show(); } @@ -266,13 +426,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 +442,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 +464,7 @@ public class AutofillService extends AccessibilityService { } } } + info.recycle(); } catch (UnsupportedEncodingException e) { Log.e(Constants.TAG, "UnsupportedEncodingException", e); } diff --git a/app/src/main/res/layout/autofill_recycler_view.xml b/app/src/main/res/layout/autofill_recycler_view.xml index 5dc950c2..7991341d 100644 --- a/app/src/main/res/layout/autofill_recycler_view.xml +++ b/app/src/main/res/layout/autofill_recycler_view.xml @@ -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_width="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>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_autofill.xml b/app/src/main/res/layout/fragment_autofill.xml index 7bdf3d84..dd9f8419 100644 --- a/app/src/main/res/layout/fragment_autofill.xml +++ b/app/src/main/res/layout/fragment_autofill.xml @@ -1,12 +1,26 @@ <?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" - android:orientation="vertical" - android:paddingBottom="20dp" - android:paddingLeft="24dp" - android:paddingRight="24dp" - android:paddingTop="20dp"> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingBottom="20dp" + android:paddingLeft="24dp" + 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" @@ -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" @@ -50,7 +72,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:checked="false" - android:text="@string/autofill_apps_never" /> + android:text="@string/autofill_apps_never"/> </RadioGroup> diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 99af0e3f..e668f5d1 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -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>
\ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9cb6ac1b..ba134174 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -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> diff --git a/app/src/main/res/xml/autofill_config.xml b/app/src/main/res/xml/autofill_config.xml index 62f82a5e..09e91174 100644 --- a/app/src/main/res/xml/autofill_config.xml +++ b/app/src/main/res/xml/autofill_config.xml @@ -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" />
\ 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 639fdfe1..f8397bc3 100644 --- a/app/src/main/res/xml/preference.xml +++ b/app/src/main/res/xml/preference.xml @@ -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"> |