Support pasting username with autofill, fixes #192 (#321)

* Support pasting username with autofill, fixes #192

The workflow for pasting usernames is as follows:

1. Select password field
2. Select password store entry with username and paste it
3. Select any other editable field
4. Paste username

* Show toast when username is available for pasting
This commit is contained in:
Felix Bechstein 2017-07-26 09:04:45 +02:00 committed by Mohamed Zenadi
parent d1ad306c1b
commit 2f75f99108
3 changed files with 129 additions and 48 deletions

View file

@ -1,6 +1,7 @@
package com.zeapo.pwdstore.autofill;
import android.accessibilityservice.AccessibilityService;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipboardManager;
@ -58,16 +59,20 @@ public class AutofillService extends AccessibilityService {
private boolean ignoreActionFocus = false;
private String webViewTitle = null;
private String webViewURL = null;
private PasswordEntry lastPassword;
private long lastPasswordMaxDate;
public final class Constants {
public static final String TAG = "Keychain";
final class Constants {
static final String TAG = "Keychain";
}
public static AutofillService getInstance() {
return instance;
}
public void setResultData(Intent data) { resultData = data; }
public void setResultData(Intent data) {
resultData = data;
}
public void setPickedPassword(String path) {
items.add(new File(PasswordRepository.getRepositoryDirectory(getApplicationContext()) + "/" + path + ".gpg"));
@ -96,6 +101,11 @@ public class AutofillService extends AccessibilityService {
return;
}
// remove stored password from cache
if (lastPassword != null && System.currentTimeMillis() > lastPasswordMaxDate) {
lastPassword = null;
}
// if returning to the source app from a successful AutofillActivity
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
&& event.getPackageName() != null && event.getPackageName().equals(packageName)
@ -140,15 +150,25 @@ public class AutofillService extends AccessibilityService {
}
}
// nothing to do if not password field focus, field is keychain app
if (!event.isPassword()
|| event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
// nothing to do if field is keychain app or system ui
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|| event.getPackageName() != null && event.getPackageName().equals("org.sufficientlysecure.keychain")
|| event.getPackageName() != null && event.getPackageName().equals("com.android.systemui")) {
dismissDialog(event);
return;
}
if (!event.isPassword()) {
if (lastPassword != null && event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && event.getSource().isEditable()) {
showPasteUsernameDialog(event.getSource(), lastPassword);
return;
} else {
// nothing to do if not password field focus
dismissDialog(event);
return;
}
}
if (dialog != null && dialog.isShowing()) {
// the current dialog must belong to this window; ignore clicks on this password field
// why handle clicks at all then? some cases e.g. Paypal there is no initial focus event
@ -220,8 +240,9 @@ public class AutofillService extends AccessibilityService {
if (items.isEmpty() && !settings.getBoolean("autofill_always", false)) {
return;
}
showDialog(packageName, appName, isWeb);
showSelectPasswordDialog(packageName, appName, isWeb);
}
private String searchWebView(AccessibilityNodeInfo source) {
return searchWebView(source, 10);
}
@ -282,13 +303,16 @@ public class AutofillService extends AccessibilityService {
prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
preference = defValue;
if (webViewURL != null) {
final String webViewUrlLowerCase = webViewURL.toLowerCase();
Map<String, ?> prefsMap = prefs.getAll();
for (String key : prefsMap.keySet()) {
// for websites unlike apps there can be blank preference of "" which
// means use default, so ignore it.
if ((webViewURL.toLowerCase().contains(key.toLowerCase()) || key.toLowerCase().contains(webViewURL.toLowerCase()))
&& !prefs.getString(key, null).equals("")) {
preference = prefs.getString(key, null);
final String value = prefs.getString(key, null);
final String keyLowerCase = key.toLowerCase();
if (value != null && !value.equals("")
&& (webViewUrlLowerCase.contains(keyLowerCase) || keyLowerCase.contains(webViewUrlLowerCase))) {
preference = value;
settingsURL = key;
}
}
@ -374,7 +398,44 @@ public class AutofillService extends AccessibilityService {
return items;
}
private void showDialog(final String packageName, final String appName, final boolean isWeb) {
private void showPasteUsernameDialog(final AccessibilityNodeInfo node, final PasswordEntry password) {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int which) {
dialog.dismiss();
dialog = null;
}
});
builder.setPositiveButton(R.string.autofill_paste, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int which) {
pasteText(node, password.getUsername());
dialog.dismiss();
dialog = null;
}
});
builder.setMessage(getString(R.string.autofill_paste_username, password.getUsername()));
dialog = builder.create();
//noinspection ConstantConditions
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.show();
}
private void showSelectPasswordDialog(final String packageName, final String appName, final boolean isWeb) {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
@Override
@ -428,6 +489,7 @@ public class AutofillService extends AccessibilityService {
});
dialog = builder.create();
//noinspection ConstantConditions
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
@ -451,6 +513,7 @@ public class AutofillService extends AccessibilityService {
public void onBound(IOpenPgpService2 service) {
decryptAndVerify();
}
@Override
public void onError(Exception e) {
e.printStackTrace();
@ -494,32 +557,15 @@ public class AutofillService extends AccessibilityService {
case OpenPgpApi.RESULT_CODE_SUCCESS: {
try {
final PasswordEntry entry = new PasswordEntry(os);
pasteText(info, entry.getPassword());
// if the user focused on something else, take focus back
// but this will open another dialog...hack to ignore this
// & need to ensure performAction correct (i.e. what is info now?)
ignoreActionFocus = info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Bundle args = new Bundle();
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
entry.getPassword());
info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
} else {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("autofill_pm", entry.getPassword());
clipboard.setPrimaryClip(clip);
info.performAction(AccessibilityNodeInfo.ACTION_PASTE);
clip = ClipData.newPlainText("autofill_pm", "");
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);
// save password entry for pasting the username as well
if (entry.hasUsername()) {
lastPassword = entry;
final int ttl = Integer.parseInt(settings.getString("general_show_time", "45"));
Toast.makeText(this, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show();
lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L;
}
}
}
info.recycle();
} catch (UnsupportedEncodingException e) {
Log.e(Constants.TAG, "UnsupportedEncodingException", e);
}
@ -546,4 +592,32 @@ public class AutofillService extends AccessibilityService {
}
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private void pasteText(final AccessibilityNodeInfo node, final String text) {
// if the user focused on something else, take focus back
// but this will open another dialog...hack to ignore this
// & need to ensure performAction correct (i.e. what is info now?)
ignoreActionFocus = node.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, text);
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
} else {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("autofill_pm", text);
clipboard.setPrimaryClip(clip);
node.performAction(AccessibilityNodeInfo.ACTION_PASTE);
clip = ClipData.newPlainText("autofill_pm", "");
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);
}
}
}
node.recycle();
}
}

View file

@ -169,6 +169,11 @@
<string name="show_extra_content_pref_title">Zeige weiteren Inhalt</string>
<string name="show_extra_content_pref_summary">Soll weiterer Inhalt sichtbar sein?</string>
<string name="pwd_generate_button">Generieren</string>
<string name="no_repo_selected">Kein externes Repository ausgewählt</string>
<string name="edit_commit_text">[ANDROID PwdStore] Edit &#160;</string>
<string name="send_plaintext_password_to">Passwort senden als Nur-Text mit behilfe von…</string>
<string name="show_password">Password wiedergeben</string>
<string name="repository_uri">Repository URI</string>
<!-- Autofill -->
<string name="autofill_description">Füge das Passwort automatisch in Apps ein (Autofill). Funktioniert nur unter Android 4.3 und höher. Dies basiert nicht auf der Zwischenablage für Android 5.0 oder höher.</string>
@ -180,8 +185,7 @@
<string name="autofill_apps_delete">Löschen</string>
<string name="autofill_pick">Auswählen…</string>
<string name="autofill_pick_and_match">Auswählen und merken…</string>
<string name="no_repo_selected">Kein externes Repository ausgewählt</string>
<string name="edit_commit_text">[ANDROID PwdStore] Edit &#160;</string>
<string name="send_plaintext_password_to">Passwort senden als Nur-Text mit behilfe von…</string>
<string name="show_password">Password wiedergeben</string>
<string name="autofill_paste">Einfügen</string>
<string name="autofill_paste_username">Benutzername einfügen?\n\n%s</string>
<string name="autofill_toast_username">Wähle ein editierbares Feld um den Benutzernamen einzufügen.\nDer Benutzername ist für %d Sekunden verfügbar.</string>
</resources>

View file

@ -177,6 +177,11 @@
<string name="show_extra_content_pref_title">Show extra content</string>
<string name="show_extra_content_pref_summary">Control the visibility of the extra content once decrypted</string>
<string name="pwd_generate_button">Generate</string>
<string name="refresh_list">Refresh list</string>
<string name="no_repo_selected">No external repository selected</string>
<string name="send_plaintext_password_to">Send password as plaintext using…</string>
<string name="show_password">Show password</string>
<string name="repository_uri">Repository URI</string>
<!-- Autofill -->
<string name="autofill_description">Autofills password fields in apps. Only works for Android versions 4.3 and up. Does not rely on the clipboard for Android versions 5.0 and up.</string>
@ -188,9 +193,7 @@
<string name="autofill_apps_delete">Delete</string>
<string name="autofill_pick">Pick…</string>
<string name="autofill_pick_and_match">Pick and match…</string>
<string name="refresh_list">Refresh list</string>
<string name="no_repo_selected">No external repository selected</string>
<string name="send_plaintext_password_to">Send password as plaintext using…</string>
<string name="show_password">Show password</string>
<string name="repository_uri">Repository URI</string>
<string name="autofill_paste">Paste</string>
<string name="autofill_paste_username">Paste username?\n\n%s</string>
<string name="autofill_toast_username">Select an editable field to past the username.\nUsername is available for %d seconds.</string>
</resources>