* 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:
parent
d1ad306c1b
commit
2f75f99108
3 changed files with 129 additions and 48 deletions
|
@ -1,6 +1,7 @@
|
||||||
package com.zeapo.pwdstore.autofill;
|
package com.zeapo.pwdstore.autofill;
|
||||||
|
|
||||||
import android.accessibilityservice.AccessibilityService;
|
import android.accessibilityservice.AccessibilityService;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
import android.content.ClipboardManager;
|
import android.content.ClipboardManager;
|
||||||
|
@ -58,16 +59,20 @@ public class AutofillService extends AccessibilityService {
|
||||||
private boolean ignoreActionFocus = false;
|
private boolean ignoreActionFocus = false;
|
||||||
private String webViewTitle = null;
|
private String webViewTitle = null;
|
||||||
private String webViewURL = null;
|
private String webViewURL = null;
|
||||||
|
private PasswordEntry lastPassword;
|
||||||
|
private long lastPasswordMaxDate;
|
||||||
|
|
||||||
public final class Constants {
|
final class Constants {
|
||||||
public static final String TAG = "Keychain";
|
static final String TAG = "Keychain";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AutofillService getInstance() {
|
public static AutofillService getInstance() {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setResultData(Intent data) { resultData = data; }
|
public void setResultData(Intent data) {
|
||||||
|
resultData = data;
|
||||||
|
}
|
||||||
|
|
||||||
public void setPickedPassword(String path) {
|
public void setPickedPassword(String path) {
|
||||||
items.add(new File(PasswordRepository.getRepositoryDirectory(getApplicationContext()) + "/" + path + ".gpg"));
|
items.add(new File(PasswordRepository.getRepositoryDirectory(getApplicationContext()) + "/" + path + ".gpg"));
|
||||||
|
@ -96,6 +101,11 @@ public class AutofillService extends AccessibilityService {
|
||||||
return;
|
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 returning to the source app from a successful AutofillActivity
|
||||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
||||||
&& event.getPackageName() != null && event.getPackageName().equals(packageName)
|
&& event.getPackageName() != null && event.getPackageName().equals(packageName)
|
||||||
|
@ -107,9 +117,9 @@ public class AutofillService extends AccessibilityService {
|
||||||
// or if page changes in chrome
|
// or if page changes in chrome
|
||||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
||||||
|| (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
|| (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
||||||
&& event.getPackageName() != null
|
&& event.getPackageName() != null
|
||||||
&& (event.getPackageName().equals("com.android.chrome")
|
&& (event.getPackageName().equals("com.android.chrome")
|
||||||
|| event.getPackageName().equals("com.android.browser")))) {
|
|| event.getPackageName().equals("com.android.browser")))) {
|
||||||
// there is a chance for getRootInActiveWindow() to return null at any time. save it.
|
// there is a chance for getRootInActiveWindow() to return null at any time. save it.
|
||||||
try {
|
try {
|
||||||
AccessibilityNodeInfo root = getRootInActiveWindow();
|
AccessibilityNodeInfo root = getRootInActiveWindow();
|
||||||
|
@ -140,15 +150,25 @@ public class AutofillService extends AccessibilityService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// nothing to do if not password field focus, field is keychain app
|
// nothing to do if field is keychain app or system ui
|
||||||
if (!event.isPassword()
|
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
||||||
|| event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
|
||||||
|| event.getPackageName() != null && event.getPackageName().equals("org.sufficientlysecure.keychain")
|
|| event.getPackageName() != null && event.getPackageName().equals("org.sufficientlysecure.keychain")
|
||||||
|| event.getPackageName() != null && event.getPackageName().equals("com.android.systemui")) {
|
|| event.getPackageName() != null && event.getPackageName().equals("com.android.systemui")) {
|
||||||
dismissDialog(event);
|
dismissDialog(event);
|
||||||
return;
|
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()) {
|
if (dialog != null && dialog.isShowing()) {
|
||||||
// the current dialog must belong to this window; ignore clicks on this password field
|
// 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
|
// 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)) {
|
if (items.isEmpty() && !settings.getBoolean("autofill_always", false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showDialog(packageName, appName, isWeb);
|
showSelectPasswordDialog(packageName, appName, isWeb);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String searchWebView(AccessibilityNodeInfo source) {
|
private String searchWebView(AccessibilityNodeInfo source) {
|
||||||
return searchWebView(source, 10);
|
return searchWebView(source, 10);
|
||||||
}
|
}
|
||||||
|
@ -282,13 +303,16 @@ public class AutofillService extends AccessibilityService {
|
||||||
prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
|
||||||
preference = defValue;
|
preference = defValue;
|
||||||
if (webViewURL != null) {
|
if (webViewURL != null) {
|
||||||
|
final String webViewUrlLowerCase = webViewURL.toLowerCase();
|
||||||
Map<String, ?> prefsMap = prefs.getAll();
|
Map<String, ?> prefsMap = prefs.getAll();
|
||||||
for (String key : prefsMap.keySet()) {
|
for (String key : prefsMap.keySet()) {
|
||||||
// for websites unlike apps there can be blank preference of "" which
|
// for websites unlike apps there can be blank preference of "" which
|
||||||
// means use default, so ignore it.
|
// means use default, so ignore it.
|
||||||
if ((webViewURL.toLowerCase().contains(key.toLowerCase()) || key.toLowerCase().contains(webViewURL.toLowerCase()))
|
final String value = prefs.getString(key, null);
|
||||||
&& !prefs.getString(key, null).equals("")) {
|
final String keyLowerCase = key.toLowerCase();
|
||||||
preference = prefs.getString(key, null);
|
if (value != null && !value.equals("")
|
||||||
|
&& (webViewUrlLowerCase.contains(keyLowerCase) || keyLowerCase.contains(webViewUrlLowerCase))) {
|
||||||
|
preference = value;
|
||||||
settingsURL = key;
|
settingsURL = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -374,7 +398,44 @@ public class AutofillService extends AccessibilityService {
|
||||||
return items;
|
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);
|
AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
|
||||||
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
|
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -410,7 +471,7 @@ public class AutofillService extends AccessibilityService {
|
||||||
lastWhichItem = which;
|
lastWhichItem = which;
|
||||||
if (which < items.size()) {
|
if (which < items.size()) {
|
||||||
bindDecryptAndVerify();
|
bindDecryptAndVerify();
|
||||||
} else if (which == items.size()){
|
} else if (which == items.size()) {
|
||||||
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
|
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
intent.putExtra("pick", true);
|
intent.putExtra("pick", true);
|
||||||
|
@ -428,6 +489,7 @@ public class AutofillService extends AccessibilityService {
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog = builder.create();
|
dialog = builder.create();
|
||||||
|
//noinspection ConstantConditions
|
||||||
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
||||||
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
|
||||||
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||||
|
@ -451,6 +513,7 @@ public class AutofillService extends AccessibilityService {
|
||||||
public void onBound(IOpenPgpService2 service) {
|
public void onBound(IOpenPgpService2 service) {
|
||||||
decryptAndVerify();
|
decryptAndVerify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(Exception e) {
|
public void onError(Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
@ -494,32 +557,15 @@ public class AutofillService extends AccessibilityService {
|
||||||
case OpenPgpApi.RESULT_CODE_SUCCESS: {
|
case OpenPgpApi.RESULT_CODE_SUCCESS: {
|
||||||
try {
|
try {
|
||||||
final PasswordEntry entry = new PasswordEntry(os);
|
final PasswordEntry entry = new PasswordEntry(os);
|
||||||
|
pasteText(info, entry.getPassword());
|
||||||
|
|
||||||
// if the user focused on something else, take focus back
|
// save password entry for pasting the username as well
|
||||||
// but this will open another dialog...hack to ignore this
|
if (entry.hasUsername()) {
|
||||||
// & need to ensure performAction correct (i.e. what is info now?)
|
lastPassword = entry;
|
||||||
ignoreActionFocus = info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
|
final int ttl = Integer.parseInt(settings.getString("general_show_time", "45"));
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
Toast.makeText(this, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show();
|
||||||
Bundle args = new Bundle();
|
lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
info.recycle();
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
Log.e(Constants.TAG, "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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,6 +169,11 @@
|
||||||
<string name="show_extra_content_pref_title">Zeige weiteren Inhalt</string>
|
<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="show_extra_content_pref_summary">Soll weiterer Inhalt sichtbar sein?</string>
|
||||||
<string name="pwd_generate_button">Generieren</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  </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 -->
|
<!-- 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>
|
<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_apps_delete">Löschen</string>
|
||||||
<string name="autofill_pick">Auswählen…</string>
|
<string name="autofill_pick">Auswählen…</string>
|
||||||
<string name="autofill_pick_and_match">Auswählen und merken…</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="autofill_paste">Einfügen</string>
|
||||||
<string name="edit_commit_text">[ANDROID PwdStore] Edit  </string>
|
<string name="autofill_paste_username">Benutzername einfügen?\n\n%s</string>
|
||||||
<string name="send_plaintext_password_to">Passwort senden als Nur-Text mit behilfe von…</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>
|
||||||
<string name="show_password">Password wiedergeben</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -177,6 +177,11 @@
|
||||||
<string name="show_extra_content_pref_title">Show extra content</string>
|
<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="show_extra_content_pref_summary">Control the visibility of the extra content once decrypted</string>
|
||||||
<string name="pwd_generate_button">Generate</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 -->
|
<!-- 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>
|
<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_apps_delete">Delete</string>
|
||||||
<string name="autofill_pick">Pick…</string>
|
<string name="autofill_pick">Pick…</string>
|
||||||
<string name="autofill_pick_and_match">Pick and match…</string>
|
<string name="autofill_pick_and_match">Pick and match…</string>
|
||||||
<string name="refresh_list">Refresh list</string>
|
<string name="autofill_paste">Paste</string>
|
||||||
<string name="no_repo_selected">No external repository selected</string>
|
<string name="autofill_paste_username">Paste username?\n\n%s</string>
|
||||||
<string name="send_plaintext_password_to">Send password as plaintext using…</string>
|
<string name="autofill_toast_username">Select an editable field to past the username.\nUsername is available for %d seconds.</string>
|
||||||
<string name="show_password">Show password</string>
|
|
||||||
<string name="repository_uri">Repository URI</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue