Merge pull request #117 from zeapo/autofill

Autofill
This commit is contained in:
wongma7 2015-08-25 13:31:07 -04:00
commit 327945f3b8
24 changed files with 1218 additions and 5 deletions

View file

@ -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>

View file

@ -111,10 +111,14 @@ public class PasswordFragment extends Fragment{
recyclerAdapter.addAll(PasswordRepository.getPasswords(item.getFile(), PasswordRepository.getRepositoryDirectory(activity)));
((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} else {
if (getArguments().getBoolean("matchWith", false)) {
((PasswordStore) getActivity()).matchPasswordWithApp(item);
} else {
((PasswordStore) getActivity()).decryptPassword(item);
}
}
}
public void savePosition(Integer position) {

View file

@ -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();
}
}

View file

@ -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) {

View file

@ -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
}
}
}

View file

@ -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();
}
}
}

View file

@ -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");
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}
}
}

View file

@ -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);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"
/>

View file

@ -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>