Browse Source

Refs #27 Use accessibility service to detect password inputs

isundil 7 years ago
parent
commit
48433c4469

+ 15 - 1
app/src/main/AndroidManifest.xml

@@ -12,6 +12,9 @@
     <!-- For GPG keys -->
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />
+    
+    <!-- Work with Accessibility service -->
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
 
     <application
         android:allowBackup="true"
@@ -22,7 +25,7 @@
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
         <service
-            android:name=".input.InputService"
+            android:name=".services.InputService"
             android:permission="android.permission.BIND_INPUT_METHOD">
             <intent-filter>
                 <action android:name="android.view.InputMethod" />
@@ -32,6 +35,17 @@
                 android:name="android.view.im"
                 android:resource="@xml/method" />
         </service>
+        <service
+            android:name=".services.AccessibilityService"
+            android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
+            <intent-filter>
+                <action android:name="android.accessibilityservice.AccessibilityService" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.accessibilityservice"
+                android:resource="@xml/accessibility_service" />
+        </service>
 
         <activity
             android:name=".settings.ui.SettingsActivity"

+ 116 - 0
app/src/main/java/info/knacki/pass/services/AccessibilityService.java

@@ -0,0 +1,116 @@
+package info.knacki.pass.services;
+
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Toast;
+
+import java.io.File;
+import java.util.logging.Logger;
+
+import info.knacki.pass.R;
+
+public class AccessibilityService extends android.accessibilityservice.AccessibilityService {
+    private boolean fManagingEvent;
+    private View fOpenWindow;
+
+    @Override
+    protected void onServiceConnected() {
+        super.onServiceConnected();
+        fManagingEvent = false;
+    }
+
+    @Override
+    public void onAccessibilityEvent(AccessibilityEvent event) {
+        synchronized (AccessibilityService.class) {
+            if (fManagingEvent)
+                return;
+            if (event.isPassword())
+                fManagingEvent = DisplayPasswordList(event.getSource());
+        }
+    }
+
+    private boolean DisplayPasswordList(AccessibilityNodeInfo source) {
+        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+                WindowManager.LayoutParams.MATCH_PARENT,
+                WindowManager.LayoutParams.WRAP_CONTENT,
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE,
+                0,
+                PixelFormat.TRANSLUCENT);
+        params.gravity = Gravity.START | Gravity.BOTTOM;
+        params.setTitle("Load Average");
+
+        try {
+            WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
+            fOpenWindow = LayoutInflater.from(this).inflate(R.layout.activity_accessibility, null, false);
+            wm.addView(fOpenWindow, params);
+            ((AccessibilityView) fOpenWindow).init(this, new AccessibilityView.AccessibilityViewListener() {
+                @Override
+                public void OnPasswordClicked(File f) {
+                    new Handler(getMainLooper()).post(() -> {
+                        CloseOpenWindow();
+                        LoadFile(f, source);
+                    });
+                }
+
+                @Override
+                public void cancel() {
+                    new Handler(getMainLooper()).post(() -> {
+                        CloseOpenWindow();
+                        fManagingEvent = false;
+                    });
+                }
+            });
+            return true;
+        }
+        catch (WindowManager.BadTokenException e) {
+            Toast.makeText(this, getResources().getString(R.string.app_name) +": " +getResources().getString(R.string.unauthorized_draw_over), Toast.LENGTH_LONG).show();
+        }
+        return false;
+    }
+
+    private void CloseOpenWindow() {
+        if (fOpenWindow != null) {
+            WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
+            wm.removeViewImmediate(fOpenWindow);
+            fOpenWindow = null;
+        }
+    }
+
+    private void LoadFile(File f, AccessibilityNodeInfo source) {
+        fManagingEvent = false;
+        Logger.getAnonymousLogger().severe("Loading " +f.getName());
+        //SendPassword(source, ""))
+    }
+
+    private void SendPassword(AccessibilityNodeInfo accessibilityNode, String password) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            Bundle passContent = new Bundle();
+            passContent.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, password);
+            try {
+                accessibilityNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, passContent);
+            } catch (Throwable e) {
+                LegacySendPassword(password);
+            }
+        } else {
+            LegacySendPassword(password);
+        }
+        fManagingEvent = false;
+    }
+
+    private void LegacySendPassword(String password) {
+        // FIXME
+    }
+
+    @Override
+    public void onInterrupt() {
+        new Handler(getMainLooper()).post(this::CloseOpenWindow);
+    }
+}

+ 41 - 0
app/src/main/java/info/knacki/pass/services/AccessibilityView.java

@@ -0,0 +1,41 @@
+package info.knacki.pass.services;
+
+import android.content.Context;
+import android.support.v7.widget.LinearLayoutCompat;
+import android.util.AttributeSet;
+import android.widget.ScrollView;
+
+import info.knacki.pass.R;
+import info.knacki.pass.io.PathUtils;
+import info.knacki.pass.ui.passwordList.PasswordClickListener;
+import info.knacki.pass.ui.passwordList.PasswordListView;
+
+public class AccessibilityView extends LinearLayoutCompat {
+    public interface AccessibilityViewListener extends PasswordClickListener {
+        void cancel();
+    }
+
+    PasswordListView fPasswordListView;
+
+    public AccessibilityView init(Context ctx, final AccessibilityViewListener listener) {
+        fPasswordListView = new PasswordListView(ctx, listener, PathUtils.GetPassDir(ctx));
+        ((ScrollView) findViewById(R.id.passwordListContainer)).addView(fPasswordListView);
+        findViewById(R.id.cancel_action).setOnClickListener(v -> {
+            if (listener != null)
+                listener.cancel();
+        });
+        return this;
+    }
+
+    public AccessibilityView(Context ctx) {
+        super(ctx);
+    }
+
+    public AccessibilityView(Context ctx, AttributeSet attrs) {
+        super(ctx, attrs);
+    }
+
+    public AccessibilityView(Context ctx, AttributeSet attrs, int defStyle) {
+        super(ctx, attrs, defStyle);
+    }
+}

+ 2 - 2
app/src/main/java/info/knacki/pass/input/InputService.java → app/src/main/java/info/knacki/pass/services/InputService.java

@@ -1,4 +1,4 @@
-package info.knacki.pass.input;
+package info.knacki.pass.services;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
@@ -82,7 +82,7 @@ public class InputService extends InputMethodService implements PasswordClickLis
     @Override
     public View onCreateInputView() {
         fInputView = LayoutInflater.from(this).inflate(R.layout.input, null, false);
-        fPasswordListView = new PasswordListView<>(this, PathUtils.GetPassDir(this));
+        fPasswordListView = new PasswordListView(this, this, PathUtils.GetPassDir(this));
         ((ScrollView)fInputView.findViewById(R.id.passwordListContainer)).addView(fPasswordListView);
         fInputView.findViewById(R.id.prevButton).setOnClickListener(view -> {
             SwitchToPreviousInputMethod();

+ 10 - 3
app/src/main/java/info/knacki/pass/settings/ui/SettingsActivity.java

@@ -16,6 +16,7 @@ import android.preference.Preference;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceFragment;
 import android.preference.SwitchPreference;
+import android.provider.Settings;
 import android.support.v4.app.ShareCompat;
 import android.support.v4.content.FileProvider;
 import android.support.v7.app.ActionBar;
@@ -138,8 +139,6 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
     }
 
     public static class GeneralPreferenceFragment extends PreferenceFragment {
-        public static final String INPUT_METHOD_SETTINGS = "android.settings.INPUT_METHOD_SETTINGS";
-
         @Override
         public void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
@@ -147,7 +146,15 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
             setHasOptionsMenu(true);
 
             findPreference(getResources().getString(R.string.id_softSettings)).setOnPreferenceClickListener(preference -> {
-                startActivity(new Intent(INPUT_METHOD_SETTINGS));
+                startActivity(new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS));
+                return true;
+            });
+            findPreference(getResources().getString(R.string.id_accessibility_settings)).setOnPreferenceClickListener(preference -> {
+                startActivity(new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS));
+                return true;
+            });
+            findPreference(getResources().getString(R.string.id_draw_over_settings)).setOnPreferenceClickListener(preference -> {
+                startActivity(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION));
                 return true;
             });
         }

+ 1 - 1
app/src/main/java/info/knacki/pass/ui/MainActivity.java

@@ -120,7 +120,7 @@ public class MainActivity extends AppCompatActivity implements PasswordEditListe
         setContentView(R.layout.activity_pass_list);
 
         setTitle(R.string.title);
-        fPasswordListView = new EditablePasswordListView<>(this, PathUtils.GetPassDir(this), false);
+        fPasswordListView = new EditablePasswordListView(this, this, PathUtils.GetPassDir(this), false);
         ((ScrollView)findViewById(R.id.passwordListContainer)).addView(fPasswordListView);
 
         requestPermissions();

+ 9 - 9
app/src/main/java/info/knacki/pass/ui/passwordList/EditablePasswordListView.java

@@ -9,12 +9,12 @@ import java.io.File;
 import info.knacki.pass.R;
 import info.knacki.pass.io.PathUtils;
 
-public class EditablePasswordListView<T extends Activity & PasswordEditListener> extends PasswordListView<T> implements PopupMenu.OnMenuItemClickListener  {
+public class EditablePasswordListView extends PasswordListView implements PopupMenu.OnMenuItemClickListener  {
     private PasswordView fSelectedPassword;
     private boolean fDisplayHiddenFiles;
 
-    public EditablePasswordListView(T ctx, String rootPath, boolean displayHidden) {
-        super(ctx, rootPath);
+    public EditablePasswordListView(Activity ctx, PasswordEditListener editListener, String rootPath, boolean displayHidden) {
+        super(ctx, editListener, rootPath);
         fDisplayHiddenFiles = displayHidden;
     }
 
@@ -39,7 +39,7 @@ public class EditablePasswordListView<T extends Activity & PasswordEditListener>
             return;
         pv.setOnLongClickListener(v -> {
             fSelectedPassword = pv;
-            PopupMenu menu = new PopupMenu(fClickListener, pv);
+            PopupMenu menu = new PopupMenu(fContext, pv);
             menu.getMenuInflater().inflate(GetMenuForFile(file), menu.getMenu());
             menu.setOnMenuItemClickListener(EditablePasswordListView.this);
             menu.show();
@@ -61,22 +61,22 @@ public class EditablePasswordListView<T extends Activity & PasswordEditListener>
         switch (item.getItemId()) {
             case R.id.remove:
                 if (fSelectedPassword.IsDir()) {
-                    fClickListener.OnRemoveDirectory(new File(fCurrentDir + "/" + fSelectedPassword.fFullName));
+                    ((PasswordEditListener) fClickListener).OnRemoveDirectory(new File(fCurrentDir + "/" + fSelectedPassword.fFullName));
                 } else {
-                    fClickListener.OnRemovePassword(new File(fCurrentDir + "/" + fSelectedPassword.fFullName));
+                    ((PasswordEditListener) fClickListener).OnRemovePassword(new File(fCurrentDir + "/" + fSelectedPassword.fFullName));
                 }
                 break;
 
             case R.id.restore:
-                fClickListener.OnRestorePassword(new File(fCurrentDir + "/" + fSelectedPassword.fFullName));
+                ((PasswordEditListener) fClickListener).OnRestorePassword(new File(fCurrentDir + "/" + fSelectedPassword.fFullName));
                 break;
 
             case R.id.encryption_information:
-                fClickListener.OnEncryptionInformation(new File(fCurrentDir +"/" +fSelectedPassword.fFullName));
+                ((PasswordEditListener) fClickListener).OnEncryptionInformation(new File(fCurrentDir +"/" +fSelectedPassword.fFullName));
                 break;
 
             case R.id.clipboard:
-                fClickListener.OnCopyToClipboard(new File(fCurrentDir +"/" +fSelectedPassword.fFullName));
+                ((PasswordEditListener) fClickListener).OnCopyToClipboard(new File(fCurrentDir +"/" +fSelectedPassword.fFullName));
                 break;
 
             default:

+ 6 - 4
app/src/main/java/info/knacki/pass/ui/passwordList/PasswordListView.java

@@ -11,10 +11,11 @@ import java.util.TreeSet;
 import info.knacki.pass.io.FileUtils;
 import info.knacki.pass.io.PathUtils;
 
-public class PasswordListView<T extends Context & PasswordClickListener> extends LinearLayout {
+public class PasswordListView extends LinearLayout {
     public final String fRootPath;
     public String fCurrentDir;
-    protected final T fClickListener;
+    protected final PasswordClickListener fClickListener;
+    protected final Context fContext;
 
     class FileIdentity implements Comparable<FileIdentity> {
         final boolean fIsDir;
@@ -74,9 +75,10 @@ public class PasswordListView<T extends Context & PasswordClickListener> extends
         return pv;
     }
 
-    public PasswordListView(final T ctx, String rootPath) {
+    public PasswordListView(final Context ctx, PasswordClickListener clickListener, String rootPath) {
         super(ctx);
-        fClickListener = ctx;
+        fClickListener = clickListener;
+        fContext = ctx;
         File rootDir = new File(rootPath);
         if (!rootDir.exists())
             FileUtils.MkDir(rootDir, true);

+ 22 - 0
app/src/main/res/layout/activity_accessibility.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<info.knacki.pass.services.AccessibilityView xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:background="@color/inputBackground"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="info.knacki.pass.services.AccessibilityView">
+    <ScrollView
+        android:id="@+id/passwordListContainer"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="@dimen/keyboard_min_height"
+        android:maxHeight="@dimen/keyboard_max_height"
+        />
+    <Button
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/cancel"
+        android:id="@+id/cancel_action"/>
+</info.knacki.pass.services.AccessibilityView>

+ 4 - 3
app/src/main/res/layout/input.xml

@@ -3,8 +3,6 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:minHeight="@dimen/keyboard_min_height"
-    android:maxHeight="@dimen/keyboard_max_height"
     tools:context=".ui.MainActivity"
     android:background="@color/inputBackground">
 
@@ -76,7 +74,10 @@
     <ScrollView
         android:id="@+id/passwordListContainer"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+        android:layout_height="match_parent"
+        android:minHeight="@dimen/keyboard_min_height"
+        android:maxHeight="@dimen/keyboard_max_height"
+        />
 </LinearLayout>
 
 </android.support.constraint.ConstraintLayout>

+ 4 - 0
app/src/main/res/values-fr/lang.xml

@@ -110,4 +110,8 @@
     <string name="change">Changer</string>
     <string name="change_encryption_method">Changer la méthode de chiffrement</string>
     <string name="trash">Poubelle</string>
+    <string name="accessibility_service_description">Detection des formulaires contenant des champs de mot de passe.</string>
+    <string name="unauthorized_draw_over">Vous devez autoriser l\'affichage par dessus d\'autres applications</string>
+    <string name="pref_title_system_accessibility_settings">Paramètres d\'accessibilité</string>
+    <string name="pref_title_system_draw_over">Gerer l\'affichage par dessus les autres applications</string>
 </resources>

+ 2 - 2
app/src/main/res/values/dimens.xml

@@ -2,7 +2,7 @@
 <resources>
     <dimen name="keyboard_key_height">32dp</dimen>
     <dimen name="big_icon_height">64dp</dimen>
-    <dimen name="keyboard_min_height">600px</dimen>
-    <dimen name="keyboard_max_height">664px</dimen>
+    <dimen name="keyboard_min_height">550px</dimen>
+    <dimen name="keyboard_max_height">614px</dimen>
     <dimen name="fab_margin">16dp</dimen>
 </resources>

+ 4 - 0
app/src/main/res/values/lang.xml

@@ -110,4 +110,8 @@
     <string name="change">Change</string>
     <string name="change_encryption_method">Change encryption method</string>
     <string name="trash">Trash</string>
+    <string name="accessibility_service_description">Detect password input automatically</string>
+    <string name="unauthorized_draw_over">You must authorized \'Draw over other apps\' permission</string>
+    <string name="pref_title_system_accessibility_settings">Accessibility settings</string>
+    <string name="pref_title_system_draw_over">Draw over other app permissions</string>
 </resources>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -25,4 +25,6 @@
     <integer name="id_keyboard_numbers" translatable="false">-2</integer>
     <integer name="id_keyboard_shift" translatable="false">-1</integer>
     <integer name="id_keyboard_delete" translatable="false">-5</integer>
+    <string name="id_accessibility_settings" translatable="false">id_accessibility_settings</string>
+    <string name="id_draw_over_settings" translatable="false">id_draw_over_settings</string>
 </resources>

+ 10 - 0
app/src/main/res/xml/accessibility_service.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- android:accessibilityEventTypes="typeAllMask" -->
+<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
+    android:description="@string/accessibility_service_description"
+    android:accessibilityEventTypes="typeViewClicked|typeViewFocused"
+    android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagReportViewIds"
+    android:accessibilityFeedbackType="feedbackGeneric"
+    android:canRetrieveWindowContent="true"
+    android:settingsActivity=".services.AccessibilityService"
+    />

+ 2 - 0
app/src/main/res/xml/pref_general.xml

@@ -1,3 +1,5 @@
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
     <Preference android:title="@string/pref_title_system_keyboard_settings" android:key="@string/id_softSettings"/>
+    <Preference android:title="@string/pref_title_system_accessibility_settings" android:key="@string/id_accessibility_settings"/>
+    <Preference android:title="@string/pref_title_system_draw_over" android:key="@string/id_draw_over_settings"/>
 </PreferenceScreen>