* 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;
|
||||
|
||||
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)
|
||||
|
@ -107,9 +117,9 @@ public class AutofillService extends AccessibilityService {
|
|||
// or if page changes in chrome
|
||||
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
||||
|| (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
||||
&& event.getPackageName() != null
|
||||
&& (event.getPackageName().equals("com.android.chrome")
|
||||
|| event.getPackageName().equals("com.android.browser")))) {
|
||||
&& event.getPackageName() != null
|
||||
&& (event.getPackageName().equals("com.android.chrome")
|
||||
|| event.getPackageName().equals("com.android.browser")))) {
|
||||
// there is a chance for getRootInActiveWindow() to return null at any time. save it.
|
||||
try {
|
||||
AccessibilityNodeInfo root = getRootInActiveWindow();
|
||||
|
@ -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
|
||||
|
@ -410,7 +471,7 @@ public class AutofillService extends AccessibilityService {
|
|||
lastWhichItem = which;
|
||||
if (which < items.size()) {
|
||||
bindDecryptAndVerify();
|
||||
} else if (which == items.size()){
|
||||
} else if (which == items.size()) {
|
||||
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
intent.putExtra("pick", true);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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  </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  </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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue