11package com .zeapo .pwdstore .autofill ;
22
33import android .accessibilityservice .AccessibilityService ;
4+ import android .annotation .TargetApi ;
45import android .app .PendingIntent ;
56import android .content .ClipData ;
67import 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}
0 commit comments