diff options
author | wongma7 <wongma7@users.noreply.github.com> | 2015-08-25 13:31:07 -0400 |
---|---|---|
committer | wongma7 <wongma7@users.noreply.github.com> | 2015-08-25 13:31:07 -0400 |
commit | 327945f3b8aa56f77559a25bc9160dae0a0cc6d2 (patch) | |
tree | c92d1450ee4930cf2b91a7b34efa5c97cee826b7 | |
parent | eced1dd314ba651f862396c340492c6f2d9da926 (diff) | |
parent | a73e8625f55e869ba0d74bfa370178c54be3662d (diff) |
Merge pull request #117 from zeapo/autofill
Autofill
24 files changed, 1218 insertions, 5 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2849695f..0789fd33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,9 @@ <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" /> + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> + <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme"> <activity android:name=".PasswordStore" android:label="@string/app_name" @@ -42,6 +45,31 @@ android:value="com.zeapo.pwdstore.PasswordStore" /> </activity> + <service android:name=".autofill.AutofillService" + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> + <intent-filter> + <action android:name="android.accessibilityservice.AccessibilityService" /> + </intent-filter> + <meta-data android:name="android.accessibilityservice" + android:resource="@xml/autofill_config" /> + </service> + + <activity android:name=".autofill.AutofillActivity" + android:parentActivityName=".PasswordStore" + android:documentLaunchMode="intoExisting" + android:excludeFromRecents="true"> + + <meta-data android:name="android.support.PARENT_ACTIVITY" + android:value="com.zeapo.pwdstore.PasswordStore" /> + </activity> + + <activity android:name=".autofill.AutofillPreferenceActivity" + android:parentActivityName=".PasswordStore"> + + <meta-data android:name="android.support.PARENT_ACTIVITY" + android:value="com.zeapo.pwdstore.PasswordStore" /> + </activity> + <activity android:name="net.rdrei.android.dirchooser.DirectoryChooserActivity" /> </application> diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java index 72636043..1daa4f90 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java @@ -112,7 +112,11 @@ public class PasswordFragment extends Fragment{ ((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true); } else { - ((PasswordStore) getActivity()).decryptPassword(item); + if (getArguments().getBoolean("matchWith", false)) { + ((PasswordStore) getActivity()).matchPasswordWithApp(item); + } else { + ((PasswordStore) getActivity()).decryptPassword(item); + } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index e51ef201..39d918c8 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java @@ -289,6 +289,12 @@ public class PasswordStore extends AppCompatActivity { Bundle args = new Bundle(); args.putString("Path", PasswordRepository.getWorkTree().getAbsolutePath()); + // if the activity was started from the autofill settings, the + // intent is to match a clicked pwd with app. pass this to fragment + if (getIntent().getBooleanExtra("matchWith", false)) { + args.putBoolean("matchWith", true); + } + plist.setArguments(args); fragmentTransaction.addToBackStack("passlist"); @@ -531,4 +537,13 @@ public class PasswordStore extends AppCompatActivity { }) .show(); } + + public void matchPasswordWithApp(PasswordItem item) { + String path = item.getFile().getAbsolutePath(); + path = path.replace(PasswordRepository.getWorkTree() + "/", "").replace(".gpg", ""); + Intent data = new Intent(); + data.putExtra("path", path); + setResult(RESULT_OK, data); + finish(); + } } diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java index 9a5c83d4..8f8e8d66 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java @@ -1,22 +1,29 @@ package com.zeapo.pwdstore; -import android.app.AlertDialog; +import android.accessibilityservice.AccessibilityServiceInfo; import android.app.DialogFragment; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; +import android.provider.Settings; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.text.SpannableStringBuilder; import android.view.MenuItem; +import android.view.accessibility.AccessibilityManager; import android.widget.Toast; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; +import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity; import com.zeapo.pwdstore.crypto.PgpHandler; import com.zeapo.pwdstore.git.GitActivity; import com.zeapo.pwdstore.utils.PasswordRepository; @@ -32,6 +39,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; +import java.util.List; import java.util.Set; public class UserPreference extends AppCompatActivity { @@ -182,6 +190,40 @@ public class UserPreference extends AppCompatActivity { findPreference("pref_select_external").setOnPreferenceChangeListener(resetRepo); findPreference("git_external").setOnPreferenceChangeListener(resetRepo); + + findPreference("autofill_apps").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(callingActivity, AutofillPreferenceActivity.class); + startActivity(intent); + return true; + } + }); + + findPreference("autofill_enable").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new AlertDialog.Builder(callingActivity). + setTitle(R.string.pref_autofill_enable_title). + setView(R.layout.autofill_instructions). + setPositiveButton(R.string.dialog_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + startActivity(intent); + } + }). + setNegativeButton(R.string.dialog_cancel, null). + setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + ((CheckBoxPreference) findPreference("autofill_enable")) + .setChecked(((UserPreference) getActivity()).isServiceEnabled()); + } + }).show(); + return true; + } + }); } @Override @@ -189,6 +231,10 @@ public class UserPreference extends AppCompatActivity { super.onStart(); final SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); findPreference("ssh_see_key").setEnabled(sharedPreferences.getBoolean("use_generated_key", false)); + + // see if the autofill service is enabled and check the preference accordingly + ((CheckBoxPreference) findPreference("autofill_enable")) + .setChecked(((UserPreference) getActivity()).isServiceEnabled()); } } @@ -268,6 +314,21 @@ public class UserPreference extends AppCompatActivity { sshKey.close(); } + // Returns whether the autofill service is enabled + private boolean isServiceEnabled() { + AccessibilityManager am = (AccessibilityManager) this + .getSystemService(Context.ACCESSIBILITY_SERVICE); + List<AccessibilityServiceInfo> runningServices = am + .getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC); + for (AccessibilityServiceInfo service : runningServices) { + if ("com.zeapo.pwdstore/.autofill.AutofillService".equals(service.getId())) { + return true; + } + } + return false; + } + + protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java new file mode 100644 index 00000000..d8e50ebd --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java @@ -0,0 +1,40 @@ +package com.zeapo.pwdstore.autofill; + +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; + +// blank activity started by service for calling startIntentSenderForResult +public class AutofillActivity extends AppCompatActivity { + public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle extras = getIntent().getExtras(); + + if (extras != null) { + try { + PendingIntent pi = extras.getParcelable("pending_intent"); + if (pi == null) { + return; + } + startIntentSenderForResult(pi.getIntentSender() + , REQUEST_CODE_DECRYPT_AND_VERIFY, null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.e(AutofillService.Constants.TAG, "SendIntentException", e); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + finish(); // go back to the password field app + if (resultCode == RESULT_OK) { + AutofillService.setUnlockOK(); // report the result to service + } + } +} 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..e708c19d --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java @@ -0,0 +1,121 @@ +package com.zeapo.pwdstore.autofill; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +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.PasswordStore; +import com.zeapo.pwdstore.R; + +public class AutofillFragment extends DialogFragment { + private static final int MATCH_WITH = 777; + + public AutofillFragment() { + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + 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(); + 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); + try { + // since we can't (easily?) pass the drawable as an argument + builder.setIcon(callingActivity.getPackageManager().getApplicationIcon(packageName)); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + SharedPreferences prefs + = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE); + String preference = prefs.getString(packageName, ""); + switch (preference) { + case "": + ((RadioButton) view.findViewById(R.id.use_default)).toggle(); + break; + 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); + } + + View.OnClickListener matchPassword = new View.OnClickListener() { + @Override + public void onClick(View v) { + ((RadioButton) view.findViewById(R.id.match)).toggle(); + Intent intent = new Intent(getActivity(), PasswordStore.class); + intent.putExtra("matchWith", true); + startActivityForResult(intent, MATCH_WITH); + } + }; + view.findViewById(R.id.match).setOnClickListener(matchPassword); + view.findViewById(R.id.matched).setOnClickListener(matchPassword); + + final SharedPreferences.Editor editor = prefs.edit(); + 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(); + int position = getArguments().getInt("position"); + callingActivity.recyclerAdapter.notifyItemChanged(position); + + if (getArguments().getBoolean("finish")) { + callingActivity.finish(); + } + } + }); + builder.setNegativeButton(R.string.dialog_cancel, null); + return builder.create(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + ((EditText) getDialog().findViewById(R.id.matched)).setText(data.getStringExtra("path")); + } else { + ((RadioButton) getDialog().findViewById(R.id.use_default)).toggle(); + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java new file mode 100644 index 00000000..16867bcd --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java @@ -0,0 +1,146 @@ +package com.zeapo.pwdstore.autofill; + +import android.app.DialogFragment; +import android.content.Intent; +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.v4.app.NavUtils; +import android.support.v4.app.TaskStackBuilder; +import android.support.v4.view.MenuItemCompat; +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.List; + +public class AutofillPreferenceActivity extends AppCompatActivity { + + private RecyclerView recyclerView; + AutofillRecyclerAdapter recyclerAdapter; // let fragment have access + private RecyclerView.LayoutManager layoutManager; + + private PackageManager pm; + + private boolean recreate; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.autofill_recycler_view); + recyclerView = (RecyclerView) findViewById(R.id.autofill_recycler); + + layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST)); + + pm = getPackageManager(); + + new populateTask().execute(); + + setTitle("Autofill Apps"); + } + + private class populateTask extends AsyncTask<Void, Void, Void> { + @Override + protected void onPreExecute() { + findViewById(R.id.progress_bar).setVisibility(View.VISIBLE); + } + + @Override + protected Void doInBackground(Void... params) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + List<ResolveInfo> allApps = pm.queryIntentActivities(intent, 0); + + 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())); + } + + recyclerAdapter = new AutofillRecyclerAdapter(allApps, iconMap, pm, AutofillPreferenceActivity.this); + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + findViewById(R.id.progress_bar).setVisibility(View.GONE); + + recyclerView.setAdapter(recyclerAdapter); + + recreate = false; + Bundle extras = getIntent().getExtras(); + if (extras != null) { + recreate = true; + recyclerView.scrollToPosition(recyclerAdapter.getPosition(extras.getString("packageName"))); + showDialog(extras.getString("packageName"), extras.getString("appName")); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.autofill_preference, menu); + MenuItem searchItem = menu.findItem(R.id.action_search); + SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + return false; + } + + @Override + public boolean onQueryTextChange(String s) { + if (recyclerAdapter != null) { + recyclerAdapter.filter(s); + } + return true; + } + }); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + // in service, we CLEAR_TASK. then we set the recreate flag. + // something of a hack, but w/o CLEAR_TASK, behaviour was unpredictable + case android.R.id.home: + Intent upIntent = NavUtils.getParentActivityIntent(this); + if (recreate) { + TaskStackBuilder.create(this) + .addNextIntentWithParentStack(upIntent) + .startActivities(); + } else { + NavUtils.navigateUpTo(this, upIntent); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + public void showDialog(String packageName, String appName) { + DialogFragment df = new AutofillFragment(); + Bundle args = new Bundle(); + args.putString("packageName", packageName); + args.putString("appName", appName); + args.putInt("position", recyclerAdapter.getPosition(packageName)); + 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 new file mode 100644 index 00000000..a131598a --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java @@ -0,0 +1,148 @@ +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; +import android.widget.ImageView; +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 PackageManager pm; + private AutofillPreferenceActivity 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) { + activity.showDialog(packageName, name.getText().toString()); + } + + } + + public AutofillRecyclerAdapter(List<ResolveInfo> allApps, HashMap<String, Pair<Drawable, String>> iconMap + , final PackageManager pm, AutofillPreferenceActivity activity) { + SortedList.Callback<ResolveInfo> callback = new SortedListAdapterCallback<ResolveInfo>(this) { + @Override + public int compare(ResolveInfo o1, ResolveInfo o2) { + return o1.loadLabel(pm).toString().toLowerCase().compareTo(o2.loadLabel(pm).toString().toLowerCase()); + } + + @Override + public boolean areContentsTheSame(ResolveInfo oldItem, ResolveInfo newItem) { + return oldItem.loadLabel(pm).toString().equals(newItem.loadLabel(pm).toString()); + } + + @Override + public boolean areItemsTheSame(ResolveInfo item1, ResolveInfo item2) { + return item1.loadLabel(pm).toString().equals(item2.loadLabel(pm).toString()); + } + }; + this.apps = new SortedList<>(ResolveInfo.class, callback); + this.apps.addAll(allApps); + this.allApps = new ArrayList<>(allApps); + this.iconMap = new HashMap<>(iconMap); + this.pm = pm; + this.activity = activity; + } + + @Override + public AutofillRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.autofill_row_layout, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(AutofillRecyclerAdapter.ViewHolder holder, int position) { + ResolveInfo app = apps.get(position); + holder.packageName = app.activityInfo.packageName; + + holder.icon.setImageDrawable(iconMap.get(holder.packageName).first); + holder.name.setText(iconMap.get(holder.packageName).second); + + holder.secondary.setVisibility(View.VISIBLE); + holder.view.setBackgroundResource(R.color.grey_white_1000); + + SharedPreferences prefs + = activity.getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE); + String preference = prefs.getString(holder.packageName, ""); + switch (preference) { + case "": + holder.secondary.setVisibility(View.GONE); + // "android:windowBackground" + holder.view.setBackgroundResource(R.color.indigo_50); + break; + case "/first": + holder.secondary.setText(R.string.autofill_apps_first); + break; + case "/never": + holder.secondary.setText(R.string.autofill_apps_never); + break; + default: + holder.secondary.setText("Match with " + preference); + break; + } + } + + @Override + public int getItemCount() { + 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 void filter(String s) { + if (s.isEmpty()) { + apps.addAll(allApps); + return; + } + apps.beginBatchedUpdates(); + for (ResolveInfo app : allApps) { + if (app.loadLabel(pm).toString().toLowerCase().contains(s.toLowerCase())) { + apps.add(app); + } else { + apps.remove(app); + } + } + apps.endBatchedUpdates(); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java new file mode 100644 index 00000000..ea09c863 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java @@ -0,0 +1,274 @@ +package com.zeapo.pwdstore.autofill; + +import android.accessibilityservice.AccessibilityService; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +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; +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +public class AutofillService extends AccessibilityService { + 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 AlertDialog dialog; + private AccessibilityWindowInfo window; + private static boolean unlockOK = false; // if openkeychain user interaction was successful + private CharSequence packageName; + private boolean ignoreActionFocus = false; + + public final class Constants { + public static final String TAG = "Keychain"; + } + + public static void setUnlockOK() { unlockOK = true; } + + @Override + protected void onServiceConnected() { + super.onServiceConnected(); + serviceConnection = new OpenPgpServiceConnection(AutofillService.this, "org.sufficientlysecure.keychain"); + serviceConnection.bindToService(); + 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 + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + && 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")) { + // the default keyboard showing/hiding is a window state changed event + // on Android 5+ we can use getWindows() to determine when the original window is not visible + // on Android 4.3 we have to use window state changed events and filter out the keyboard ones + // there may be other exceptions... + boolean dismiss; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dismiss = !getWindows().contains(window); + } else { + dismiss = !(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + && event.getPackageName().toString().contains("inputmethod")); + } + if (dismiss && dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + return; + } + + if (dialog != null && dialog.isShowing()) { + // if the view was clicked, the click event follows the focus event + // since the focus event was already handled, ignore click event + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) { + return; + } + // if past this point, a new dialog will be created, so dismiss the existing + dialog.dismiss(); + } + + // ignore the ACTION_FOCUS from decryptAndVerify otherwise dialog will appear after Fill + if (ignoreActionFocus) { + ignoreActionFocus = false; + return; + } + + info = event.getSource(); + + // save the dialog's corresponding window so we can use getWindows() above to check whether dismiss + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + 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; + } + final String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString(); + + // 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(event.getPackageName().toString(), defValue); + switch (preference) { + case "/first": + if (!PasswordRepository.isInitialized()) { + PasswordRepository.initialize(this); + } + items = recursiveFilter(appName, null); + break; + case "/never": + 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))); + } + if (items.isEmpty()) { + return; + } + + 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) { + decryptAndVerify(); + } + }); + 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", info.getPackageName()); + intent.putExtra("appName", appName); + 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.show(); + } + + 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())); + } + if (item.toString().toLowerCase().contains(filter.toLowerCase())) { + items.add(item); + } + } + return items; + } + + @Override + public void onInterrupt() { + + } + + public void decryptAndVerify() { + unlockOK = false; + packageName = info.getPackageName(); + Intent data = new Intent(); + data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + InputStream is = null; + try { + is = FileUtils.openInputStream(items.get(0).getFile()); + } catch (IOException e) { + e.printStackTrace(); + } + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + OpenPgpApi api = new OpenPgpApi(AutofillService.this, serviceConnection.getService()); + Intent result = api.executeApi(data, is, os); + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + 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); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Bundle args = new Bundle(); + args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, + passContent[0]); + info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); + } else { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + 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)) { + for (int i = 0; i < 19; i++) { + clip = ClipData.newPlainText(String.valueOf(i), String.valueOf(i)); + clipboard.setPrimaryClip(clip); + } + } + } + } catch (UnsupportedEncodingException e) { + Log.e(Constants.TAG, "UnsupportedEncodingException", e); + } + break; + } + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: { + Log.i("PgpHandler", "RESULT_CODE_USER_INTERACTION_REQUIRED"); + PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + // need to start a blank activity to call startIntentSenderForResult + Intent intent = new Intent(AutofillService.this, AutofillActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra("pending_intent", pi); + startActivity(intent); + break; + } + case OpenPgpApi.RESULT_CODE_ERROR: { + OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); + Toast.makeText(AutofillService.this, + "Error from OpenKeyChain : " + error.getMessage(), + Toast.LENGTH_LONG).show(); + Log.e(Constants.TAG, "onError getErrorId:" + error.getErrorId()); + Log.e(Constants.TAG, "onError getMessage:" + error.getMessage()); + break; + } + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java b/app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java new file mode 100644 index 00000000..bd5ec9de --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java @@ -0,0 +1,105 @@ +package com.zeapo.pwdstore.autofill; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + private static final int[] ATTRS = new int[]{ + android.R.attr.listDivider + }; + + public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; + + public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; + + private Drawable mDivider; + + private int mOrientation; + + public DividerItemDecoration(Context context, int orientation) { + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + setOrientation(orientation); + } + + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { + throw new IllegalArgumentException("invalid orientation"); + } + mOrientation = orientation; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + public void drawVertical(Canvas c, RecyclerView parent) { + final int left = parent.getPaddingLeft(); + final int right = parent.getWidth() - parent.getPaddingRight(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int top = child.getBottom() + params.bottomMargin; + final int bottom = top + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + public void drawHorizontal(Canvas c, RecyclerView parent) { + final int top = parent.getPaddingTop(); + final int bottom = parent.getHeight() - parent.getPaddingBottom(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int left = child.getRight() + params.rightMargin; + final int right = left + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +} + diff --git a/app/src/main/res/drawable/autofill_ins_1.png b/app/src/main/res/drawable/autofill_ins_1.png Binary files differnew file mode 100644 index 00000000..56052303 --- /dev/null +++ b/app/src/main/res/drawable/autofill_ins_1.png diff --git a/app/src/main/res/drawable/autofill_ins_2.png b/app/src/main/res/drawable/autofill_ins_2.png Binary files differnew file mode 100644 index 00000000..36767847 --- /dev/null +++ b/app/src/main/res/drawable/autofill_ins_2.png diff --git a/app/src/main/res/drawable/autofill_ins_3.png b/app/src/main/res/drawable/autofill_ins_3.png Binary files differnew file mode 100644 index 00000000..8e423209 --- /dev/null +++ b/app/src/main/res/drawable/autofill_ins_3.png diff --git a/app/src/main/res/drawable/autofill_row_background.xml b/app/src/main/res/drawable/autofill_row_background.xml new file mode 100644 index 00000000..05e887ca --- /dev/null +++ b/app/src/main/res/drawable/autofill_row_background.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_selected="true" android:drawable="@color/blue_grey_200" /> + <item android:drawable="@color/grey_white_1000" /> +</selector>
\ No newline at end of file diff --git a/app/src/main/res/layout/app_list_item.xml b/app/src/main/res/layout/app_list_item.xml new file mode 100644 index 00000000..1a6c6e9b --- /dev/null +++ b/app/src/main/res/layout/app_list_item.xml @@ -0,0 +1,24 @@ +<?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="40dp" + android:orientation="horizontal" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:gravity="center_vertical"> + + <ImageView + android:id="@android:id/icon1" + android:layout_width="24dp" + android:layout_height="24dp"/> + + <TextView android:id="@android:id/text1" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:layout_marginLeft="8dp"/> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/autofill_instructions.xml b/app/src/main/res/layout/autofill_instructions.xml new file mode 100644 index 00000000..82ad16d1 --- /dev/null +++ b/app/src/main/res/layout/autofill_instructions.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="match_parent"> + +<LinearLayout android:orientation="vertical" + android:layout_height="fill_parent" + android:layout_width="fill_parent" + android:paddingLeft="24dp" + android:paddingRight="24dp" + android:paddingTop="20dp" + android:paddingBottom="20dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="@dimen/abc_text_size_menu_material" + android:text="@string/pref_autofill_enable_msg" + android:id="@+id/textView"/> + + <ImageView + android:layout_width="match_parent" + android:id="@+id/imageView" + android:src="@drawable/autofill_ins_1" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:layout_height="wrap_content" + android:adjustViewBounds="true"/> + + <ImageView + android:layout_width="match_parent" + android:id="@+id/imageView2" + android:src="@drawable/autofill_ins_2" + android:layout_marginBottom="8dp" + android:layout_height="wrap_content" + android:adjustViewBounds="true"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="@dimen/abc_text_size_menu_material" + android:text="@string/pref_autofill_enable_msg2" + android:id="@+id/textView3"/> + + <ImageView + android:layout_width="match_parent" + android:layout_height="114dp" + android:id="@+id/imageView3" + android:src="@drawable/autofill_ins_3" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="@dimen/abc_text_size_menu_material" + android:text="@string/pref_autofill_enable_msg3" + android:id="@+id/textView4"/> + + +</LinearLayout> +</ScrollView>
\ No newline at end of file diff --git a/app/src/main/res/layout/autofill_recycler_view.xml b/app/src/main/res/layout/autofill_recycler_view.xml new file mode 100644 index 00000000..4b4bcf26 --- /dev/null +++ b/app/src/main/res/layout/autofill_recycler_view.xml @@ -0,0 +1,20 @@ +<?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.support.v7.widget.RecyclerView + android:id="@+id/autofill_recycler" + android:scrollbars="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + + <ProgressBar + style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/progress_bar" + android:indeterminate="true" + android:layout_centerInParent="true" + android:visibility="gone"/> +</RelativeLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/autofill_row_layout.xml b/app/src/main/res/layout/autofill_row_layout.xml new file mode 100644 index 00000000..6e51fab3 --- /dev/null +++ b/app/src/main/res/layout/autofill_row_layout.xml @@ -0,0 +1,46 @@ +<?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="64dp" + android:background="@drawable/autofill_row_background"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?android:attr/selectableItemBackground" + android:orientation="horizontal" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:gravity="center_vertical"> + + <ImageView + android:layout_width="48dp" + android:layout_height="48dp" + android:id="@+id/app_icon" + /> + + <LinearLayout + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:layout_marginLeft="8dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/app_name"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:id="@+id/secondary_text" + android:textColor="@color/grey_600"/> + </LinearLayout> + + </LinearLayout> + + +</LinearLayout>
\ 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 new file mode 100644 index 00000000..a5b1ac90 --- /dev/null +++ b/app/src/main/res/layout/fragment_autofill.xml @@ -0,0 +1,59 @@ +<?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:paddingLeft="24dp" + android:paddingRight="24dp" + android:paddingTop="20dp" + android:paddingBottom="20dp"> + + <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="@string/autofill_apps_default" + android:id="@+id/use_default" + android:layout_gravity="center_vertical" + android:checked="false"/> + + <RadioButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/autofill_apps_first" + 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="@string/autofill_apps_match_ellipsis" + android:id="@+id/match" + android:layout_gravity="center_vertical" + android:checked="false" + /> + + <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="@string/autofill_apps_never" + android:id="@+id/never" + android:layout_gravity="center_vertical" + android:checked="false" + /> + + </RadioGroup> + +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/menu/autofill_preference.xml b/app/src/main/res/menu/autofill_preference.xml new file mode 100644 index 00000000..77ce95f4 --- /dev/null +++ b/app/src/main/res/menu/autofill_preference.xml @@ -0,0 +1,12 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:pwstore="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + tools:context=".pwdstore.autofill.AutofillPreferenceActivity"> + <item + android:id="@+id/action_search" + android:icon="@drawable/ic_action_search" + android:title="@string/action_search" + pwstore:actionViewClass="android.support.v7.widget.SearchView" + pwstore:showAsAction="ifRoom|collapseActionView"/> + +</menu>
\ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 309ff2d6..8d97f2ef 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -116,8 +116,8 @@ <string name="ssh_key_error_dialog_text">Zpráva : \n</string> <string name="pref_recursive_filter">Rekurzivní filtrování</string> <string name="pref_recursive_filter_hint">Rekurzivní hledání hesel v aktuálním adresáři.</string> + <string name="pref_clear_clipboard_title">Zaplnit schránku 20krát</string> <string name="pref_clear_clipboard_hint">Uložit dvacet náhodných textů do schránky namísto pouze jednoho. Užitečné pro telefony Samsug, které nabízejí funkci historie schránky.</string> - <string name="pref_clear_clipboard">Zaplnit schránku 20krát</string> <!-- pwgen fragment --> <string name="pwgen_generate">Generovat</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a3d4061..4047691f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -117,8 +117,16 @@ <string name="ssh_key_error_dialog_text">Message : \n</string> <string name="pref_recursive_filter">Recursive filtering</string> <string name="pref_recursive_filter_hint">Recursively find passwords of the current directory.</string> + <string name="pref_autofill_enable_title">Enable autofill</string> + <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_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_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> - <string name="pref_clear_clipboard">Clear clipboard 20 times</string> <!-- pwgen fragment --> <string name="pwgen_generate">Generate</string> @@ -154,4 +162,11 @@ <string name="pwd_generate_button">Generate</string> <string name="category_string">"Category: "</string> + <!-- Autofill --> + <string name="autofill_description">Autofills password fields in apps. Only works for Android versions 4.3 and up. Does not rely on the clipboard for Android versions 5.0 and up.</string> + <string name="autofill_fill">Fill</string> + <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_never">Never match</string> </resources> diff --git a/app/src/main/res/xml/autofill_config.xml b/app/src/main/res/xml/autofill_config.xml new file mode 100644 index 00000000..a604fc07 --- /dev/null +++ b/app/src/main/res/xml/autofill_config.xml @@ -0,0 +1,8 @@ +<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" + android:description="@string/autofill_description" + android:accessibilityEventTypes="typeViewFocused|typeViewClicked|typeWindowStateChanged" + android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows" + android:accessibilityFeedbackType="feedbackGeneric" + android:notificationTimeout="100" + android:canRetrieveWindowContent="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 e4071c96..639fdfe1 100644 --- a/app/src/main/res/xml/preference.xml +++ b/app/src/main/res/xml/preference.xml @@ -70,11 +70,30 @@ android:summary="@string/pref_recursive_filter_hint" android:title="@string/pref_recursive_filter" /> </PreferenceCategory> + + <PreferenceCategory android:title="Autofill"> + <CheckBoxPreference + android:defaultValue="true" + android:key="autofill_enable" + android:title="@string/pref_autofill_enable_title"/> + <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" + android:defaultValue="true" + android:key="autofill_default" + android:summary="@string/pref_autofill_default_hint" + android:title="@string/pref_autofill_default_title"/> + </PreferenceCategory> + <PreferenceCategory android:title="Misc"> <CheckBoxPreference android:defaultValue="false" android:key="clear_clipboard_20x" android:summary="@string/pref_clear_clipboard_hint" - android:title="@string/pref_clear_clipboard" /> + android:title="@string/pref_clear_clipboard_title" /> </PreferenceCategory> </PreferenceScreen>
\ No newline at end of file |