From 02bfcf6c3f5ddd806dd9a92823926eced139842f Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Tue, 28 Jul 2015 03:11:02 -0400 Subject: Create app autofill service: a dialog pops up for all password fields & has a button to paste/set a password found in the store with name matching app's --- app/src/main/AndroidManifest.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'app/src/main/AndroidManifest.xml') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2849695f..d45583c9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,9 @@ + + + + + + + + + + + + + + -- cgit v1.2.3 From 2d7c37d379ae48fa3ff464032ad4cb1759b1dc31 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Fri, 31 Jul 2015 09:00:06 -0400 Subject: Add per-app settings page --- app/src/main/AndroidManifest.xml | 4 +- .../java/com/zeapo/pwdstore/AutofillActivity.java | 35 --- .../java/com/zeapo/pwdstore/AutofillService.java | 246 -------------------- .../java/com/zeapo/pwdstore/UserPreference.java | 10 + .../zeapo/pwdstore/autofill/AutofillActivity.java | 84 +++++++ .../pwdstore/autofill/AutofillRecyclerAdapter.java | 62 +++++ .../zeapo/pwdstore/autofill/AutofillService.java | 254 +++++++++++++++++++++ app/src/main/res/layout/autofill_recycler_view.xml | 12 + app/src/main/res/layout/autofill_row_layout.xml | 49 ++++ app/src/main/res/xml/preference.xml | 8 + 10 files changed, 481 insertions(+), 283 deletions(-) delete mode 100644 app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java delete mode 100644 app/src/main/java/com/zeapo/pwdstore/AutofillService.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java create mode 100644 app/src/main/res/layout/autofill_recycler_view.xml create mode 100644 app/src/main/res/layout/autofill_row_layout.xml (limited to 'app/src/main/AndroidManifest.xml') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d45583c9..b3600c53 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,7 +45,7 @@ android:value="com.zeapo.pwdstore.PasswordStore" /> - @@ -54,7 +54,7 @@ android:resource="@xml/autofill_config" /> - + diff --git a/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java deleted file mode 100644 index 2a1227b9..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.zeapo.pwdstore; - -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; - private boolean bound = false; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Intent intent = getIntent(); - PendingIntent pi = intent.getExtras().getParcelable("pending_intent"); - try { - 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/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/AutofillService.java deleted file mode 100644 index 887e100e..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/AutofillService.java +++ /dev/null @@ -1,246 +0,0 @@ -package com.zeapo.pwdstore; - -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.widget.Toast; - -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 items; // password choices - private AlertDialog dialog; - 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); - } - - @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(); - } - if (!event.isPassword() - || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 - || event.getPackageName().equals("org.sufficientlysecure.keychain")) { - if (!(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED - && event.getPackageName().toString().contains("com.android.inputmethod")) - && dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - return; - } - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - // ignore the ACTION_FOCUS from decryptAndVerify - if (ignoreActionFocus) { - ignoreActionFocus = false; - return; - } - info = event.getSource(); - PackageManager packageManager = getPackageManager(); - ApplicationInfo applicationInfo; - try { - applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0); - } catch (PackageManager.NameNotFoundException e) { - applicationInfo = null; - } - String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString(); - items = recursiveFilter(appName, null); - if (items.isEmpty()) { - return; - } - - if (dialog == null) { - AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog); - builder.setNegativeButton("Cancel", null); - builder.setPositiveButton("Fill", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - decryptAndVerify(); - } - }); - 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.setTitle(items.get(0).getName()); - dialog.show(); - dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); - } - - private ArrayList recursiveFilter(String filter, File dir) { - ArrayList items = new ArrayList<>(); - if (!PasswordRepository.isInitialized()) { - initialize(); - } - ArrayList passwordItems = dir == null ? - PasswordRepository.getPasswords() : - PasswordRepository.getPasswords(dir); - for (PasswordItem item : passwordItems) { - if (item.getType() == PasswordItem.TYPE_CATEGORY) { - recursiveFilter(filter, item.getFile()); - } - if (item.toString().toLowerCase().contains(filter.toLowerCase())) { - items.add(item); - } - } - return items; - } - - // just like PasswordRepository.initialize(). - private void initialize() { - File dir = null; - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - - if (settings.getBoolean("git_external", false)) { - if (settings.getString("git_external_repo", null) != null) { - dir = new File(settings.getString("git_external_repo", null)); - } - } else { - dir = new File(getFilesDir() + "/store"); - } - // temp for debug - if (dir == null) { - Intent intent = new Intent(this, UserPreference.class); - intent.putExtra("operation", "git_external"); - startActivity(intent); - return; - } - - // uninitialize the repo if the dir does not exist or is absolutely empty - if (!dir.exists() || !dir.isDirectory() || FileUtils.listFiles(dir, null, false).isEmpty()) { - settings.edit().putBoolean("repository_initialized", false).apply(); - } - - if (!PasswordRepository.getPasswords(dir).isEmpty()) { - settings.edit().putBoolean("repository_initialized", true).apply(); - } - - // create the repository static variable in PasswordRepository - PasswordRepository.getRepository(new File(dir.getAbsolutePath() + "/.git")); - } - - @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.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - 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/UserPreference.java b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java index 9a5c83d4..e2a9e5a7 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java @@ -17,6 +17,7 @@ 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.AutofillActivity; import com.zeapo.pwdstore.crypto.PgpHandler; import com.zeapo.pwdstore.git.GitActivity; import com.zeapo.pwdstore.utils.PasswordRepository; @@ -182,6 +183,15 @@ 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, AutofillActivity.class); + startActivity(intent); + return false; + } + }); } @Override 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..5d8fac5e --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java @@ -0,0 +1,84 @@ +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.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; + +import com.zeapo.pwdstore.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +// blank activity started by service for calling startIntentSenderForResult +public class AutofillActivity extends AppCompatActivity { + public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913; + private boolean bound = false; + + private RecyclerView recyclerView; + private AutofillRecyclerAdapter recyclerAdapter; + private RecyclerView.LayoutManager layoutManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + // if called by service just for startIntentSenderForResult + if (extras != null) { + try { + PendingIntent pi = intent.getExtras().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); + } + return; + } + // otherwise if called from settings + final PackageManager pm = getPackageManager(); + List allApps = pm.getInstalledApplications(0); + + SharedPreferences prefs + = getSharedPreferences("autofill", Context.MODE_PRIVATE); + Map prefApps = prefs.getAll(); + ArrayList apps = new ArrayList<>(); + for (ApplicationInfo applicationInfo : allApps) { + if (prefApps.containsKey(applicationInfo.packageName)) { + apps.add(applicationInfo); + } + } + + setContentView(R.layout.autofill_recycler_view); + recyclerView = (RecyclerView) findViewById(R.id.autofill_recycler); + + layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + + recyclerAdapter = new AutofillRecyclerAdapter(apps, pm); + recyclerView.setAdapter(recyclerAdapter); + + setTitle("Autofill Apps"); + + } + + @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/AutofillRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java new file mode 100644 index 00000000..b59223bd --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java @@ -0,0 +1,62 @@ +package com.zeapo.pwdstore.autofill; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.support.v7.widget.RecyclerView; +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; + +public class AutofillRecyclerAdapter extends RecyclerView.Adapter { + private ArrayList apps; + private PackageManager pm; + + public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + public View view; + public TextView name; + public ImageView icon; + + public ViewHolder(View view) { + super(view); + this.view = view; + name = (TextView) view.findViewById(R.id.app_name); + icon = (ImageView) view.findViewById(R.id.app_icon); + view.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + + } + } + + public AutofillRecyclerAdapter(ArrayList apps, PackageManager pm) { + this.apps = apps; + this.pm = pm; + } + + @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) { + ApplicationInfo app = apps.get(position); + holder.name.setText(pm.getApplicationLabel(app)); + holder.icon.setImageDrawable(pm.getApplicationIcon(app)); + } + + @Override + public int getItemCount() { + return apps.size(); + } +} 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..6188ef6d --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java @@ -0,0 +1,254 @@ +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.widget.Toast; + +import com.zeapo.pwdstore.R; +import com.zeapo.pwdstore.UserPreference; +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 items; // password choices + private AlertDialog dialog; + 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); + } + + @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(); + } + if (!event.isPassword() + || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 + || event.getPackageName().equals("org.sufficientlysecure.keychain")) { + if (!(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + && event.getPackageName().toString().contains("com.android.inputmethod")) + && dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + return; + } + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + // ignore the ACTION_FOCUS from decryptAndVerify + if (ignoreActionFocus) { + ignoreActionFocus = false; + return; + } + info = event.getSource(); + PackageManager packageManager = getPackageManager(); + ApplicationInfo applicationInfo; + try { + applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0); + } catch (PackageManager.NameNotFoundException e) { + applicationInfo = null; + } + String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString(); + items = recursiveFilter(appName, null); + if (items.isEmpty()) { + return; + } + + if (dialog == null) { + AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog); + builder.setNegativeButton("Cancel", null); + builder.setPositiveButton("Fill", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + decryptAndVerify(); + } + }); + builder.setNeutralButton("Match", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }); + 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.setTitle(items.get(0).getName()); + dialog.show(); + dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); + } + + private ArrayList recursiveFilter(String filter, File dir) { + ArrayList items = new ArrayList<>(); + if (!PasswordRepository.isInitialized()) { + initialize(); + } + ArrayList passwordItems = dir == null ? + PasswordRepository.getPasswords() : + PasswordRepository.getPasswords(dir); + for (PasswordItem item : passwordItems) { + if (item.getType() == PasswordItem.TYPE_CATEGORY) { + recursiveFilter(filter, item.getFile()); + } + if (item.toString().toLowerCase().contains(filter.toLowerCase())) { + items.add(item); + } + } + return items; + } + + // just like PasswordRepository.initialize(). + private void initialize() { + File dir = null; + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); + + if (settings.getBoolean("git_external", false)) { + if (settings.getString("git_external_repo", null) != null) { + dir = new File(settings.getString("git_external_repo", null)); + } + } else { + dir = new File(getFilesDir() + "/store"); + } + // temp for debug + if (dir == null) { + Intent intent = new Intent(this, UserPreference.class); + intent.putExtra("operation", "git_external"); + startActivity(intent); + return; + } + + // uninitialize the repo if the dir does not exist or is absolutely empty + if (!dir.exists() || !dir.isDirectory() || FileUtils.listFiles(dir, null, false).isEmpty()) { + settings.edit().putBoolean("repository_initialized", false).apply(); + } + + if (!PasswordRepository.getPasswords(dir).isEmpty()) { + settings.edit().putBoolean("repository_initialized", true).apply(); + } + + // create the repository static variable in PasswordRepository + PasswordRepository.getRepository(new File(dir.getAbsolutePath() + "/.git")); + } + + @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.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + 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/res/layout/autofill_recycler_view.xml b/app/src/main/res/layout/autofill_recycler_view.xml new file mode 100644 index 00000000..27afa77d --- /dev/null +++ b/app/src/main/res/layout/autofill_recycler_view.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/autofill_row_layout.xml b/app/src/main/res/layout/autofill_row_layout.xml new file mode 100644 index 00000000..3f94a8ad --- /dev/null +++ b/app/src/main/res/layout/autofill_row_layout.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml index e4071c96..3a8c4fdf 100644 --- a/app/src/main/res/xml/preference.xml +++ b/app/src/main/res/xml/preference.xml @@ -70,6 +70,14 @@ android:summary="@string/pref_recursive_filter_hint" android:title="@string/pref_recursive_filter" /> + + + + + Date: Mon, 3 Aug 2015 08:31:23 -0400 Subject: Add searching for apps in autofill settings page --- app/src/main/AndroidManifest.xml | 3 + .../zeapo/pwdstore/autofill/AutofillActivity.java | 68 +++++++++++++++++++--- .../zeapo/pwdstore/autofill/AutofillService.java | 2 +- app/src/main/res/layout/app_list_item.xml | 23 ++++++++ app/src/main/res/layout/autofill_recycler_view.xml | 7 +++ 5 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 app/src/main/res/layout/app_list_item.xml (limited to 'app/src/main/AndroidManifest.xml') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b3600c53..3ddc04e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,7 +55,10 @@ + android:parentActivityName=".PasswordStore"> + diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java index 5d8fac5e..5c42ab8d 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java @@ -7,11 +7,19 @@ import android.content.IntentSender; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.MatrixCursor; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.SearchView; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; import com.zeapo.pwdstore.R; @@ -48,9 +56,15 @@ public class AutofillActivity extends AppCompatActivity { return; } // otherwise if called from settings - final PackageManager pm = getPackageManager(); - List allApps = pm.getInstalledApplications(0); + setContentView(R.layout.autofill_recycler_view); + recyclerView = (RecyclerView) findViewById(R.id.autofill_recycler); + + layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + // apps for which the user has custom settings should be in the recycler + final PackageManager pm = getPackageManager(); + final List allApps = pm.getInstalledApplications(0); SharedPreferences prefs = getSharedPreferences("autofill", Context.MODE_PRIVATE); Map prefApps = prefs.getAll(); @@ -60,18 +74,54 @@ public class AutofillActivity extends AppCompatActivity { apps.add(applicationInfo); } } + recyclerAdapter = new AutofillRecyclerAdapter(apps, pm); + recyclerView.setAdapter(recyclerAdapter); - setContentView(R.layout.autofill_recycler_view); - recyclerView = (RecyclerView) findViewById(R.id.autofill_recycler); + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + final SearchView searchView = (SearchView) findViewById(R.id.app_search); - layoutManager = new LinearLayoutManager(this); - recyclerView.setLayoutManager(layoutManager); + // create search suggestions of apps with icons & names + final SimpleCursorAdapter.ViewBinder viewBinder = new SimpleCursorAdapter.ViewBinder() { + @Override + public boolean setViewValue(View view, Cursor cursor, int columnIndex) { + if (view instanceof TextView) { + ((TextView) view).setText(cursor.getString(columnIndex)); + } else if (view instanceof ImageView) { + try { + ((ImageView) view).setImageDrawable(pm.getApplicationIcon(cursor.getString(columnIndex))); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return false; + } + } + return true; + } + }; + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } - recyclerAdapter = new AutofillRecyclerAdapter(apps, pm); - recyclerView.setAdapter(recyclerAdapter); + @Override + public boolean onQueryTextChange(String newText) { + // should be a better/faster way to do this? + MatrixCursor matrixCursor = new MatrixCursor(new String[]{"_id", "package", "label"}); + for (ApplicationInfo applicationInfo : allApps) { + if (applicationInfo.loadLabel(pm).toString().toLowerCase().contains(newText.toLowerCase())) { + matrixCursor.addRow(new Object[]{0, applicationInfo.packageName, applicationInfo.loadLabel(pm)}); + } + } + SimpleCursorAdapter simpleCursorAdapter = new SimpleCursorAdapter(AutofillActivity.this + , R.layout.app_list_item, matrixCursor, new String[]{"package", "label"} + , new int[]{android.R.id.icon1, android.R.id.text1}, 0); + simpleCursorAdapter.setViewBinder(viewBinder); + searchView.setSuggestionsAdapter(simpleCursorAdapter); + return false; + } + }); setTitle("Autofill Apps"); - } @Override diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java index 6188ef6d..f3108733 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java @@ -60,7 +60,7 @@ public class AutofillService extends AccessibilityService { serviceConnection.bindToService(); settings = PreferenceManager.getDefaultSharedPreferences(this); } - + // TODO handle CLICKS and change search/search results (just use first result) @Override public void onAccessibilityEvent(AccessibilityEvent event) { // if returning to the source app from a successful AutofillActivity diff --git a/app/src/main/res/layout/app_list_item.xml b/app/src/main/res/layout/app_list_item.xml new file mode 100644 index 00000000..7df28cc6 --- /dev/null +++ b/app/src/main/res/layout/app_list_item.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/autofill_recycler_view.xml b/app/src/main/res/layout/autofill_recycler_view.xml index 27afa77d..98ff779d 100644 --- a/app/src/main/res/layout/autofill_recycler_view.xml +++ b/app/src/main/res/layout/autofill_recycler_view.xml @@ -4,6 +4,13 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + Date: Mon, 10 Aug 2015 13:28:15 -0400 Subject: Move preferences to its own activity, leaving a "blank" openkeychain api one --- app/src/main/AndroidManifest.xml | 7 ++ .../java/com/zeapo/pwdstore/UserPreference.java | 6 +- .../zeapo/pwdstore/autofill/AutofillActivity.java | 127 ------------------- .../zeapo/pwdstore/autofill/AutofillFragment.java | 4 +- .../autofill/AutofillPreferenceActivity.java | 137 ++++++++++++++++++++- .../pwdstore/autofill/AutofillRecyclerAdapter.java | 4 +- 6 files changed, 150 insertions(+), 135 deletions(-) (limited to 'app/src/main/AndroidManifest.xml') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3ddc04e8..a6ffa791 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,6 +61,13 @@ android:value="com.zeapo.pwdstore.PasswordStore" /> + + android:parentActivityName=".PasswordStore"> + + + + diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java index e2a9e5a7..cb2d2881 100644 --- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.java +++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.java @@ -17,7 +17,7 @@ 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.AutofillActivity; +import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity; import com.zeapo.pwdstore.crypto.PgpHandler; import com.zeapo.pwdstore.git.GitActivity; import com.zeapo.pwdstore.utils.PasswordRepository; @@ -187,9 +187,9 @@ public class UserPreference extends AppCompatActivity { findPreference("autofill_apps").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { - Intent intent = new Intent(callingActivity, AutofillActivity.class); + Intent intent = new Intent(callingActivity, AutofillPreferenceActivity.class); startActivity(intent); - return false; + return true; } }); } diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java index 2d538504..5a097d07 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillActivity.java @@ -1,48 +1,22 @@ package com.zeapo.pwdstore.autofill; -import android.app.DialogFragment; import android.app.PendingIntent; -import android.content.Context; import android.content.Intent; import android.content.IntentSender; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.database.MatrixCursor; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.util.Log; -import android.view.View; -import android.view.WindowManager; -import android.widget.ImageView; -import android.widget.SearchView; -import android.widget.SimpleCursorAdapter; -import android.widget.TextView; - -import com.zeapo.pwdstore.R; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; // blank activity started by service for calling startIntentSenderForResult public class AutofillActivity extends AppCompatActivity { public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913; - private RecyclerView recyclerView; - AutofillRecyclerAdapter recyclerAdapter; // let fragment have access - private RecyclerView.LayoutManager layoutManager; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); Bundle extras = intent.getExtras(); - // if called by service just for startIntentSenderForResult if (extras != null) { try { PendingIntent pi = intent.getExtras().getParcelable("pending_intent"); @@ -54,108 +28,7 @@ public class AutofillActivity extends AppCompatActivity { } catch (IntentSender.SendIntentException e) { Log.e(AutofillService.Constants.TAG, "SendIntentException", e); } - return; - } - // otherwise if called from settings - setContentView(R.layout.autofill_recycler_view); - recyclerView = (RecyclerView) findViewById(R.id.autofill_recycler); - - layoutManager = new LinearLayoutManager(this); - recyclerView.setLayoutManager(layoutManager); - - // apps for which the user has custom settings should be in the recycler - final PackageManager pm = getPackageManager(); - SharedPreferences prefs - = getSharedPreferences("autofill", Context.MODE_PRIVATE); - Map prefApps = prefs.getAll(); - ArrayList apps = new ArrayList<>(); - for (String packageName : prefApps.keySet()) { - try { - apps.add(pm.getApplicationInfo(packageName, 0)); - } catch (PackageManager.NameNotFoundException e) { - // remove invalid entries (from uninstalled apps?) - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(packageName).apply(); - } } - recyclerAdapter = new AutofillRecyclerAdapter(apps, pm, this); - recyclerView.setAdapter(recyclerAdapter); - - // show the search bar by default but don't open the keyboard - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); - final SearchView searchView = (SearchView) findViewById(R.id.app_search); - searchView.clearFocus(); - - // create search suggestions of apps with icons & names - final SimpleCursorAdapter.ViewBinder viewBinder = new SimpleCursorAdapter.ViewBinder() { - @Override - public boolean setViewValue(View view, Cursor cursor, int columnIndex) { - if (view instanceof TextView) { - ((TextView) view).setText(cursor.getString(columnIndex)); - } else if (view instanceof ImageView) { - try { - ((ImageView) view).setImageDrawable(pm.getApplicationIcon(cursor.getString(columnIndex))); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - return false; - } - } - return true; - } - }; - - final List allApps = pm.getInstalledApplications(0); - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - return false; - } - - @Override - public boolean onQueryTextChange(String newText) { - // should be a better/faster way to do this? - MatrixCursor matrixCursor = new MatrixCursor(new String[]{"_id", "package", "label"}); - for (ApplicationInfo applicationInfo : allApps) { - // exclude apps that already have settings; the search is just for adding - if (applicationInfo.loadLabel(pm).toString().toLowerCase().contains(newText.toLowerCase()) - && !recyclerAdapter.contains(applicationInfo.packageName)) { - matrixCursor.addRow(new Object[]{0, applicationInfo.packageName, applicationInfo.loadLabel(pm)}); - } - } - SimpleCursorAdapter simpleCursorAdapter = new SimpleCursorAdapter(AutofillActivity.this - , R.layout.app_list_item, matrixCursor, new String[]{"package", "label"} - , new int[]{android.R.id.icon1, android.R.id.text1}, 0); - simpleCursorAdapter.setViewBinder(viewBinder); - searchView.setSuggestionsAdapter(simpleCursorAdapter); - return false; - } - }); - - searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { - @Override - public boolean onSuggestionSelect(int position) { - return false; - } - - @Override - public boolean onSuggestionClick(int position) { - Cursor cursor = searchView.getSuggestionsAdapter().getCursor(); - String packageName = cursor.getString(1); - String appName = cursor.getString(2); - - // similar to what happens in ViewHolder.onClick but position -1 - DialogFragment df = new AutofillFragment(); - Bundle args = new Bundle(); - args.putString("packageName", packageName); - args.putString("appName", appName); - args.putInt("position", -1); - df.setArguments(args); - df.show(getFragmentManager(), "autofill_dialog"); - return false; - } - }); - - setTitle("Autofill Apps"); } @Override diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java index c1d75acd..b85dec6a 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillFragment.java @@ -25,9 +25,9 @@ public class AutofillFragment extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - // this fragment is only created from the settings page (AutofillActivity) + // this fragment is only created from the settings page (AutofillPreferenceActivity) // need to interact with the recyclerAdapter which is a member of activity - final AutofillActivity callingActivity = (AutofillActivity) getActivity(); + final AutofillPreferenceActivity callingActivity = (AutofillPreferenceActivity) getActivity(); LayoutInflater inflater = callingActivity.getLayoutInflater(); final View view = inflater.inflate(R.layout.fragment_autofill, null); diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java index 6f212149..9dac9a07 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java @@ -1,4 +1,139 @@ package com.zeapo.pwdstore.autofill; -public class AutofillPreferenceActivity { +import android.app.DialogFragment; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.SearchView; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; + +import com.zeapo.pwdstore.R; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class AutofillPreferenceActivity extends AppCompatActivity { + + private RecyclerView recyclerView; + AutofillRecyclerAdapter recyclerAdapter; // let fragment have access + private RecyclerView.LayoutManager layoutManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // otherwise if called from settings + setContentView(R.layout.autofill_recycler_view); + recyclerView = (RecyclerView) findViewById(R.id.autofill_recycler); + + layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + + // apps for which the user has custom settings should be in the recycler + final PackageManager pm = getPackageManager(); + SharedPreferences prefs + = getSharedPreferences("autofill", Context.MODE_PRIVATE); + Map prefApps = prefs.getAll(); + ArrayList apps = new ArrayList<>(); + for (String packageName : prefApps.keySet()) { + try { + apps.add(pm.getApplicationInfo(packageName, 0)); + } catch (PackageManager.NameNotFoundException e) { + // remove invalid entries (from uninstalled apps?) + SharedPreferences.Editor editor = prefs.edit(); + editor.remove(packageName).apply(); + } + } + recyclerAdapter = new AutofillRecyclerAdapter(apps, pm, this); + recyclerView.setAdapter(recyclerAdapter); + + // show the search bar by default but don't open the keyboard + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + final SearchView searchView = (SearchView) findViewById(R.id.app_search); + searchView.clearFocus(); + + // create search suggestions of apps with icons & names + final SimpleCursorAdapter.ViewBinder viewBinder = new SimpleCursorAdapter.ViewBinder() { + @Override + public boolean setViewValue(View view, Cursor cursor, int columnIndex) { + if (view instanceof TextView) { + ((TextView) view).setText(cursor.getString(columnIndex)); + } else if (view instanceof ImageView) { + try { + ((ImageView) view).setImageDrawable(pm.getApplicationIcon(cursor.getString(columnIndex))); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return false; + } + } + return true; + } + }; + + final List allApps = pm.getInstalledApplications(0); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + // should be a better/faster way to do this? + MatrixCursor matrixCursor = new MatrixCursor(new String[]{"_id", "package", "label"}); + for (ApplicationInfo applicationInfo : allApps) { + // exclude apps that already have settings; the search is just for adding + if (applicationInfo.loadLabel(pm).toString().toLowerCase().contains(newText.toLowerCase()) + && !recyclerAdapter.contains(applicationInfo.packageName)) { + matrixCursor.addRow(new Object[]{0, applicationInfo.packageName, applicationInfo.loadLabel(pm)}); + } + } + SimpleCursorAdapter simpleCursorAdapter = new SimpleCursorAdapter(AutofillPreferenceActivity.this + , R.layout.app_list_item, matrixCursor, new String[]{"package", "label"} + , new int[]{android.R.id.icon1, android.R.id.text1}, 0); + simpleCursorAdapter.setViewBinder(viewBinder); + searchView.setSuggestionsAdapter(simpleCursorAdapter); + return false; + } + }); + + searchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { + @Override + public boolean onSuggestionSelect(int position) { + return false; + } + + @Override + public boolean onSuggestionClick(int position) { + Cursor cursor = searchView.getSuggestionsAdapter().getCursor(); + String packageName = cursor.getString(1); + String appName = cursor.getString(2); + + // similar to what happens in ViewHolder.onClick but position -1 + DialogFragment df = new AutofillFragment(); + Bundle args = new Bundle(); + args.putString("packageName", packageName); + args.putString("appName", appName); + args.putInt("position", -1); + df.setArguments(args); + df.show(getFragmentManager(), "autofill_dialog"); + return false; + } + }); + + setTitle("Autofill Apps"); + } } diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java index eff94c51..b43f5d6e 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillRecyclerAdapter.java @@ -20,7 +20,7 @@ import java.util.ArrayList; public class AutofillRecyclerAdapter extends RecyclerView.Adapter { private ArrayList apps; private PackageManager pm; - private AutofillActivity activity; + private AutofillPreferenceActivity activity; public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { public View view; @@ -50,7 +50,7 @@ public class AutofillRecyclerAdapter extends RecyclerView.Adapter apps, PackageManager pm, AutofillActivity activity) { + public AutofillRecyclerAdapter(ArrayList apps, PackageManager pm, AutofillPreferenceActivity activity) { this.apps = apps; this.pm = pm; this.activity = activity; -- cgit v1.2.3 From ac533d83aab29005f70c97ed939a6d6f2e13aa16 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Thu, 13 Aug 2015 21:16:10 -0400 Subject: Preference activity up & back behaviour. Especially when opened with 'Settings' dialog button --- app/src/main/AndroidManifest.xml | 8 +++---- .../autofill/AutofillPreferenceActivity.java | 26 ++++++++++++++++++++-- .../zeapo/pwdstore/autofill/AutofillService.java | 2 +- 3 files changed, 29 insertions(+), 7 deletions(-) (limited to 'app/src/main/AndroidManifest.xml') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a6ffa791..ecd106cd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,18 +54,18 @@ android:resource="@xml/autofill_config" /> - + + android:value="com.zeapo.pwdstore.PasswordStore" /> - + + android:value="com.zeapo.pwdstore.PasswordStore" /> diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java index 91131956..6f507fa5 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillPreferenceActivity.java @@ -5,6 +5,8 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; 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; @@ -26,11 +28,12 @@ public class AutofillPreferenceActivity extends AppCompatActivity { AutofillRecyclerAdapter recyclerAdapter; // let fragment have access private RecyclerView.LayoutManager layoutManager; + private boolean recreate; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // otherwise if called from settings setContentView(R.layout.autofill_recycler_view); recyclerView = (RecyclerView) findViewById(R.id.autofill_recycler); @@ -53,8 +56,10 @@ public class AutofillPreferenceActivity extends AppCompatActivity { setTitle("Autofill Apps"); + 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")); } @@ -94,6 +99,24 @@ public class AutofillPreferenceActivity extends AppCompatActivity { }); 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(); @@ -103,6 +126,5 @@ public class AutofillPreferenceActivity extends AppCompatActivity { args.putInt("position", recyclerAdapter.getPosition(packageName)); df.setArguments(args); df.show(getFragmentManager(), "autofill_dialog"); - // TODO if called from dialog 'Settings' button, should activity finish at OK? } } diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java index 1b8ff740..688e22fc 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java @@ -158,7 +158,7 @@ public class AutofillService extends AccessibilityService { public void onClick(DialogInterface dialog, int which) { // 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.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.putExtra("packageName", info.getPackageName()); intent.putExtra("appName", appName); startActivity(intent); -- cgit v1.2.3 From b1807197f85c4d6ceccc3ca23654bb184f9caa13 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Fri, 14 Aug 2015 14:12:02 -0400 Subject: Fix crash on first start & use documentLaunchMode for nicer openkeychain-api behaviour (5+ only) --- app/src/main/AndroidManifest.xml | 4 +++- .../java/com/zeapo/pwdstore/autofill/AutofillService.java | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) (limited to 'app/src/main/AndroidManifest.xml') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ecd106cd..0789fd33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,7 +55,9 @@ + android:parentActivityName=".PasswordStore" + android:documentLaunchMode="intoExisting" + android:excludeFromRecents="true"> diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java index 688e22fc..1e64e8c8 100644 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java +++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java @@ -130,11 +130,17 @@ public class AutofillService extends AccessibilityService { 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<>(); @@ -177,9 +183,6 @@ public class AutofillService extends AccessibilityService { private ArrayList recursiveFilter(String filter, File dir) { ArrayList items = new ArrayList<>(); - if (!PasswordRepository.isInitialized()) { - PasswordRepository.initialize(this); - } ArrayList passwordItems = dir == null ? PasswordRepository.getPasswords(PasswordRepository.getRepositoryDirectory(this)) : PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(this)); @@ -252,8 +255,7 @@ public class AutofillService extends AccessibilityService { 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.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.putExtra("pending_intent", pi); startActivity(intent); break; -- cgit v1.2.3