Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Commit 2f75f99

Browse files
felixbzeapo
authored andcommitted
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
1 parent d1ad306 commit 2f75f99

File tree

3 files changed

+130
-49
lines changed

3 files changed

+130
-49
lines changed

app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java

Lines changed: 114 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.zeapo.pwdstore.autofill;
22

33
import android.accessibilityservice.AccessibilityService;
4+
import android.annotation.TargetApi;
45
import android.app.PendingIntent;
56
import android.content.ClipData;
67
import android.content.ClipboardManager;
@@ -58,16 +59,20 @@ public class AutofillService extends AccessibilityService {
5859
private boolean ignoreActionFocus = false;
5960
private String webViewTitle = null;
6061
private String webViewURL = null;
62+
private PasswordEntry lastPassword;
63+
private long lastPasswordMaxDate;
6164

62-
public final class Constants {
63-
public static final String TAG = "Keychain";
65+
final class Constants {
66+
static final String TAG = "Keychain";
6467
}
6568

6669
public static AutofillService getInstance() {
6770
return instance;
6871
}
6972

70-
public void setResultData(Intent data) { resultData = data; }
73+
public void setResultData(Intent data) {
74+
resultData = data;
75+
}
7176

7277
public void setPickedPassword(String path) {
7378
items.add(new File(PasswordRepository.getRepositoryDirectory(getApplicationContext()) + "/" + path + ".gpg"));
@@ -96,6 +101,11 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
96101
return;
97102
}
98103

104+
// remove stored password from cache
105+
if (lastPassword != null && System.currentTimeMillis() > lastPasswordMaxDate) {
106+
lastPassword = null;
107+
}
108+
99109
// if returning to the source app from a successful AutofillActivity
100110
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
101111
&& event.getPackageName() != null && event.getPackageName().equals(packageName)
@@ -107,9 +117,9 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
107117
// or if page changes in chrome
108118
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
109119
|| (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
110-
&& event.getPackageName() != null
111-
&& (event.getPackageName().equals("com.android.chrome")
112-
|| event.getPackageName().equals("com.android.browser")))) {
120+
&& event.getPackageName() != null
121+
&& (event.getPackageName().equals("com.android.chrome")
122+
|| event.getPackageName().equals("com.android.browser")))) {
113123
// there is a chance for getRootInActiveWindow() to return null at any time. save it.
114124
try {
115125
AccessibilityNodeInfo root = getRootInActiveWindow();
@@ -140,15 +150,25 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
140150
}
141151
}
142152

143-
// nothing to do if not password field focus, field is keychain app
144-
if (!event.isPassword()
145-
|| event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
153+
// nothing to do if field is keychain app or system ui
154+
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
146155
|| event.getPackageName() != null && event.getPackageName().equals("org.sufficientlysecure.keychain")
147156
|| event.getPackageName() != null && event.getPackageName().equals("com.android.systemui")) {
148157
dismissDialog(event);
149158
return;
150159
}
151160

161+
if (!event.isPassword()) {
162+
if (lastPassword != null && event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && event.getSource().isEditable()) {
163+
showPasteUsernameDialog(event.getSource(), lastPassword);
164+
return;
165+
} else {
166+
// nothing to do if not password field focus
167+
dismissDialog(event);
168+
return;
169+
}
170+
}
171+
152172
if (dialog != null && dialog.isShowing()) {
153173
// the current dialog must belong to this window; ignore clicks on this password field
154174
// why handle clicks at all then? some cases e.g. Paypal there is no initial focus event
@@ -220,8 +240,9 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
220240
if (items.isEmpty() && !settings.getBoolean("autofill_always", false)) {
221241
return;
222242
}
223-
showDialog(packageName, appName, isWeb);
243+
showSelectPasswordDialog(packageName, appName, isWeb);
224244
}
245+
225246
private String searchWebView(AccessibilityNodeInfo source) {
226247
return searchWebView(source, 10);
227248
}
@@ -282,13 +303,16 @@ private String setWebMatchingPasswords(String webViewTitle, String webViewURL) {
282303
prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
283304
preference = defValue;
284305
if (webViewURL != null) {
306+
final String webViewUrlLowerCase = webViewURL.toLowerCase();
285307
Map<String, ?> prefsMap = prefs.getAll();
286308
for (String key : prefsMap.keySet()) {
287309
// for websites unlike apps there can be blank preference of "" which
288310
// means use default, so ignore it.
289-
if ((webViewURL.toLowerCase().contains(key.toLowerCase()) || key.toLowerCase().contains(webViewURL.toLowerCase()))
290-
&& !prefs.getString(key, null).equals("")) {
291-
preference = prefs.getString(key, null);
311+
final String value = prefs.getString(key, null);
312+
final String keyLowerCase = key.toLowerCase();
313+
if (value != null && !value.equals("")
314+
&& (webViewUrlLowerCase.contains(keyLowerCase) || keyLowerCase.contains(webViewUrlLowerCase))) {
315+
preference = value;
292316
settingsURL = key;
293317
}
294318
}
@@ -374,7 +398,44 @@ private ArrayList<File> searchPasswords(File path, String appName) {
374398
return items;
375399
}
376400

377-
private void showDialog(final String packageName, final String appName, final boolean isWeb) {
401+
private void showPasteUsernameDialog(final AccessibilityNodeInfo node, final PasswordEntry password) {
402+
if (dialog != null) {
403+
dialog.dismiss();
404+
dialog = null;
405+
}
406+
407+
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
408+
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
409+
@Override
410+
public void onClick(DialogInterface d, int which) {
411+
dialog.dismiss();
412+
dialog = null;
413+
}
414+
});
415+
builder.setPositiveButton(R.string.autofill_paste, new DialogInterface.OnClickListener() {
416+
@Override
417+
public void onClick(DialogInterface d, int which) {
418+
pasteText(node, password.getUsername());
419+
dialog.dismiss();
420+
dialog = null;
421+
}
422+
});
423+
builder.setMessage(getString(R.string.autofill_paste_username, password.getUsername()));
424+
425+
dialog = builder.create();
426+
//noinspection ConstantConditions
427+
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
428+
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
429+
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
430+
dialog.show();
431+
}
432+
433+
private void showSelectPasswordDialog(final String packageName, final String appName, final boolean isWeb) {
434+
if (dialog != null) {
435+
dialog.dismiss();
436+
dialog = null;
437+
}
438+
378439
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
379440
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
380441
@Override
@@ -410,7 +471,7 @@ public void onClick(DialogInterface dialog, int which) {
410471
lastWhichItem = which;
411472
if (which < items.size()) {
412473
bindDecryptAndVerify();
413-
} else if (which == items.size()){
474+
} else if (which == items.size()) {
414475
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
415476
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
416477
intent.putExtra("pick", true);
@@ -428,6 +489,7 @@ public void onClick(DialogInterface dialog, int which) {
428489
});
429490

430491
dialog = builder.create();
492+
//noinspection ConstantConditions
431493
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
432494
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
433495
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
@@ -451,6 +513,7 @@ private class onBoundListener implements OpenPgpServiceConnection.OnBound {
451513
public void onBound(IOpenPgpService2 service) {
452514
decryptAndVerify();
453515
}
516+
454517
@Override
455518
public void onError(Exception e) {
456519
e.printStackTrace();
@@ -494,32 +557,15 @@ private void decryptAndVerify() {
494557
case OpenPgpApi.RESULT_CODE_SUCCESS: {
495558
try {
496559
final PasswordEntry entry = new PasswordEntry(os);
497-
498-
// if the user focused on something else, take focus back
499-
// but this will open another dialog...hack to ignore this
500-
// & need to ensure performAction correct (i.e. what is info now?)
501-
ignoreActionFocus = info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
502-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
503-
Bundle args = new Bundle();
504-
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
505-
entry.getPassword());
506-
info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
507-
} else {
508-
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
509-
ClipData clip = ClipData.newPlainText("autofill_pm", entry.getPassword());
510-
clipboard.setPrimaryClip(clip);
511-
info.performAction(AccessibilityNodeInfo.ACTION_PASTE);
512-
513-
clip = ClipData.newPlainText("autofill_pm", "");
514-
clipboard.setPrimaryClip(clip);
515-
if (settings.getBoolean("clear_clipboard_20x", false)) {
516-
for (int i = 0; i < 19; i++) {
517-
clip = ClipData.newPlainText(String.valueOf(i), String.valueOf(i));
518-
clipboard.setPrimaryClip(clip);
519-
}
520-
}
560+
pasteText(info, entry.getPassword());
561+
562+
// save password entry for pasting the username as well
563+
if (entry.hasUsername()) {
564+
lastPassword = entry;
565+
final int ttl = Integer.parseInt(settings.getString("general_show_time", "45"));
566+
Toast.makeText(this, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show();
567+
lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L;
521568
}
522-
info.recycle();
523569
} catch (UnsupportedEncodingException e) {
524570
Log.e(Constants.TAG, "UnsupportedEncodingException", e);
525571
}
@@ -546,4 +592,32 @@ private void decryptAndVerify() {
546592
}
547593
}
548594
}
595+
596+
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
597+
private void pasteText(final AccessibilityNodeInfo node, final String text) {
598+
// if the user focused on something else, take focus back
599+
// but this will open another dialog...hack to ignore this
600+
// & need to ensure performAction correct (i.e. what is info now?)
601+
ignoreActionFocus = node.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
602+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
603+
Bundle args = new Bundle();
604+
args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text);
605+
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
606+
} else {
607+
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
608+
ClipData clip = ClipData.newPlainText("autofill_pm", text);
609+
clipboard.setPrimaryClip(clip);
610+
node.performAction(AccessibilityNodeInfo.ACTION_PASTE);
611+
612+
clip = ClipData.newPlainText("autofill_pm", "");
613+
clipboard.setPrimaryClip(clip);
614+
if (settings.getBoolean("clear_clipboard_20x", false)) {
615+
for (int i = 0; i < 19; i++) {
616+
clip = ClipData.newPlainText(String.valueOf(i), String.valueOf(i));
617+
clipboard.setPrimaryClip(clip);
618+
}
619+
}
620+
}
621+
node.recycle();
622+
}
549623
}

app/src/main/res/values-de/strings.xml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@
169169
<string name="show_extra_content_pref_title">Zeige weiteren Inhalt</string>
170170
<string name="show_extra_content_pref_summary">Soll weiterer Inhalt sichtbar sein?</string>
171171
<string name="pwd_generate_button">Generieren</string>
172+
<string name="no_repo_selected">Kein externes Repository ausgewählt</string>
173+
<string name="edit_commit_text">[ANDROID PwdStore] Edit &#160;</string>
174+
<string name="send_plaintext_password_to">Passwort senden als Nur-Text mit behilfe von…</string>
175+
<string name="show_password">Password wiedergeben</string>
176+
<string name="repository_uri">Repository URI</string>
172177

173178
<!-- Autofill -->
174179
<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 @@
180185
<string name="autofill_apps_delete">Löschen</string>
181186
<string name="autofill_pick">Auswählen…</string>
182187
<string name="autofill_pick_and_match">Auswählen und merken…</string>
183-
<string name="no_repo_selected">Kein externes Repository ausgewählt</string>
184-
<string name="edit_commit_text">[ANDROID PwdStore] Edit &#160;</string>
185-
<string name="send_plaintext_password_to">Passwort senden als Nur-Text mit behilfe von…</string>
186-
<string name="show_password">Password wiedergeben</string>
188+
<string name="autofill_paste">Einfügen</string>
189+
<string name="autofill_paste_username">Benutzername einfügen?\n\n%s</string>
190+
<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>
187191
</resources>

app/src/main/res/values/strings.xml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@
177177
<string name="show_extra_content_pref_title">Show extra content</string>
178178
<string name="show_extra_content_pref_summary">Control the visibility of the extra content once decrypted</string>
179179
<string name="pwd_generate_button">Generate</string>
180+
<string name="refresh_list">Refresh list</string>
181+
<string name="no_repo_selected">No external repository selected</string>
182+
<string name="send_plaintext_password_to">Send password as plaintext using…</string>
183+
<string name="show_password">Show password</string>
184+
<string name="repository_uri">Repository URI</string>
180185

181186
<!-- Autofill -->
182187
<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 @@
188193
<string name="autofill_apps_delete">Delete</string>
189194
<string name="autofill_pick">Pick…</string>
190195
<string name="autofill_pick_and_match">Pick and match…</string>
191-
<string name="refresh_list">Refresh list</string>
192-
<string name="no_repo_selected">No external repository selected</string>
193-
<string name="send_plaintext_password_to">Send password as plaintext using…</string>
194-
<string name="show_password">Show password</string>
195-
<string name="repository_uri">Repository URI</string>
196+
<string name="autofill_paste">Paste</string>
197+
<string name="autofill_paste_username">Paste username?\n\n%s</string>
198+
<string name="autofill_toast_username">Select an editable field to past the username.\nUsername is available for %d seconds.</string>
196199
</resources>

0 commit comments

Comments
 (0)