2016-01-26 23:58:33 -05:00
package keepass2android.autofill ;
import android.accessibilityservice.AccessibilityService ;
import android.app.Notification ;
import android.app.NotificationManager ;
import android.app.PendingIntent ;
import android.content.Intent ;
import android.content.pm.ApplicationInfo ;
import android.content.pm.PackageManager ;
import android.os.Build ;
import android.os.Bundle ;
import android.view.accessibility.AccessibilityEvent ;
import android.view.accessibility.AccessibilityNodeInfo ;
import java.net.URI ;
import java.net.URISyntaxException ;
import java.util.ArrayList ;
import java.util.List ;
import java.util.Objects ;
import keepass2android.kbbridge.KeyboardData ;
/ * *
* Created by Philipp on 25 . 01 . 2016 .
* /
public class AutoFillService extends AccessibilityService {
private static boolean _hasUsedData = false ;
private static String _lastSearchUrl ;
private static final String _logTag = " KP2AAF " ;
private static boolean _isRunning ;
private final int autoFillNotificationId = 798810 ;
private final String androidAppPrefix = " androidapp:// " ;
@Override
public void onCreate ( ) {
super . onCreate ( ) ;
_isRunning = true ;
android . util . Log . d ( _logTag , " OnCreate " ) ;
}
@Override
public void onDestroy ( ) {
super . onDestroy ( ) ;
_isRunning = false ;
}
interface NodeCondition
{
boolean check ( AccessibilityNodeInfo n ) ;
}
class WindowIdCondition implements NodeCondition
{
private int id ;
public WindowIdCondition ( int id )
{
this . id = id ;
}
@Override
public boolean check ( AccessibilityNodeInfo n ) {
return n . getWindowId ( ) = = id ;
}
}
class SystemUiCondition implements NodeCondition
{
@Override
public boolean check ( AccessibilityNodeInfo n ) {
return ( n . getViewIdResourceName ( ) ! = null ) & & ( n . getViewIdResourceName ( ) . startsWith ( " com.android.systemui " ) ) ;
}
}
private class PasswordFieldCondition implements NodeCondition {
@Override
public boolean check ( AccessibilityNodeInfo n ) {
return n . isPassword ( ) & & (
( n . getText ( ) = = null )
| | ( " " . equals ( n . getText ( ) ) ) ) ;
}
}
private class EditTextCondition implements NodeCondition {
@Override
public boolean check ( AccessibilityNodeInfo n ) {
//it seems like n.Editable is not a good check as this is false for some fields which are actually editable, at least in tests with Chrome.
return ( n . getClassName ( ) ! = null ) & & ( n . getClassName ( ) . toString ( ) . toLowerCase ( ) . contains ( " edittext " ) ) ;
}
}
public static boolean isAvailable ( )
{
return ( Build . VERSION . SDK_INT > = Build . VERSION_CODES . LOLLIPOP ) ;
}
public static boolean isRunning ( )
{
return _isRunning ;
}
@Override
public void onAccessibilityEvent ( AccessibilityEvent event ) {
android . util . Log . d ( _logTag , " OnAccEvent " ) ;
try
{
if ( event . getEventType ( ) = = AccessibilityEvent . TYPE_WINDOW_CONTENT_CHANGED
| | event . getEventType ( ) = = AccessibilityEvent . TYPE_WINDOW_STATE_CHANGED )
{
2016-02-07 14:55:29 -05:00
CharSequence packageName = event . getPackageName ( ) ;
android . util . Log . d ( _logTag , " event: " + event . getEventType ( ) + " , package = " + packageName ) ;
2016-01-26 23:58:33 -05:00
if ( " com.android.systemui " . equals ( event . getPackageName ( ) ) )
{
android . util . Log . d ( _logTag , " return. " ) ;
return ; //avoid that the notification is cancelled when pulling down notif drawer
}
else
{
android . util . Log . d ( _logTag , " no com.android.systemui " ) ;
}
2016-02-07 14:55:29 -05:00
if ( ( packageName ! = null )
& & ( packageName . toString ( ) . startsWith ( " keepass2android. " ) ) )
{
android . util . Log . d ( _logTag , " don't autofill kp2a. " ) ;
return ;
}
2016-01-26 23:58:33 -05:00
AccessibilityNodeInfo root = getRootInActiveWindow ( ) ;
int eventWindowId = event . getWindowId ( ) ;
if ( ( ExistsNodeOrChildren ( root , new WindowIdCondition ( eventWindowId ) ) & & ! ExistsNodeOrChildren ( root , new SystemUiCondition ( ) ) ) )
{
boolean cancelNotification = true ;
String url = androidAppPrefix + root . getPackageName ( ) ;
if ( " com.android.chrome " . equals ( root . getPackageName ( ) ) )
{
List < AccessibilityNodeInfo > urlFields = root . findAccessibilityNodeInfosByViewId ( " com.android.chrome:id/url_bar " ) ;
url = urlFromAddressFields ( urlFields , url ) ;
}
else if ( " com.android.browser " . equals ( root . getPackageName ( ) ) )
{
List < AccessibilityNodeInfo > urlFields = root . findAccessibilityNodeInfosByViewId ( " com.android.browser:id/url " ) ;
url = urlFromAddressFields ( urlFields , url ) ;
}
if ( ExistsNodeOrChildren ( root , new PasswordFieldCondition ( ) ) )
{
if ( ( getLastReceivedCredentialsUser ( ) ! = null ) & &
( Objects . equals ( url , _lastSearchUrl )
| | isSame ( getCredentialsField ( " URL " ) , url ) ) )
{
android . util . Log . d ( _logTag , " Filling credentials for " + url ) ;
List < AccessibilityNodeInfo > emptyPasswordFields = new ArrayList < > ( ) ;
GetNodeOrChildren ( root , new PasswordFieldCondition ( ) , emptyPasswordFields ) ;
List < AccessibilityNodeInfo > allEditTexts = new ArrayList < > ( ) ;
GetNodeOrChildren ( root , new EditTextCondition ( ) , allEditTexts ) ;
AccessibilityNodeInfo usernameEdit = null ;
for ( int i = 0 ; i < allEditTexts . size ( ) ; i + + )
{
if ( allEditTexts . get ( i ) . isPassword ( ) = = false )
{
usernameEdit = allEditTexts . get ( i ) ;
android . util . Log . d ( _logTag , " setting usernameEdit = " + usernameEdit . getText ( ) + " " ) ;
}
else break ;
}
FillPassword ( url , usernameEdit , emptyPasswordFields ) ;
}
else
{
android . util . Log . d ( _logTag , " Notif for " + url ) ;
AskFillPassword ( url ) ;
cancelNotification = false ;
}
}
if ( cancelNotification )
{
( ( NotificationManager ) getSystemService ( NOTIFICATION_SERVICE ) ) . cancel ( autoFillNotificationId ) ;
android . util . Log . d ( _logTag , " Cancel notif " ) ;
}
}
}
}
catch ( Exception e )
{
2016-02-15 15:23:48 -05:00
android . util . Log . e ( _logTag , ( e . toString ( ) = = null ) ? " (null) " : e . toString ( ) ) ;
2016-01-26 23:58:33 -05:00
2016-04-04 16:13:28 -04:00
/ * Intent intent = new Intent ( Intent . ACTION_SEND ) ;
2016-01-30 02:40:39 -05:00
intent . setType ( " message/rfc822 " ) ;
String to = " crocoapps@gmail.com " ;
intent . putExtra ( Intent . EXTRA_EMAIL , new String [ ] { to } ) ;
2016-02-15 15:23:48 -05:00
intent . putExtra ( Intent . EXTRA_SUBJECT , " Error report 7d+ " ) ;
2016-01-30 02:40:39 -05:00
intent . putExtra ( Intent . EXTRA_TEXT ,
" Please send the following text as an error report to crocoapps@gmail.com. You may also add additional information about the workflow you tried to perform. This will help me improve the app. Thanks! \ n " + e . toString ( ) ) ;
2016-01-26 23:58:33 -05:00
Notification . Builder builder = new Notification . Builder ( this ) ;
builder . setSmallIcon ( keepass2android . softkeyboard . R . drawable . ic_notify_autofill )
. setContentText ( e . toString ( ) )
. setContentTitle ( " error information " )
2016-01-30 02:40:39 -05:00
. setWhen ( java . lang . System . currentTimeMillis ( ) )
. setContentIntent ( PendingIntent . getActivity ( this , 0 , Intent . createChooser ( intent , " Send error report " ) , PendingIntent . FLAG_CANCEL_CURRENT ) ) ;
2016-01-26 23:58:33 -05:00
NotificationManager notificationManager = ( NotificationManager ) getSystemService ( NOTIFICATION_SERVICE ) ;
2016-04-04 16:13:28 -04:00
notificationManager . notify ( autoFillNotificationId + 1 , builder . build ( ) ) ; * /
2016-01-26 23:58:33 -05:00
}
}
private void AskFillPassword ( String url )
{
Intent startKp2aIntent = getPackageManager ( ) . getLaunchIntentForPackage ( getApplicationContext ( ) . getPackageName ( ) ) ;
if ( startKp2aIntent ! = null )
{
startKp2aIntent . addCategory ( Intent . CATEGORY_LAUNCHER ) ;
startKp2aIntent . addFlags ( Intent . FLAG_ACTIVITY_NEW_TASK | Intent . FLAG_ACTIVITY_CLEAR_TASK ) ;
String taskName = " SearchUrlTask " ;
startKp2aIntent . putExtra ( " KP2A_APPTASK " , taskName ) ;
startKp2aIntent . putExtra ( " UrlToSearch " , url ) ;
}
PendingIntent pending = PendingIntent . getActivity ( this , 0 , startKp2aIntent , PendingIntent . FLAG_UPDATE_CURRENT ) ;
String targetName = url ;
if ( url . startsWith ( androidAppPrefix ) )
{
String packageName = url . substring ( androidAppPrefix . length ( ) ) ;
try
{
ApplicationInfo appInfo = getPackageManager ( ) . getApplicationInfo ( packageName , 0 ) ;
targetName = ( String ) ( appInfo ! = null ? getPackageManager ( ) . getApplicationLabel ( appInfo ) : packageName ) ;
}
catch ( Exception e )
{
2016-02-15 15:23:48 -05:00
android . util . Log . d ( _logTag , ( e . toString ( ) = = null ) ? " (null) " : e . toString ( ) ) ;
2016-01-26 23:58:33 -05:00
targetName = packageName ;
}
}
else
{
targetName = getHost ( url ) ;
}
Notification . Builder builder = new Notification . Builder ( this ) ;
//TODO icon
//TODO plugin icon
builder . setSmallIcon ( keepass2android . softkeyboard . R . drawable . ic_notify_autofill )
. setContentText ( getString ( keepass2android . softkeyboard . R . string . NotificationContentText , new Object [ ] { targetName } ) )
. setContentTitle ( getString ( keepass2android . softkeyboard . R . string . NotificationTitle ) )
. setWhen ( java . lang . System . currentTimeMillis ( ) )
. setVisibility ( Notification . VISIBILITY_SECRET )
. setContentIntent ( pending ) ;
NotificationManager notificationManager = ( NotificationManager ) getSystemService ( NOTIFICATION_SERVICE ) ;
notificationManager . notify ( autoFillNotificationId , builder . build ( ) ) ;
}
private void FillPassword ( String url , AccessibilityNodeInfo usernameEdit , List < AccessibilityNodeInfo > passwordFields )
{
if ( ( keepass2android . kbbridge . KeyboardData . hasData ( ) ) & & ( _hasUsedData = = false ) )
{
fillDataInTextField ( usernameEdit , getLastReceivedCredentialsUser ( ) ) ;
for ( int i = 0 ; i < passwordFields . size ( ) ; i + + )
{
fillDataInTextField ( passwordFields . get ( i ) , getLastReceivedCredentialsPassword ( ) ) ;
}
_hasUsedData = true ;
}
//LookupCredentialsActivity.LastReceivedCredentials = null;
}
private void fillDataInTextField ( AccessibilityNodeInfo edit , String value ) {
2016-02-07 14:55:29 -05:00
if ( ( value = = null ) | | ( edit = = null ) )
2016-01-26 23:58:33 -05:00
return ;
Bundle b = new Bundle ( ) ;
b . putString ( AccessibilityNodeInfo . ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE , value ) ;
edit . performAction ( AccessibilityNodeInfo . ACTION_SET_TEXT , b ) ;
}
private boolean isSame ( String url1 , String url2 ) {
if ( url1 = = null )
return ( url2 = = null ) ;
2016-01-31 10:44:21 -05:00
if ( url2 = = null )
return ( url1 = = null ) ;
if ( url1 . startsWith ( " androidapp:// " ) )
return url1 . equals ( url2 ) ;
2016-01-26 23:58:33 -05:00
return getHost ( url1 ) . equals ( getHost ( url2 ) ) ;
}
private String getHost ( String url )
{
URI uri = null ;
try {
uri = new URI ( url ) ;
String domain = uri . getHost ( ) ;
2016-02-07 14:55:29 -05:00
if ( domain = = null )
return url ;
2016-01-26 23:58:33 -05:00
return domain . startsWith ( " www. " ) ? domain . substring ( 4 ) : domain ;
} catch ( URISyntaxException e ) {
android . util . Log . d ( _logTag , " error parsing url: " + url + e . toString ( ) ) ;
return url ;
}
2016-02-07 14:55:29 -05:00
2016-01-26 23:58:33 -05:00
}
private String getLastReceivedCredentialsUser ( ) {
return getCredentialsField ( " UserName " ) ;
}
private String getLastReceivedCredentialsPassword ( ) {
return getCredentialsField ( " Password " ) ;
}
private String getCredentialsField ( String key ) {
for ( int i = 0 ; i < KeyboardData . availableFields . size ( ) ; i + + )
{
if ( key . equals ( KeyboardData . availableFields . get ( i ) . key ) )
{
if ( KeyboardData . availableFields . get ( i ) . value ! = null )
return KeyboardData . availableFields . get ( i ) . value ;
}
}
return null ;
}
private void GetNodeOrChildren ( AccessibilityNodeInfo n , NodeCondition condition , List < AccessibilityNodeInfo > result ) {
if ( n ! = null )
{
if ( condition . check ( n ) )
result . add ( n ) ;
for ( int i = 0 ; i < n . getChildCount ( ) ; i + + )
{
GetNodeOrChildren ( n . getChild ( i ) , condition , result ) ;
}
}
}
private boolean ExistsNodeOrChildren ( AccessibilityNodeInfo n , NodeCondition condition ) {
if ( n = = null ) return false ;
if ( condition . check ( n ) )
return true ;
for ( int i = 0 ; i < n . getChildCount ( ) ; i + + )
{
if ( ExistsNodeOrChildren ( n . getChild ( i ) , condition ) )
return true ;
}
return false ;
}
private String urlFromAddressFields ( List < AccessibilityNodeInfo > urlFields , String url ) {
if ( ! urlFields . isEmpty ( ) )
{
AccessibilityNodeInfo addressField = urlFields . get ( 0 ) ;
2016-02-07 14:55:29 -05:00
CharSequence text = addressField . getText ( ) ;
if ( text ! = null )
{
url = text . toString ( ) ;
if ( ! url . contains ( " :// " ) )
url = " http:// " + url ;
}
2016-01-26 23:58:33 -05:00
}
return url ;
}
@Override
public void onInterrupt ( ) {
}
public static void NotifyNewData ( String searchUrl )
{
_hasUsedData = false ;
_lastSearchUrl = searchUrl ;
android . util . Log . d ( _logTag , " Notify new data: " + searchUrl ) ;
}
}