Merge pull request #146 from zeapo/webview
Autofill: multiple associations per app & WebView/Chrome
This commit is contained in:
commit
2064c2eaac
12 changed files with 661 additions and 171 deletions
|
@ -46,7 +46,7 @@
|
|||
</activity>
|
||||
|
||||
<service android:name=".autofill.AutofillService"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
|
|
|
@ -1,22 +1,34 @@
|
|||
package com.zeapo.pwdstore.autofill;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentSender;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
|
||||
import com.zeapo.pwdstore.PasswordStore;
|
||||
|
||||
import org.eclipse.jgit.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
// blank activity started by service for calling startIntentSenderForResult
|
||||
public class AutofillActivity extends AppCompatActivity {
|
||||
public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913;
|
||||
public static final int REQUEST_CODE_PICK = 777;
|
||||
public static final int REQUEST_CODE_PICK_MATCH_WITH = 778;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle extras = getIntent().getExtras();
|
||||
|
||||
if (extras != null) {
|
||||
if (extras != null && extras.containsKey("pending_intent")) {
|
||||
try {
|
||||
PendingIntent pi = extras.getParcelable("pending_intent");
|
||||
if (pi == null) {
|
||||
|
@ -27,14 +39,65 @@ public class AutofillActivity extends AppCompatActivity {
|
|||
} catch (IntentSender.SendIntentException e) {
|
||||
Log.e(AutofillService.Constants.TAG, "SendIntentException", e);
|
||||
}
|
||||
} else if (extras != null && extras.containsKey("pick")) {
|
||||
Intent intent = new Intent(getApplicationContext(), PasswordStore.class);
|
||||
intent.putExtra("matchWith", true);
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK);
|
||||
} else if (extras != null && extras.containsKey("pickMatchWith")) {
|
||||
Intent intent = new Intent(getApplicationContext(), PasswordStore.class);
|
||||
intent.putExtra("matchWith", true);
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_MATCH_WITH);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
finish(); // go back to the password field app
|
||||
if (resultCode == RESULT_OK) {
|
||||
AutofillService.setResultData(data); // report the result to service
|
||||
switch (requestCode) {
|
||||
case REQUEST_CODE_DECRYPT_AND_VERIFY:
|
||||
if (resultCode == RESULT_OK) {
|
||||
AutofillService.getInstance().setResultData(data); // report the result to service
|
||||
}
|
||||
break;
|
||||
case REQUEST_CODE_PICK:
|
||||
if (resultCode == RESULT_OK) {
|
||||
AutofillService.getInstance().setPickedPassword(data.getStringExtra("path"));
|
||||
}
|
||||
break;
|
||||
case REQUEST_CODE_PICK_MATCH_WITH:
|
||||
if (resultCode == RESULT_OK) {
|
||||
// need to not only decrypt the picked password, but also
|
||||
// update the "match with" preference
|
||||
Bundle extras = getIntent().getExtras();
|
||||
String packageName = extras.getString("packageName");
|
||||
boolean isWeb = extras.getBoolean("isWeb");
|
||||
|
||||
String path = data.getStringExtra("path");
|
||||
AutofillService.getInstance().setPickedPassword(data.getStringExtra("path"));
|
||||
|
||||
SharedPreferences prefs;
|
||||
if (!isWeb) {
|
||||
prefs = getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
||||
} else {
|
||||
prefs = getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
||||
}
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
String preference = prefs.getString(packageName, "");
|
||||
switch (preference) {
|
||||
case "":
|
||||
case "/first":
|
||||
case "/never":
|
||||
editor.putString(packageName, path);
|
||||
break;
|
||||
default:
|
||||
List<String> matches = new ArrayList<>(Arrays.asList(preference.trim().split("\n")));
|
||||
matches.add(path);
|
||||
String paths = StringUtils.join(matches, "\n");
|
||||
editor.putString(packageName, paths);
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.app.Activity;
|
|||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.DialogFragment;
|
||||
|
@ -12,15 +13,23 @@ import android.content.SharedPreferences;
|
|||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.zeapo.pwdstore.PasswordStore;
|
||||
import com.zeapo.pwdstore.R;
|
||||
|
||||
public class AutofillFragment extends DialogFragment {
|
||||
private static final int MATCH_WITH = 777;
|
||||
private ArrayAdapter<String> adapter;
|
||||
private boolean isWeb;
|
||||
|
||||
public AutofillFragment() {
|
||||
}
|
||||
|
@ -32,23 +41,60 @@ public class AutofillFragment extends DialogFragment {
|
|||
// need to interact with the recyclerAdapter which is a member of activity
|
||||
final AutofillPreferenceActivity callingActivity = (AutofillPreferenceActivity) getActivity();
|
||||
LayoutInflater inflater = callingActivity.getLayoutInflater();
|
||||
|
||||
final View view = inflater.inflate(R.layout.fragment_autofill, null);
|
||||
|
||||
builder.setView(view);
|
||||
|
||||
final String packageName = getArguments().getString("packageName");
|
||||
String appName = getArguments().getString("appName");
|
||||
final String appName = getArguments().getString("appName");
|
||||
isWeb = getArguments().getBoolean("isWeb");
|
||||
|
||||
builder.setTitle(appName);
|
||||
// set the dialog icon and title or webURL editText
|
||||
String iconPackageName;
|
||||
if (!isWeb) {
|
||||
iconPackageName = packageName;
|
||||
builder.setTitle(appName);
|
||||
view.findViewById(R.id.webURL).setVisibility(View.GONE);
|
||||
} else {
|
||||
iconPackageName = "com.android.browser";
|
||||
builder.setTitle("Website");
|
||||
((EditText) view.findViewById(R.id.webURL)).setText(packageName);
|
||||
}
|
||||
try {
|
||||
// since we can't (easily?) pass the drawable as an argument
|
||||
builder.setIcon(callingActivity.getPackageManager().getApplicationIcon(packageName));
|
||||
builder.setIcon(callingActivity.getPackageManager().getApplicationIcon(iconPackageName));
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
SharedPreferences prefs
|
||||
= getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
||||
// set up the listview now for items added by button/from preferences
|
||||
adapter = new ArrayAdapter<String>(getActivity().getApplicationContext()
|
||||
, android.R.layout.simple_list_item_1, android.R.id.text1) {
|
||||
// set text color to black because default is white...
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
TextView textView = (TextView) super.getView(position, convertView, parent);
|
||||
textView.setTextColor(ContextCompat.getColor(getContext(), R.color.grey_black_1000));
|
||||
return textView;
|
||||
}
|
||||
};
|
||||
((ListView) view.findViewById(R.id.matched)).setAdapter(adapter);
|
||||
// delete items by clicking them
|
||||
((ListView) view.findViewById(R.id.matched)).setOnItemClickListener(
|
||||
new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
adapter.remove(adapter.getItem(position));
|
||||
}
|
||||
});
|
||||
|
||||
// set the existing preference, if any
|
||||
SharedPreferences prefs;
|
||||
if (!isWeb) {
|
||||
prefs = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
||||
} else {
|
||||
prefs = getActivity().getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
||||
}
|
||||
String preference = prefs.getString(packageName, "");
|
||||
switch (preference) {
|
||||
case "":
|
||||
|
@ -62,9 +108,11 @@ public class AutofillFragment extends DialogFragment {
|
|||
break;
|
||||
default:
|
||||
((RadioButton) view.findViewById(R.id.match)).toggle();
|
||||
((EditText) view.findViewById(R.id.matched)).setText(preference);
|
||||
// trim to remove the last blank element
|
||||
adapter.addAll(preference.trim().split("\n"));
|
||||
}
|
||||
|
||||
// add items with the + button
|
||||
View.OnClickListener matchPassword = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
@ -74,48 +122,130 @@ public class AutofillFragment extends DialogFragment {
|
|||
startActivityForResult(intent, MATCH_WITH);
|
||||
}
|
||||
};
|
||||
view.findViewById(R.id.match).setOnClickListener(matchPassword);
|
||||
view.findViewById(R.id.matched).setOnClickListener(matchPassword);
|
||||
view.findViewById(R.id.matchButton).setOnClickListener(matchPassword);
|
||||
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
// write to preferences when OK clicked
|
||||
builder.setPositiveButton(R.string.dialog_ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.autofill_radiogroup);
|
||||
switch (radioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.use_default:
|
||||
editor.remove(packageName);
|
||||
break;
|
||||
case R.id.first:
|
||||
editor.putString(packageName, "/first");
|
||||
break;
|
||||
case R.id.never:
|
||||
editor.putString(packageName, "/never");
|
||||
break;
|
||||
default:
|
||||
EditText matched = (EditText) view.findViewById(R.id.matched);
|
||||
String path = matched.getText().toString();
|
||||
editor.putString(packageName, path);
|
||||
}
|
||||
editor.apply();
|
||||
|
||||
// if recyclerAdapter has not loaded yet, there is no need to notifyItemChanged
|
||||
if (callingActivity.recyclerAdapter != null) {
|
||||
int position = callingActivity.recyclerAdapter.getPosition(packageName);
|
||||
callingActivity.recyclerAdapter.notifyItemChanged(position);
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.dialog_cancel, null);
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
if (isWeb) {
|
||||
builder.setNeutralButton(R.string.autofill_apps_delete, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (callingActivity.recyclerAdapter != null
|
||||
&& packageName != null && !packageName.equals("")) {
|
||||
editor.remove(packageName);
|
||||
callingActivity.recyclerAdapter.removeWebsite(packageName);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
// need to the onClick here for buttons to dismiss dialog only when wanted
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
AlertDialog ad = (AlertDialog) getDialog();
|
||||
if(ad != null) {
|
||||
Button positiveButton = ad.getButton(Dialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
AutofillPreferenceActivity callingActivity = (AutofillPreferenceActivity) getActivity();
|
||||
Dialog dialog = getDialog();
|
||||
|
||||
SharedPreferences prefs;
|
||||
if (!isWeb) {
|
||||
prefs = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
||||
} else {
|
||||
prefs = getActivity().getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
||||
}
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
|
||||
String packageName = getArguments().getString("packageName", "");
|
||||
if (isWeb) {
|
||||
packageName = ((EditText) dialog.findViewById(R.id.webURL)).getText().toString();
|
||||
|
||||
// handle some errors and don't dismiss the dialog
|
||||
EditText webURL = (EditText) dialog.findViewById(R.id.webURL);
|
||||
if (packageName.equals("")) {
|
||||
webURL.setError("URL cannot be blank");
|
||||
return;
|
||||
}
|
||||
String oldPackageName = getArguments().getString("packageName", "");
|
||||
if (!oldPackageName.equals(packageName) && prefs.getAll().containsKey(packageName)) {
|
||||
webURL.setError("URL already exists");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// write to preferences accordingly
|
||||
RadioGroup radioGroup = (RadioGroup) dialog.findViewById(R.id.autofill_radiogroup);
|
||||
switch (radioGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.use_default:
|
||||
if (!isWeb) {
|
||||
editor.remove(packageName);
|
||||
} else {
|
||||
editor.putString(packageName, "");
|
||||
}
|
||||
break;
|
||||
case R.id.first:
|
||||
editor.putString(packageName, "/first");
|
||||
break;
|
||||
case R.id.never:
|
||||
editor.putString(packageName, "/never");
|
||||
break;
|
||||
default:
|
||||
StringBuilder paths = new StringBuilder();
|
||||
for (int i = 0; i < adapter.getCount(); i++) {
|
||||
paths.append(adapter.getItem(i));
|
||||
if (i != adapter.getCount()) {
|
||||
paths.append("\n");
|
||||
}
|
||||
}
|
||||
editor.putString(packageName, paths.toString());
|
||||
}
|
||||
editor.apply();
|
||||
|
||||
// notify the recycler adapter if it is loaded
|
||||
if (callingActivity.recyclerAdapter != null) {
|
||||
int position;
|
||||
if (!isWeb) {
|
||||
String appName = getArguments().getString("appName", "");
|
||||
position = callingActivity.recyclerAdapter.getPosition(appName);
|
||||
callingActivity.recyclerAdapter.notifyItemChanged(position);
|
||||
} else {
|
||||
position = callingActivity.recyclerAdapter.getPosition(packageName);
|
||||
String oldPackageName = getArguments().getString("packageName", "");
|
||||
if (oldPackageName.equals(packageName)) {
|
||||
callingActivity.recyclerAdapter.notifyItemChanged(position);
|
||||
} else if (oldPackageName.equals("")){
|
||||
callingActivity.recyclerAdapter.addWebsite(packageName);
|
||||
} else {
|
||||
editor.remove(oldPackageName);
|
||||
callingActivity.recyclerAdapter.updateWebsite(oldPackageName, packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
((EditText) getDialog().findViewById(R.id.matched)).setText(data.getStringExtra("path"));
|
||||
} else {
|
||||
((RadioButton) getDialog().findViewById(R.id.use_default)).toggle();
|
||||
adapter.add(data.getStringExtra("path"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package com.zeapo.pwdstore.autofill;
|
||||
|
||||
import android.app.DialogFragment;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.view.MenuItemCompat;
|
||||
|
@ -14,15 +16,15 @@ import android.support.v7.app.AppCompatActivity;
|
|||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.util.Pair;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.zeapo.pwdstore.R;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AutofillPreferenceActivity extends AppCompatActivity {
|
||||
|
||||
|
@ -49,15 +51,24 @@ public class AutofillPreferenceActivity extends AppCompatActivity {
|
|||
|
||||
new populateTask().execute();
|
||||
|
||||
// if the preference activity was started from the autofill dialog
|
||||
recreate = false;
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
recreate = true;
|
||||
|
||||
showDialog(extras.getString("packageName"), extras.getString("appName"));
|
||||
showDialog(extras.getString("packageName"), extras.getString("appName"), extras.getBoolean("isWeb"));
|
||||
}
|
||||
|
||||
setTitle("Autofill Apps");
|
||||
|
||||
final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
|
||||
fab.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
showDialog("", "", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class populateTask extends AsyncTask<Void, Void, Void> {
|
||||
|
@ -70,15 +81,25 @@ public class AutofillPreferenceActivity extends AppCompatActivity {
|
|||
protected Void doInBackground(Void... params) {
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
List<ResolveInfo> allApps = pm.queryIntentActivities(intent, 0);
|
||||
List<ResolveInfo> allAppsResolveInfo = pm.queryIntentActivities(intent, 0);
|
||||
List<AutofillRecyclerAdapter.AppInfo> allApps = new ArrayList<>();
|
||||
|
||||
HashMap<String, Pair<Drawable, String>> iconMap = new HashMap<>(allApps.size());
|
||||
for (ResolveInfo app : allApps) {
|
||||
iconMap.put(app.activityInfo.packageName
|
||||
, Pair.create(app.loadIcon(pm), app.loadLabel(pm).toString()));
|
||||
for (ResolveInfo app : allAppsResolveInfo) {
|
||||
allApps.add(new AutofillRecyclerAdapter.AppInfo(app.activityInfo.packageName
|
||||
, app.loadLabel(pm).toString(), false, app.loadIcon(pm)));
|
||||
}
|
||||
|
||||
recyclerAdapter = new AutofillRecyclerAdapter(allApps, iconMap, pm, AutofillPreferenceActivity.this);
|
||||
SharedPreferences prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
||||
Map<String, ?> prefsMap = prefs.getAll();
|
||||
for (String key : prefsMap.keySet()) {
|
||||
try {
|
||||
allApps.add(new AutofillRecyclerAdapter.AppInfo(key, key, true, pm.getApplicationIcon("com.android.browser")));
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
allApps.add(new AutofillRecyclerAdapter.AppInfo(key, key, true, null));
|
||||
}
|
||||
}
|
||||
|
||||
recyclerAdapter = new AutofillRecyclerAdapter(allApps, pm, AutofillPreferenceActivity.this);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -89,7 +110,7 @@ public class AutofillPreferenceActivity extends AppCompatActivity {
|
|||
recyclerView.setAdapter(recyclerAdapter);
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
recyclerView.scrollToPosition(recyclerAdapter.getPosition(extras.getString("packageName")));
|
||||
recyclerView.scrollToPosition(recyclerAdapter.getPosition(extras.getString("appName")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,11 +159,12 @@ public class AutofillPreferenceActivity extends AppCompatActivity {
|
|||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void showDialog(String packageName, String appName) {
|
||||
public void showDialog(String packageName, String appName, boolean isWeb) {
|
||||
DialogFragment df = new AutofillFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString("packageName", packageName);
|
||||
args.putString("appName", appName);
|
||||
args.putBoolean("isWeb", isWeb);
|
||||
df.setArguments(args);
|
||||
df.show(getFragmentManager(), "autofill_dialog");
|
||||
}
|
||||
|
|
|
@ -3,12 +3,10 @@ package com.zeapo.pwdstore.autofill;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.v7.util.SortedList;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.util.SortedListAdapterCallback;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -18,15 +16,15 @@ import android.widget.TextView;
|
|||
import com.zeapo.pwdstore.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecyclerAdapter.ViewHolder> {
|
||||
private SortedList<ResolveInfo> apps;
|
||||
private ArrayList<ResolveInfo> allApps;
|
||||
private HashMap<String, Pair<Drawable, String>> iconMap;
|
||||
|
||||
private SortedList<AppInfo> apps;
|
||||
private ArrayList<AppInfo> allApps; // for filtering, maintain a list of all
|
||||
private PackageManager pm;
|
||||
private AutofillPreferenceActivity activity;
|
||||
Drawable browserIcon = null;
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
public View view;
|
||||
|
@ -34,6 +32,8 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
|
|||
public TextView secondary;
|
||||
public ImageView icon;
|
||||
public String packageName;
|
||||
public String appName;
|
||||
public Boolean isWeb;
|
||||
|
||||
public ViewHolder(View view) {
|
||||
super(view);
|
||||
|
@ -46,35 +46,60 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
|
|||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
activity.showDialog(packageName, name.getText().toString());
|
||||
activity.showDialog(packageName, appName, isWeb);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public AutofillRecyclerAdapter(List<ResolveInfo> allApps, HashMap<String, Pair<Drawable, String>> iconMap
|
||||
, final PackageManager pm, AutofillPreferenceActivity activity) {
|
||||
SortedList.Callback<ResolveInfo> callback = new SortedListAdapterCallback<ResolveInfo>(this) {
|
||||
public static class AppInfo {
|
||||
public String packageName;
|
||||
public String appName;
|
||||
public boolean isWeb;
|
||||
public Drawable icon;
|
||||
|
||||
public AppInfo(String packageName, String appName, boolean isWeb, Drawable icon) {
|
||||
this.packageName = packageName;
|
||||
this.appName = appName;
|
||||
this.isWeb = isWeb;
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o != null && o instanceof AppInfo && this.appName.equals(((AppInfo) o).appName);
|
||||
}
|
||||
}
|
||||
|
||||
public AutofillRecyclerAdapter(List<AppInfo> allApps, final PackageManager pm
|
||||
, AutofillPreferenceActivity activity) {
|
||||
SortedList.Callback<AppInfo> callback = new SortedListAdapterCallback<AppInfo>(this) {
|
||||
// don't take into account secondary text. This is good enough
|
||||
// for the limited add/remove usage for websites
|
||||
@Override
|
||||
public int compare(ResolveInfo o1, ResolveInfo o2) {
|
||||
return o1.loadLabel(pm).toString().toLowerCase().compareTo(o2.loadLabel(pm).toString().toLowerCase());
|
||||
public int compare(AppInfo o1, AppInfo o2) {
|
||||
return o1.appName.toLowerCase().compareTo(o2.appName.toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(ResolveInfo oldItem, ResolveInfo newItem) {
|
||||
return oldItem.loadLabel(pm).toString().equals(newItem.loadLabel(pm).toString());
|
||||
public boolean areContentsTheSame(AppInfo oldItem, AppInfo newItem) {
|
||||
return oldItem.appName.equals(newItem.appName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(ResolveInfo item1, ResolveInfo item2) {
|
||||
return item1.loadLabel(pm).toString().equals(item2.loadLabel(pm).toString());
|
||||
public boolean areItemsTheSame(AppInfo item1, AppInfo item2) {
|
||||
return item1.appName.equals(item2.appName);
|
||||
}
|
||||
};
|
||||
this.apps = new SortedList<>(ResolveInfo.class, callback);
|
||||
this.apps = new SortedList<>(AppInfo.class, callback);
|
||||
this.apps.addAll(allApps);
|
||||
this.allApps = new ArrayList<>(allApps);
|
||||
this.iconMap = new HashMap<>(iconMap);
|
||||
this.pm = pm;
|
||||
this.activity = activity;
|
||||
try {
|
||||
browserIcon = activity.getPackageManager().getApplicationIcon("com.android.browser");
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -86,17 +111,23 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(AutofillRecyclerAdapter.ViewHolder holder, int position) {
|
||||
ResolveInfo app = apps.get(position);
|
||||
holder.packageName = app.activityInfo.packageName;
|
||||
AppInfo app = apps.get(position);
|
||||
holder.packageName = app.packageName;
|
||||
holder.appName = app.appName;
|
||||
holder.isWeb = app.isWeb;
|
||||
|
||||
holder.icon.setImageDrawable(iconMap.get(holder.packageName).first);
|
||||
holder.name.setText(iconMap.get(holder.packageName).second);
|
||||
holder.icon.setImageDrawable(app.icon);
|
||||
holder.name.setText(app.appName);
|
||||
|
||||
holder.secondary.setVisibility(View.VISIBLE);
|
||||
holder.view.setBackgroundResource(R.color.grey_white_1000);
|
||||
|
||||
SharedPreferences prefs
|
||||
= activity.getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
||||
SharedPreferences prefs;
|
||||
if (!app.appName.equals(app.packageName)) {
|
||||
prefs = activity.getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
||||
} else {
|
||||
prefs = activity.getApplicationContext().getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
||||
}
|
||||
String preference = prefs.getString(holder.packageName, "");
|
||||
switch (preference) {
|
||||
case "":
|
||||
|
@ -110,7 +141,12 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
|
|||
holder.secondary.setText(R.string.autofill_apps_never);
|
||||
break;
|
||||
default:
|
||||
holder.secondary.setText("Match with " + preference);
|
||||
holder.secondary.setText(R.string.autofill_apps_match);
|
||||
holder.secondary.append(" " + preference.split("\n")[0]);
|
||||
if ((preference.trim().split("\n").length - 1) > 0) {
|
||||
holder.secondary.append(" and "
|
||||
+ (preference.trim().split("\n").length - 1) + " more");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -120,13 +156,25 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
|
|||
return apps.size();
|
||||
}
|
||||
|
||||
public int getPosition(String packageName) {
|
||||
for (int i = 0; i < apps.size(); i++) {
|
||||
if (apps.get(i).activityInfo.packageName.equals(packageName)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
public int getPosition(String appName) {
|
||||
return apps.indexOf(new AppInfo(null, appName, false, null));
|
||||
}
|
||||
|
||||
// for websites, URL = packageName == appName
|
||||
public void addWebsite(String packageName) {
|
||||
apps.add(new AppInfo(packageName, packageName, true, browserIcon));
|
||||
allApps.add(new AppInfo(packageName, packageName, true, browserIcon));
|
||||
}
|
||||
|
||||
public void removeWebsite(String packageName) {
|
||||
apps.remove(new AppInfo(null, packageName, false, null));
|
||||
allApps.remove(new AppInfo(null, packageName, false, null)); // compare with equals
|
||||
}
|
||||
|
||||
public void updateWebsite(String oldPackageName, String packageName) {
|
||||
apps.updateItemAt(getPosition(oldPackageName), new AppInfo (packageName, packageName, true, browserIcon));
|
||||
allApps.remove(new AppInfo(null, oldPackageName, false, null)); // compare with equals
|
||||
allApps.add(new AppInfo(null, packageName, false, null));
|
||||
}
|
||||
|
||||
public void filter(String s) {
|
||||
|
@ -135,8 +183,8 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecycl
|
|||
return;
|
||||
}
|
||||
apps.beginBatchedUpdates();
|
||||
for (ResolveInfo app : allApps) {
|
||||
if (app.loadLabel(pm).toString().toLowerCase().contains(s.toLowerCase())) {
|
||||
for (AppInfo app : allApps) {
|
||||
if (app.appName.toLowerCase().contains(s.toLowerCase())) {
|
||||
apps.add(app);
|
||||
} else {
|
||||
apps.remove(app);
|
||||
|
|
|
@ -24,7 +24,6 @@ import android.view.accessibility.AccessibilityWindowInfo;
|
|||
import android.widget.Toast;
|
||||
|
||||
import com.zeapo.pwdstore.R;
|
||||
import com.zeapo.pwdstore.utils.PasswordItem;
|
||||
import com.zeapo.pwdstore.utils.PasswordRepository;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
@ -38,24 +37,47 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AutofillService extends AccessibilityService {
|
||||
private static AutofillService instance;
|
||||
private OpenPgpServiceConnection serviceConnection;
|
||||
private SharedPreferences settings;
|
||||
private AccessibilityNodeInfo info; // the original source of the event (the edittext field)
|
||||
private ArrayList<PasswordItem> items; // password choices
|
||||
private ArrayList<File> items; // password choices
|
||||
private int lastWhichItem;
|
||||
private AlertDialog dialog;
|
||||
private AccessibilityWindowInfo window;
|
||||
private static Intent resultData = null; // need the intent which contains results from user interaction
|
||||
private Intent resultData = null; // need the intent which contains results from user interaction
|
||||
private CharSequence packageName;
|
||||
private boolean ignoreActionFocus = false;
|
||||
private String webViewTitle = null;
|
||||
private String webViewURL = null;
|
||||
|
||||
public final class Constants {
|
||||
public static final String TAG = "Keychain";
|
||||
}
|
||||
|
||||
public static void setResultData(Intent data) { resultData = data; }
|
||||
public static AutofillService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void setResultData(Intent data) { resultData = data; }
|
||||
|
||||
public void setPickedPassword(String path) {
|
||||
items.add(new File(PasswordRepository.getWorkTree() + "/" + path + ".gpg"));
|
||||
bindDecryptAndVerify();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onServiceConnected() {
|
||||
|
@ -66,18 +88,54 @@ public class AutofillService extends AccessibilityService {
|
|||
settings = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
}
|
||||
|
||||
// TODO change search/search results (just use first result)
|
||||
@Override
|
||||
public void onAccessibilityEvent(AccessibilityEvent event) {
|
||||
// TODO there should be a better way of disabling service
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if returning to the source app from a successful AutofillActivity
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
||||
&& event.getPackageName().equals(packageName) && resultData != null) {
|
||||
bindDecryptAndVerify();
|
||||
}
|
||||
|
||||
// nothing to do if not password field focus, android version, or field is keychain app
|
||||
// look for webView and trigger accessibility events if window changes
|
||||
// or if page changes in chrome
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
||||
|| (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
||||
&& event.getSource() != null
|
||||
&& (event.getSource().getPackageName().equals("com.android.chrome")
|
||||
|| event.getSource().getPackageName().equals("com.android.browser")))) {
|
||||
// there is a chance for getRootInActiveWindow() to return null at any time. save it.
|
||||
AccessibilityNodeInfo root = getRootInActiveWindow();
|
||||
webViewTitle = searchWebView(root);
|
||||
webViewURL = null;
|
||||
if (webViewTitle != null) {
|
||||
List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar");
|
||||
if (nodes.isEmpty()) {
|
||||
nodes = root.findAccessibilityNodeInfosByViewId("com.android.browser:id/url");
|
||||
}
|
||||
for (AccessibilityNodeInfo node : nodes)
|
||||
if (node.getText() != null) {
|
||||
try {
|
||||
webViewURL = new URL(node.getText().toString()).getHost();
|
||||
} catch (MalformedURLException e) {
|
||||
if (e.toString().contains("Protocol not found")) {
|
||||
try {
|
||||
webViewURL = new URL("http://" + node.getText().toString()).getHost();
|
||||
} catch (MalformedURLException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// nothing to do if not password field focus, field is keychain app
|
||||
if (!event.isPassword()
|
||||
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
|
||||
|| event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
||||
|| event.getPackageName().equals("org.sufficientlysecure.keychain")) {
|
||||
dismissDialog(event);
|
||||
return;
|
||||
|
@ -91,6 +149,7 @@ public class AutofillService extends AccessibilityService {
|
|||
}
|
||||
// if it was not a click, the field was refocused or another field was focused; recreate
|
||||
dialog.dismiss();
|
||||
dialog = null;
|
||||
}
|
||||
|
||||
// ignore the ACTION_FOCUS from decryptAndVerify otherwise dialog will appear after Fill
|
||||
|
@ -103,12 +162,14 @@ public class AutofillService extends AccessibilityService {
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
&& !Settings.canDrawOverlays(this)) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:" + getApplicationContext().getPackageName()));
|
||||
Uri.parse("package:" + getPackageName()));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
return;
|
||||
}
|
||||
|
||||
// we are now going to attempt to fill, save AccessibilityNodeInfo for later in decryptAndVerify
|
||||
// (there should be a proper way to do this, although this seems to work 90% of the time)
|
||||
info = event.getSource();
|
||||
|
||||
// save the dialog's corresponding window so we can use getWindows() in dismissDialog
|
||||
|
@ -116,22 +177,63 @@ public class AutofillService extends AccessibilityService {
|
|||
window = info.getWindow();
|
||||
}
|
||||
|
||||
// get the app name and find a corresponding password
|
||||
PackageManager packageManager = getPackageManager();
|
||||
ApplicationInfo applicationInfo;
|
||||
try {
|
||||
applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
applicationInfo = null;
|
||||
}
|
||||
final String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString();
|
||||
String packageName;
|
||||
String appName;
|
||||
boolean isWeb;
|
||||
|
||||
getMatchingPassword(appName, info.getPackageName().toString());
|
||||
if (items.isEmpty()) {
|
||||
// Match with the app if a webview was not found or one was found but
|
||||
// there's no title or url to go by
|
||||
if (webViewTitle == null || (webViewTitle.equals("") && webViewURL == null)) {
|
||||
packageName = info.getPackageName().toString();
|
||||
|
||||
// get the app name and find a corresponding password
|
||||
PackageManager packageManager = getPackageManager();
|
||||
ApplicationInfo applicationInfo;
|
||||
try {
|
||||
applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
applicationInfo = null;
|
||||
}
|
||||
appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString();
|
||||
|
||||
isWeb = false;
|
||||
|
||||
setMatchingPasswords(appName, packageName, false);
|
||||
} else {
|
||||
packageName = setMatchingPasswords(webViewTitle, webViewURL, true);
|
||||
appName = packageName;
|
||||
isWeb = true;
|
||||
}
|
||||
|
||||
// if autofill_always checked, show dialog even if no matches (automatic
|
||||
// or otherwise)
|
||||
if (items.isEmpty() && !settings.getBoolean("autofill_always", false)) {
|
||||
return;
|
||||
}
|
||||
showDialog(packageName, appName, isWeb);
|
||||
}
|
||||
|
||||
showDialog(appName);
|
||||
private String searchWebView(AccessibilityNodeInfo source) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < source.getChildCount(); i++) {
|
||||
AccessibilityNodeInfo u = source.getChild(i);
|
||||
if (u == null) {
|
||||
continue;
|
||||
}
|
||||
if (u.getClassName() != null && u.getClassName().equals("android.webkit.WebView")) {
|
||||
if (u.getContentDescription() != null) {
|
||||
return u.getContentDescription().toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (searchWebView(u) != null) {
|
||||
return searchWebView(u);
|
||||
}
|
||||
u.recycle();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// dismiss the dialog if the window has changed
|
||||
|
@ -149,80 +251,157 @@ public class AutofillService extends AccessibilityService {
|
|||
}
|
||||
if (dismiss && dialog != null && dialog.isShowing()) {
|
||||
dialog.dismiss();
|
||||
dialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void getMatchingPassword(String appName, String packageName) {
|
||||
private String setMatchingPasswords(String appName, String packageName, boolean isWeb) {
|
||||
// Return the URL needed to open the corresponding Settings.
|
||||
String settingsURL = packageName;
|
||||
|
||||
// if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
|
||||
String defValue = settings.getBoolean("autofill_default", true) ? "/first" : "/never";
|
||||
SharedPreferences prefs = getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
||||
String preference = prefs.getString(packageName, defValue);
|
||||
SharedPreferences prefs;
|
||||
String preference;
|
||||
|
||||
// for websites unlike apps there can be blank preference of "" which
|
||||
// means use default, so ignore it.
|
||||
if (!isWeb) {
|
||||
prefs = getSharedPreferences("autofill", Context.MODE_PRIVATE);
|
||||
preference = prefs.getString(packageName, defValue);
|
||||
} else {
|
||||
prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
||||
preference = defValue;
|
||||
Map<String, ?> prefsMap = prefs.getAll();
|
||||
for (String key : prefsMap.keySet()) {
|
||||
if ((webViewURL.toLowerCase().contains(key.toLowerCase()) || key.toLowerCase().contains(webViewURL.toLowerCase()))
|
||||
&& !prefs.getString(key, null).equals("")) {
|
||||
preference = prefs.getString(key, null);
|
||||
settingsURL = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (preference) {
|
||||
case "/first":
|
||||
if (!PasswordRepository.isInitialized()) {
|
||||
PasswordRepository.initialize(this);
|
||||
}
|
||||
items = recursiveFilter(appName, null);
|
||||
items = searchPasswords(PasswordRepository.getRepositoryDirectory(this), appName);
|
||||
break;
|
||||
case "/never":
|
||||
items.clear();
|
||||
return;
|
||||
default:
|
||||
if (!PasswordRepository.isInitialized()) {
|
||||
PasswordRepository.initialize(this);
|
||||
}
|
||||
String path = PasswordRepository.getWorkTree() + "/" + preference + ".gpg";
|
||||
File file = new File(path);
|
||||
items = new ArrayList<>();
|
||||
items.add(PasswordItem.newPassword(file.getName(), file, PasswordRepository.getRepositoryDirectory(this)));
|
||||
break;
|
||||
default:
|
||||
getPreferredPasswords(preference);
|
||||
}
|
||||
|
||||
return settingsURL;
|
||||
}
|
||||
|
||||
// Put the newline separated list of passwords from the SharedPreferences
|
||||
// file into the items list.
|
||||
private void getPreferredPasswords(String preference) {
|
||||
if (!PasswordRepository.isInitialized()) {
|
||||
PasswordRepository.initialize(this);
|
||||
}
|
||||
String preferredPasswords[] = preference.split("\n");
|
||||
items = new ArrayList<>();
|
||||
for (String password : preferredPasswords) {
|
||||
String path = PasswordRepository.getWorkTree() + "/" + password + ".gpg";
|
||||
if (new File(path).exists()) {
|
||||
items.add(new File(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
private ArrayList<File> searchPasswords(File path, String appName) {
|
||||
ArrayList<File> passList
|
||||
= PasswordRepository.getFilesList(path);
|
||||
|
||||
if (passList.size() == 0) return new ArrayList<>();
|
||||
|
||||
ArrayList<File> items = new ArrayList<>();
|
||||
|
||||
for (File file : passList) {
|
||||
if (file.isFile()) {
|
||||
if (appName.toLowerCase().contains(file.getName().toLowerCase().replace(".gpg", ""))) {
|
||||
items.add(file);
|
||||
}
|
||||
} else {
|
||||
// ignore .git directory
|
||||
if (file.getName().equals(".git"))
|
||||
continue;
|
||||
items.addAll(searchPasswords(file, appName));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private void showDialog(final String appName) {
|
||||
if (dialog == null) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
|
||||
builder.setNegativeButton(R.string.dialog_cancel, null);
|
||||
builder.setPositiveButton(R.string.autofill_fill, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
private void showDialog(final String packageName, final String appName, final boolean isWeb) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
|
||||
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface d, int which) {
|
||||
dialog.dismiss();
|
||||
dialog = null;
|
||||
}
|
||||
});
|
||||
builder.setNeutralButton("Settings", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) { //TODO make icon? gear?
|
||||
// the user will have to return to the app themselves.
|
||||
Intent intent = new Intent(AutofillService.this, AutofillPreferenceActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.putExtra("packageName", packageName);
|
||||
intent.putExtra("appName", appName);
|
||||
intent.putExtra("isWeb", isWeb);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
// populate the dialog items, always with pick + pick and match. Could
|
||||
// make it optional (or make height a setting for the same effect)
|
||||
CharSequence itemNames[] = new CharSequence[items.size() + 2];
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
itemNames[i] = items.get(i).getName().replace(".gpg", "");
|
||||
}
|
||||
itemNames[items.size()] = "Pick...";
|
||||
itemNames[items.size() + 1] = "Pick and match...";
|
||||
builder.setItems(itemNames, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
lastWhichItem = which;
|
||||
if (which < items.size()) {
|
||||
bindDecryptAndVerify();
|
||||
}
|
||||
});
|
||||
builder.setNeutralButton("Settings", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) { //TODO make icon? gear?
|
||||
// the user will have to return to the app themselves.
|
||||
Intent intent = new Intent(AutofillService.this, AutofillPreferenceActivity.class);
|
||||
} else if (which == items.size()){
|
||||
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.putExtra("packageName", info.getPackageName());
|
||||
intent.putExtra("appName", appName);
|
||||
intent.putExtra("pick", true);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
lastWhichItem--; // will add one element to items, so lastWhichItem=items.size()+1
|
||||
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.putExtra("pickMatchWith", true);
|
||||
intent.putExtra("packageName", packageName);
|
||||
intent.putExtra("isWeb", isWeb);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
dialog = builder.create();
|
||||
dialog.setIcon(R.drawable.ic_launcher);
|
||||
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
||||
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||
dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
});
|
||||
|
||||
dialog = builder.create();
|
||||
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
||||
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||
// arbitrary non-annoying size
|
||||
int height = 154;
|
||||
if (itemNames.length > 1) {
|
||||
height += 46;
|
||||
}
|
||||
dialog.setTitle(items.get(0).toString());
|
||||
dialog.getWindow().setLayout((int) (240 * getApplicationContext().getResources().getDisplayMetrics().density)
|
||||
, (int) (height * getApplicationContext().getResources().getDisplayMetrics().density));
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
@ -266,13 +445,14 @@ public class AutofillService extends AccessibilityService {
|
|||
}
|
||||
InputStream is = null;
|
||||
try {
|
||||
is = FileUtils.openInputStream(items.get(0).getFile());
|
||||
is = FileUtils.openInputStream(items.get(lastWhichItem));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
|
||||
OpenPgpApi api = new OpenPgpApi(AutofillService.this, serviceConnection.getService());
|
||||
// TODO we are dropping frames, (did we before??) find out why and maybe make this async
|
||||
Intent result = api.executeApi(data, is, os);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS: {
|
||||
|
@ -281,6 +461,7 @@ public class AutofillService extends AccessibilityService {
|
|||
|
||||
// if the user focused on something else, take focus back
|
||||
// but this will open another dialog...hack to ignore this
|
||||
// & need to ensure performAction correct (i.e. what is info now?)
|
||||
ignoreActionFocus = info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Bundle args = new Bundle();
|
||||
|
@ -302,6 +483,7 @@ public class AutofillService extends AccessibilityService {
|
|||
}
|
||||
}
|
||||
}
|
||||
info.recycle();
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.e(Constants.TAG, "UnsupportedEncodingException", e);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/autofill_recycler"
|
||||
|
@ -17,4 +18,19 @@
|
|||
android:layout_centerInParent="true"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:src="@drawable/ic_action_new"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
app:elevation="6dp"
|
||||
app:pressedTranslationZ="12dp"
|
||||
app:backgroundTint="@color/blue_grey_500"
|
||||
app:rippleColor="@color/blue_grey_50"
|
||||
app:borderWidth="0dp"
|
||||
android:layout_margin="@dimen/fab_compat_margin"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"/>
|
||||
</RelativeLayout>
|
|
@ -1,12 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="20dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="20dp">
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="20dp"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="20dp">
|
||||
|
||||
<android.support.design.widget.TextInputLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:hintTextAppearance="@style/TextAppearance.AppCompat">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/webURL"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="URL"
|
||||
android:inputType="textUri"/>
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/autofill_radiogroup"
|
||||
|
@ -37,12 +51,20 @@
|
|||
android:checked="false"
|
||||
android:text="@string/autofill_apps_match_ellipsis" />
|
||||
|
||||
<EditText
|
||||
<ListView
|
||||
android:id="@+id/matched"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:editable="false" />
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<Button
|
||||
style="?android:attr/buttonStyleSmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="+"
|
||||
android:id="@+id/matchButton"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/never"
|
||||
|
@ -50,7 +72,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:checked="false"
|
||||
android:text="@string/autofill_apps_never" />
|
||||
android:text="@string/autofill_apps_never"/>
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
|
|
|
@ -168,5 +168,6 @@
|
|||
<string name="autofill_apps_default">Použít výchozí nastavení</string>
|
||||
<string name="autofill_apps_first">Automaticky spárovat</string>
|
||||
<string name="autofill_apps_match_ellipsis">Spárovat s…</string>
|
||||
<string name="autofill_apps_match">Spárovat s</string>
|
||||
<string name="autofill_apps_never">Nikdy nepárovat</string>
|
||||
</resources>
|
|
@ -112,7 +112,7 @@
|
|||
<string name="pref_password_dialog_title">Set the time you want the password to be in clipboard</string>
|
||||
<string name="pref_copy_title">Automatically Copy Password</string>
|
||||
<string name="pref_copy_dialog_title">Automatically copy the password to the clipboard after decryption was successful.</string>
|
||||
<string name="ssh_key_success_dialog_title" translatable="false">SSH-key imported</string>
|
||||
<string name="ssh_key_success_dialog_title">SSH-key imported</string>
|
||||
<string name="ssh_key_error_dialog_title">Error while trying to import the ssh-key</string>
|
||||
<string name="ssh_key_error_dialog_text">Message : \n</string>
|
||||
<string name="pref_recursive_filter">Recursive filtering</string>
|
||||
|
@ -121,10 +121,11 @@
|
|||
<string name="pref_autofill_enable_msg">Tap OK to go to Accessibility settings. There, tap Password Store under Services then tap the switch in the top right to turn it on or off.</string>
|
||||
<string name="pref_autofill_enable_msg2">Once the service is on, a dialog will appear when you click on a password field in an app if a matching password for the app exists.</string>
|
||||
<string name="pref_autofill_enable_msg3">Password Store attempts to match apps with passwords automatically. You can change this default setting and also matching settings per-app.</string>
|
||||
<string name="pref_autofill_apps_title">Per-app settings</string>
|
||||
<string name="pref_autofill_apps_title">App and website settings</string>
|
||||
<string name="pref_autofill_apps_hint">Customize autofill settings for specific apps.</string>
|
||||
<string name="pref_autofill_default_title">Automatically match by default</string>
|
||||
<string name="pref_autofill_default_hint">Default to \'Automatically match\' for apps without custom settings. Otherwise, \'Never match.\'</string>
|
||||
<string name="pref_autofill_always_title">Always show dialog</string>
|
||||
<string name="pref_clear_clipboard_title">Clear clipboard 20 times</string>
|
||||
<string name="pref_clear_clipboard_hint">Store nonsense in the clipboard 20 times instead of just once. Useful on Samsung phones that feature clipboard history.</string>
|
||||
|
||||
|
@ -168,5 +169,7 @@
|
|||
<string name="autofill_apps_default">Use default setting</string>
|
||||
<string name="autofill_apps_first">Automatically match</string>
|
||||
<string name="autofill_apps_match_ellipsis">Match with…</string>
|
||||
<string name="autofill_apps_match">Match with</string>
|
||||
<string name="autofill_apps_never">Never match</string>
|
||||
<string name="autofill_apps_delete">Delete</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/autofill_description"
|
||||
android:accessibilityEventTypes="typeViewFocused|typeViewClicked|typeWindowStateChanged|typeWindowContentChanged"
|
||||
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows"
|
||||
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagReportViewIds"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:notificationTimeout="100"
|
||||
android:canRetrieveWindowContent="true"
|
||||
android:canRequestEnhancedWebAccessibility="true"
|
||||
/>
|
|
@ -79,7 +79,6 @@
|
|||
<Preference
|
||||
android:dependency="autofill_enable"
|
||||
android:key="autofill_apps"
|
||||
android:summary="@string/pref_autofill_apps_hint"
|
||||
android:title="@string/pref_autofill_apps_title"/>
|
||||
<CheckBoxPreference
|
||||
android:dependency="autofill_enable"
|
||||
|
@ -87,6 +86,11 @@
|
|||
android:key="autofill_default"
|
||||
android:summary="@string/pref_autofill_default_hint"
|
||||
android:title="@string/pref_autofill_default_title"/>
|
||||
<CheckBoxPreference
|
||||
android:dependency="autofill_enable"
|
||||
android:defaultValue="false"
|
||||
android:key="autofill_always"
|
||||
android:title="@string/pref_autofill_always_title"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="Misc">
|
||||
|
|
Loading…
Reference in a new issue