diff options
author | wongma7 <wongma7@users.noreply.github.com> | 2015-08-25 13:31:07 -0400 |
---|---|---|
committer | wongma7 <wongma7@users.noreply.github.com> | 2015-08-25 13:31:07 -0400 |
commit | 327945f3b8aa56f77559a25bc9160dae0a0cc6d2 (patch) | |
tree | c92d1450ee4930cf2b91a7b34efa5c97cee826b7 /app/src/main/java | |
parent | eced1dd314ba651f862396c340492c6f2d9da926 (diff) | |
parent | a73e8625f55e869ba0d74bfa370178c54be3662d (diff) |
Merge pull request #117 from zeapo/autofill
Autofill
Diffstat (limited to 'app/src/main/java')
9 files changed, 916 insertions, 2 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java index 72636043..1daa4f90 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.java @@ -112,7 +112,11 @@ public class PasswordFragment extends Fragment{ ((AppCompatActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true); } else { - ((PasswordStore) getActivity()).decryptPassword(item); + if (getArguments().getBoolean("matchWith", false)) { + ((PasswordStore) getActivity()).matchPasswordWithApp(item); + } else { + ((PasswordStore) getActivity()).decryptPassword(item); + } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java index e51ef201..39d918c8 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.java @@ -289,6 +289,12 @@ public class PasswordStore extends AppCompatActivity { Bundle args = new Bundle(); args.putString("Path", PasswordRepository.getWorkTree().getAbsolutePath()); + // if the activity was started from the autofill settings, the + // intent is to match a clicked pwd with app. pass this to fragment + if (getIntent().getBooleanExtra("matchWith", false)) { + args.putBoolean("matchWith", true); + } + plist.setArguments(args); fragmentTransaction.addToBackStack("passlist"); @@ -531,4 +537,13 @@ public class PasswordStore extends AppCompatActivity { }) .show(); } + + public void matchPasswordWithApp(PasswordItem item) { + String path = item.getFile().getAbsolutePath(); + path = path.replace(PasswordRepository.getWorkTree() + "/", "").replace(".gpg", ""); + Intent data = new Intent(); + data.putExtra("path", path); + setResult(RESULT_OK, data); + finish(); + } } diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java index 9a5c83d4..8f8e8d66 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java @@ -1,22 +1,29 @@ package com.zeapo.pwdstore; -import android.app.AlertDialog; +import android.accessibilityservice.AccessibilityServiceInfo; import android.app.DialogFragment; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceManager; +import android.provider.Settings; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.text.SpannableStringBuilder; import android.view.MenuItem; +import android.view.accessibility.AccessibilityManager; import android.widget.Toast; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; +import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity; import com.zeapo.pwdstore.crypto.PgpHandler; import com.zeapo.pwdstore.git.GitActivity; import com.zeapo.pwdstore.utils.PasswordRepository; @@ -32,6 +39,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; +import java.util.List; import java.util.Set; public class UserPreference extends AppCompatActivity { @@ -182,6 +190,40 @@ public class UserPreference extends AppCompatActivity { findPreference("pref_select_external").setOnPreferenceChangeListener(resetRepo); findPreference("git_external").setOnPreferenceChangeListener(resetRepo); + + findPreference("autofill_apps").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(callingActivity, AutofillPreferenceActivity.class); + startActivity(intent); + return true; + } + }); + + findPreference("autofill_enable").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + new AlertDialog.Builder(callingActivity). + setTitle(R.string.pref_autofill_enable_title). + setView(R.layout.autofill_instructions). + setPositiveButton(R.string.dialog_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + startActivity(intent); + } + }). + setNegativeButton(R.string.dialog_cancel, null). + setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + ((CheckBoxPreference) findPreference("autofill_enable")) + .setChecked(((UserPreference) getActivity()).isServiceEnabled()); + } + }).show(); + return true; + } + }); } @Override @@ -189,6 +231,10 @@ public class UserPreference extends AppCompatActivity { super.onStart(); final SharedPreferences sharedPreferences = getPreferenceManager().getSharedPreferences(); findPreference("ssh_see_key").setEnabled(sharedPreferences.getBoolean("use_generated_key", false)); + + // see if the autofill service is enabled and check the preference accordingly + ((CheckBoxPreference) findPreference("autofill_enable")) + .setChecked(((UserPreference) getActivity()).isServiceEnabled()); } } @@ -268,6 +314,21 @@ public class UserPreference extends AppCompatActivity { sshKey.close(); } + // Returns whether the autofill service is enabled + private boolean isServiceEnabled() { + AccessibilityManager am = (AccessibilityManager) this + .getSystemService(Context.ACCESSIBILITY_SERVICE); + List<AccessibilityServiceInfo> runningServices = am + .getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_GENERIC); + for (AccessibilityServiceInfo service : runningServices) { + if ("com.zeapo.pwdstore/.autofill.AutofillService".equals(service.getId())) { + return true; + } + } + return false; + } + + protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java new file mode 100644 index 00000000..d8e50ebd --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java @@ -0,0 +1,40 @@ +package com.zeapo.pwdstore.autofill; + +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; + +// blank activity started by service for calling startIntentSenderForResult +public class AutofillActivity extends AppCompatActivity { + public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle extras = getIntent().getExtras(); + + if (extras != null) { + try { + PendingIntent pi = extras.getParcelable("pending_intent"); + if (pi == null) { + return; + } + startIntentSenderForResult(pi.getIntentSender() + , REQUEST_CODE_DECRYPT_AND_VERIFY, null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.e(AutofillService.Constants.TAG, "SendIntentException", e); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + finish(); // go back to the password field app + if (resultCode == RESULT_OK) { + AutofillService.setUnlockOK(); // report the result to service + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java new file mode 100644 index 00000000..e708c19d --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java @@ -0,0 +1,121 @@ +package com.zeapo.pwdstore.autofill; + +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.support.v7.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import com.zeapo.pwdstore.PasswordStore; +import com.zeapo.pwdstore.R; + +public class AutofillFragment extends DialogFragment { + private static final int MATCH_WITH = 777; + + public AutofillFragment() { + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + // this fragment is only created from the settings page (AutofillPreferenceActivity) + // need to interact with the recyclerAdapter which is a member of activity + final AutofillPreferenceActivity callingActivity = (AutofillPreferenceActivity) getActivity(); + LayoutInflater inflater = callingActivity.getLayoutInflater(); + final View view = inflater.inflate(R.layout.fragment_autofill, null); + + builder.setView(view); + + final String packageName = getArguments().getString("packageName"); + String appName = getArguments().getString("appName"); + + builder.setTitle(appName); + try { + // since we can't (easily?) pass the drawable as an argument + builder.setIcon(callingActivity.getPackageManager().getApplicationIcon(packageName)); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + SharedPreferences prefs + = getActivity().getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE); + String preference = prefs.getString(packageName, ""); + switch (preference) { + case "": + ((RadioButton) view.findViewById(R.id.use_default)).toggle(); + break; + case "/first": + ((RadioButton) view.findViewById(R.id.first)).toggle(); + break; + case "/never": + ((RadioButton) view.findViewById(R.id.never)).toggle(); + break; + default: + ((RadioButton) view.findViewById(R.id.match)).toggle(); + ((EditText) view.findViewById(R.id.matched)).setText(preference); + } + + View.OnClickListener matchPassword = new View.OnClickListener() { + @Override + public void onClick(View v) { + ((RadioButton) view.findViewById(R.id.match)).toggle(); + Intent intent = new Intent(getActivity(), PasswordStore.class); + intent.putExtra("matchWith", true); + startActivityForResult(intent, MATCH_WITH); + } + }; + view.findViewById(R.id.match).setOnClickListener(matchPassword); + view.findViewById(R.id.matched).setOnClickListener(matchPassword); + + final SharedPreferences.Editor editor = prefs.edit(); + builder.setPositiveButton(R.string.dialog_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.autofill_radiogroup); + switch (radioGroup.getCheckedRadioButtonId()) { + case R.id.use_default: + editor.remove(packageName); + break; + case R.id.first: + editor.putString(packageName, "/first"); + break; + case R.id.never: + editor.putString(packageName, "/never"); + break; + default: + EditText matched = (EditText) view.findViewById(R.id.matched); + String path = matched.getText().toString(); + editor.putString(packageName, path); + } + editor.apply(); + int position = getArguments().getInt("position"); + callingActivity.recyclerAdapter.notifyItemChanged(position); + + if (getArguments().getBoolean("finish")) { + callingActivity.finish(); + } + } + }); + builder.setNegativeButton(R.string.dialog_cancel, null); + return builder.create(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) { + ((EditText) getDialog().findViewById(R.id.matched)).setText(data.getStringExtra("path")); + } else { + ((RadioButton) getDialog().findViewById(R.id.use_default)).toggle(); + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java new file mode 100644 index 00000000..16867bcd --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java @@ -0,0 +1,146 @@ +package com.zeapo.pwdstore.autofill; + +import android.app.DialogFragment; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.support.v4.app.TaskStackBuilder; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.util.Pair; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import com.zeapo.pwdstore.R; + +import java.util.HashMap; +import java.util.List; + +public class AutofillPreferenceActivity extends AppCompatActivity { + + private RecyclerView recyclerView; + AutofillRecyclerAdapter recyclerAdapter; // let fragment have access + private RecyclerView.LayoutManager layoutManager; + + private PackageManager pm; + + private boolean recreate; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.autofill_recycler_view); + recyclerView = (RecyclerView) findViewById(R.id.autofill_recycler); + + layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + recyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST)); + + pm = getPackageManager(); + + new populateTask().execute(); + + setTitle("Autofill Apps"); + } + + private class populateTask extends AsyncTask<Void, Void, Void> { + @Override + protected void onPreExecute() { + findViewById(R.id.progress_bar).setVisibility(View.VISIBLE); + } + + @Override + protected Void doInBackground(Void... params) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + List<ResolveInfo> allApps = pm.queryIntentActivities(intent, 0); + + HashMap<String, Pair<Drawable, String>> iconMap = new HashMap<>(allApps.size()); + for (ResolveInfo app : allApps) { + iconMap.put(app.activityInfo.packageName + , Pair.create(app.loadIcon(pm), app.loadLabel(pm).toString())); + } + + recyclerAdapter = new AutofillRecyclerAdapter(allApps, iconMap, pm, AutofillPreferenceActivity.this); + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + findViewById(R.id.progress_bar).setVisibility(View.GONE); + + recyclerView.setAdapter(recyclerAdapter); + + recreate = false; + Bundle extras = getIntent().getExtras(); + if (extras != null) { + recreate = true; + recyclerView.scrollToPosition(recyclerAdapter.getPosition(extras.getString("packageName"))); + showDialog(extras.getString("packageName"), extras.getString("appName")); + } + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.autofill_preference, menu); + MenuItem searchItem = menu.findItem(R.id.action_search); + SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); + + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + return false; + } + + @Override + public boolean onQueryTextChange(String s) { + if (recyclerAdapter != null) { + recyclerAdapter.filter(s); + } + return true; + } + }); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + // in service, we CLEAR_TASK. then we set the recreate flag. + // something of a hack, but w/o CLEAR_TASK, behaviour was unpredictable + case android.R.id.home: + Intent upIntent = NavUtils.getParentActivityIntent(this); + if (recreate) { + TaskStackBuilder.create(this) + .addNextIntentWithParentStack(upIntent) + .startActivities(); + } else { + NavUtils.navigateUpTo(this, upIntent); + } + return true; + } + return super.onOptionsItemSelected(item); + } + + public void showDialog(String packageName, String appName) { + DialogFragment df = new AutofillFragment(); + Bundle args = new Bundle(); + args.putString("packageName", packageName); + args.putString("appName", appName); + args.putInt("position", recyclerAdapter.getPosition(packageName)); + df.setArguments(args); + df.show(getFragmentManager(), "autofill_dialog"); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java new file mode 100644 index 00000000..a131598a --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java @@ -0,0 +1,148 @@ +package com.zeapo.pwdstore.autofill; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.support.v7.util.SortedList; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.util.SortedListAdapterCallback; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.zeapo.pwdstore.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class AutofillRecyclerAdapter extends RecyclerView.Adapter<AutofillRecyclerAdapter.ViewHolder> { + private SortedList<ResolveInfo> apps; + private ArrayList<ResolveInfo> allApps; + private HashMap<String, Pair<Drawable, String>> iconMap; + private PackageManager pm; + private AutofillPreferenceActivity activity; + + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + public View view; + public TextView name; + public TextView secondary; + public ImageView icon; + public String packageName; + + public ViewHolder(View view) { + super(view); + this.view = view; + name = (TextView) view.findViewById(R.id.app_name); + secondary = (TextView) view.findViewById(R.id.secondary_text); + icon = (ImageView) view.findViewById(R.id.app_icon); + view.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + activity.showDialog(packageName, name.getText().toString()); + } + + } + + public AutofillRecyclerAdapter(List<ResolveInfo> allApps, HashMap<String, Pair<Drawable, String>> iconMap + , final PackageManager pm, AutofillPreferenceActivity activity) { + SortedList.Callback<ResolveInfo> callback = new SortedListAdapterCallback<ResolveInfo>(this) { + @Override + public int compare(ResolveInfo o1, ResolveInfo o2) { + return o1.loadLabel(pm).toString().toLowerCase().compareTo(o2.loadLabel(pm).toString().toLowerCase()); + } + + @Override + public boolean areContentsTheSame(ResolveInfo oldItem, ResolveInfo newItem) { + return oldItem.loadLabel(pm).toString().equals(newItem.loadLabel(pm).toString()); + } + + @Override + public boolean areItemsTheSame(ResolveInfo item1, ResolveInfo item2) { + return item1.loadLabel(pm).toString().equals(item2.loadLabel(pm).toString()); + } + }; + this.apps = new SortedList<>(ResolveInfo.class, callback); + this.apps.addAll(allApps); + this.allApps = new ArrayList<>(allApps); + this.iconMap = new HashMap<>(iconMap); + this.pm = pm; + this.activity = activity; + } + + @Override + public AutofillRecyclerAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.autofill_row_layout, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(AutofillRecyclerAdapter.ViewHolder holder, int position) { + ResolveInfo app = apps.get(position); + holder.packageName = app.activityInfo.packageName; + + holder.icon.setImageDrawable(iconMap.get(holder.packageName).first); + holder.name.setText(iconMap.get(holder.packageName).second); + + holder.secondary.setVisibility(View.VISIBLE); + holder.view.setBackgroundResource(R.color.grey_white_1000); + + SharedPreferences prefs + = activity.getApplicationContext().getSharedPreferences("autofill", Context.MODE_PRIVATE); + String preference = prefs.getString(holder.packageName, ""); + switch (preference) { + case "": + holder.secondary.setVisibility(View.GONE); + // "android:windowBackground" + holder.view.setBackgroundResource(R.color.indigo_50); + break; + case "/first": + holder.secondary.setText(R.string.autofill_apps_first); + break; + case "/never": + holder.secondary.setText(R.string.autofill_apps_never); + break; + default: + holder.secondary.setText("Match with " + preference); + break; + } + } + + @Override + public int getItemCount() { + return apps.size(); + } + + public int getPosition(String packageName) { + for (int i = 0; i < apps.size(); i++) { + if (apps.get(i).activityInfo.packageName.equals(packageName)) { + return i; + } + } + return -1; + } + + public void filter(String s) { + if (s.isEmpty()) { + apps.addAll(allApps); + return; + } + apps.beginBatchedUpdates(); + for (ResolveInfo app : allApps) { + if (app.loadLabel(pm).toString().toLowerCase().contains(s.toLowerCase())) { + apps.add(app); + } else { + apps.remove(app); + } + } + apps.endBatchedUpdates(); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java new file mode 100644 index 00000000..ea09c863 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java @@ -0,0 +1,274 @@ +package com.zeapo.pwdstore.autofill; + +import android.accessibilityservice.AccessibilityService; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; +import android.widget.Toast; + +import com.zeapo.pwdstore.R; +import com.zeapo.pwdstore.utils.PasswordItem; +import com.zeapo.pwdstore.utils.PasswordRepository; + +import org.apache.commons.io.FileUtils; +import org.openintents.openpgp.OpenPgpError; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; + +public class AutofillService extends AccessibilityService { + private OpenPgpServiceConnection serviceConnection; + private SharedPreferences settings; + private AccessibilityNodeInfo info; // the original source of the event (the edittext field) + private ArrayList<PasswordItem> items; // password choices + private AlertDialog dialog; + private AccessibilityWindowInfo window; + private static boolean unlockOK = false; // if openkeychain user interaction was successful + private CharSequence packageName; + private boolean ignoreActionFocus = false; + + public final class Constants { + public static final String TAG = "Keychain"; + } + + public static void setUnlockOK() { unlockOK = true; } + + @Override + protected void onServiceConnected() { + super.onServiceConnected(); + serviceConnection = new OpenPgpServiceConnection(AutofillService.this, "org.sufficientlysecure.keychain"); + serviceConnection.bindToService(); + settings = PreferenceManager.getDefaultSharedPreferences(this); + } + // TODO change search/search results (just use first result) + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + // if returning to the source app from a successful AutofillActivity + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + && event.getPackageName().equals(packageName) && unlockOK) { + decryptAndVerify(); + } + + // nothing to do if not password field focus, android version, or field is keychain app + if (!event.isPassword() + || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 + || event.getPackageName().equals("org.sufficientlysecure.keychain")) { + // the default keyboard showing/hiding is a window state changed event + // on Android 5+ we can use getWindows() to determine when the original window is not visible + // on Android 4.3 we have to use window state changed events and filter out the keyboard ones + // there may be other exceptions... + boolean dismiss; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + dismiss = !getWindows().contains(window); + } else { + dismiss = !(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + && event.getPackageName().toString().contains("inputmethod")); + } + if (dismiss && dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + return; + } + + if (dialog != null && dialog.isShowing()) { + // if the view was clicked, the click event follows the focus event + // since the focus event was already handled, ignore click event + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) { + return; + } + // if past this point, a new dialog will be created, so dismiss the existing + dialog.dismiss(); + } + + // ignore the ACTION_FOCUS from decryptAndVerify otherwise dialog will appear after Fill + if (ignoreActionFocus) { + ignoreActionFocus = false; + return; + } + + info = event.getSource(); + + // save the dialog's corresponding window so we can use getWindows() above to check whether dismiss + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window = info.getWindow(); + } + + // get the app name and find a corresponding password + PackageManager packageManager = getPackageManager(); + ApplicationInfo applicationInfo; + try { + applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0); + } catch (PackageManager.NameNotFoundException e) { + applicationInfo = null; + } + final String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString(); + + // if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never" + String defValue = settings.getBoolean("autofill_default", true) ? "/first" : "/never"; + SharedPreferences prefs = getSharedPreferences("autofill", Context.MODE_PRIVATE); + String preference = prefs.getString(event.getPackageName().toString(), defValue); + switch (preference) { + case "/first": + if (!PasswordRepository.isInitialized()) { + PasswordRepository.initialize(this); + } + items = recursiveFilter(appName, null); + break; + case "/never": + return; + default: + if (!PasswordRepository.isInitialized()) { + PasswordRepository.initialize(this); + } + String path = PasswordRepository.getWorkTree() + "/" + preference + ".gpg"; + File file = new File(path); + items = new ArrayList<>(); + items.add(PasswordItem.newPassword(file.getName(), file, PasswordRepository.getRepositoryDirectory(this))); + } + if (items.isEmpty()) { + return; + } + + if (dialog == null) { + AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog); + builder.setNegativeButton(R.string.dialog_cancel, null); + builder.setPositiveButton(R.string.autofill_fill, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + decryptAndVerify(); + } + }); + builder.setNeutralButton("Settings", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { //TODO make icon? gear? + // the user will have to return to the app themselves. + Intent intent = new Intent(AutofillService.this, AutofillPreferenceActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra("packageName", info.getPackageName()); + intent.putExtra("appName", appName); + startActivity(intent); + } + }); + dialog = builder.create(); + dialog.setIcon(R.drawable.ic_launcher); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); + } + dialog.setTitle(items.get(0).toString()); + dialog.show(); + } + + private ArrayList<PasswordItem> recursiveFilter(String filter, File dir) { + ArrayList<PasswordItem> items = new ArrayList<>(); + ArrayList<PasswordItem> passwordItems = dir == null ? + PasswordRepository.getPasswords(PasswordRepository.getRepositoryDirectory(this)) : + PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(this)); + for (PasswordItem item : passwordItems) { + if (item.getType() == PasswordItem.TYPE_CATEGORY) { + items.addAll(recursiveFilter(filter, item.getFile())); + } + if (item.toString().toLowerCase().contains(filter.toLowerCase())) { + items.add(item); + } + } + return items; + } + + @Override + public void onInterrupt() { + + } + + public void decryptAndVerify() { + unlockOK = false; + packageName = info.getPackageName(); + Intent data = new Intent(); + data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + InputStream is = null; + try { + is = FileUtils.openInputStream(items.get(0).getFile()); + } catch (IOException e) { + e.printStackTrace(); + } + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + OpenPgpApi api = new OpenPgpApi(AutofillService.this, serviceConnection.getService()); + Intent result = api.executeApi(data, is, os); + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: { + try { + String[] passContent = os.toString("UTF-8").split("\n"); + + // if the user focused on something else, take focus back + // but this will open another dialog...hack to ignore this + ignoreActionFocus = info.performAction(AccessibilityNodeInfo.ACTION_FOCUS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Bundle args = new Bundle(); + args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, + passContent[0]); + info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); + } else { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("autofill_pm", passContent[0]); + clipboard.setPrimaryClip(clip); + info.performAction(AccessibilityNodeInfo.ACTION_PASTE); + + clip = ClipData.newPlainText("autofill_pm", "MyPasswordIsDaBest!"); + clipboard.setPrimaryClip(clip); + if (settings.getBoolean("clear_clipboard_20x", false)) { + for (int i = 0; i < 19; i++) { + clip = ClipData.newPlainText(String.valueOf(i), String.valueOf(i)); + clipboard.setPrimaryClip(clip); + } + } + } + } catch (UnsupportedEncodingException e) { + Log.e(Constants.TAG, "UnsupportedEncodingException", e); + } + break; + } + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: { + Log.i("PgpHandler", "RESULT_CODE_USER_INTERACTION_REQUIRED"); + PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT); + // need to start a blank activity to call startIntentSenderForResult + Intent intent = new Intent(AutofillService.this, AutofillActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra("pending_intent", pi); + startActivity(intent); + break; + } + case OpenPgpApi.RESULT_CODE_ERROR: { + OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR); + Toast.makeText(AutofillService.this, + "Error from OpenKeyChain : " + error.getMessage(), + Toast.LENGTH_LONG).show(); + Log.e(Constants.TAG, "onError getErrorId:" + error.getErrorId()); + Log.e(Constants.TAG, "onError getMessage:" + error.getMessage()); + break; + } + } + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java b/app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java new file mode 100644 index 00000000..bd5ec9de --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/DividerItemDecoration.java @@ -0,0 +1,105 @@ +package com.zeapo.pwdstore.autofill; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + private static final int[] ATTRS = new int[]{ + android.R.attr.listDivider + }; + + public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; + + public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; + + private Drawable mDivider; + + private int mOrientation; + + public DividerItemDecoration(Context context, int orientation) { + final TypedArray a = context.obtainStyledAttributes(ATTRS); + mDivider = a.getDrawable(0); + a.recycle(); + setOrientation(orientation); + } + + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { + throw new IllegalArgumentException("invalid orientation"); + } + mOrientation = orientation; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + public void drawVertical(Canvas c, RecyclerView parent) { + final int left = parent.getPaddingLeft(); + final int right = parent.getWidth() - parent.getPaddingRight(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int top = child.getBottom() + params.bottomMargin; + final int bottom = top + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + public void drawHorizontal(Canvas c, RecyclerView parent) { + final int top = parent.getPaddingTop(); + final int bottom = parent.getHeight() - parent.getPaddingBottom(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int left = child.getRight() + params.rightMargin; + final int right = left + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +} + |