|
|
@@ -0,0 +1,193 @@
|
|
|
+package info.knacki.pass.services;
|
|
|
+
|
|
|
+import android.content.ClipData;
|
|
|
+import android.content.ClipboardManager;
|
|
|
+import android.content.Context;
|
|
|
+import android.content.Intent;
|
|
|
+import android.graphics.PixelFormat;
|
|
|
+import android.os.Build;
|
|
|
+import android.os.Bundle;
|
|
|
+import android.os.Handler;
|
|
|
+import android.os.IBinder;
|
|
|
+import android.support.annotation.RequiresApi;
|
|
|
+import android.view.Gravity;
|
|
|
+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.Date;
|
|
|
+import java.util.logging.Level;
|
|
|
+import java.util.logging.Logger;
|
|
|
+
|
|
|
+import info.knacki.pass.R;
|
|
|
+import info.knacki.pass.io.FileInterfaceFactory;
|
|
|
+import info.knacki.pass.io.OnResponseListener;
|
|
|
+import info.knacki.pass.ui.passwordPicker.PasswordPickerFactory;
|
|
|
+
|
|
|
+@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
|
|
+public class AccessibilityService extends android.accessibilityservice.AccessibilityService {
|
|
|
+ private static final Logger log = Logger.getLogger(AccessibilityService.class.getName());
|
|
|
+ private final static long CANCEL_DELAY_SEC = 10;
|
|
|
+ private static AccessibilityService fInstance;
|
|
|
+ private boolean fManagingEvent;
|
|
|
+
|
|
|
+ private class LastCancellation {
|
|
|
+ public long lastCancelledTime;
|
|
|
+ public String lastCancelledPackage;
|
|
|
+
|
|
|
+ private LastCancellation() {
|
|
|
+ Clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ public void Cancel(AccessibilityNodeInfo source) {
|
|
|
+ fLastCancellation.lastCancelledPackage = source.getPackageName().toString();
|
|
|
+ fLastCancellation.lastCancelledTime = new Date().getTime() / 1000;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean IsCancelling(AccessibilityNodeInfo source, boolean clearCancelling) {
|
|
|
+ boolean cancelling = source.getPackageName().toString().equals(lastCancelledPackage) && (new Date().getTime() / 1000) -lastCancelledTime <= CANCEL_DELAY_SEC;
|
|
|
+ if (cancelling && clearCancelling)
|
|
|
+ this.Clear();
|
|
|
+ return cancelling;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void Clear() {
|
|
|
+ this.lastCancelledPackage = null;
|
|
|
+ this.lastCancelledTime = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ protected LastCancellation fLastCancellation = new LastCancellation();
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void onServiceConnected() {
|
|
|
+ super.onServiceConnected();
|
|
|
+ fInstance = this;
|
|
|
+ fManagingEvent = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onAccessibilityEvent(AccessibilityEvent event) {
|
|
|
+ synchronized (AccessibilityService.class) {
|
|
|
+ if (fManagingEvent || fLastCancellation.IsCancelling(event.getSource(), true))
|
|
|
+ return;
|
|
|
+ if (event.isPassword())
|
|
|
+ fManagingEvent = DisplayPasswordList(event.getSource());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean DisplayPasswordList(final 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;
|
|
|
+
|
|
|
+ final AccessibilityView openWindow = new AccessibilityView(this);
|
|
|
+ openWindow.Init(this, new AccessibilityView.AccessibilityViewListener() {
|
|
|
+ @Override
|
|
|
+ public void OnPasswordClicked(File f) {
|
|
|
+ new Handler(getMainLooper()).post(() -> {
|
|
|
+ LoadFile(f, openWindow.getWindowToken(), source, () ->
|
|
|
+ CloseOpenWindow(openWindow)
|
|
|
+ );
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void cancel() {
|
|
|
+ new Handler(getMainLooper()).post(() -> {
|
|
|
+ fLastCancellation.Cancel(source);
|
|
|
+ CloseOpenWindow(openWindow);
|
|
|
+ fManagingEvent = false;
|
|
|
+ SetFocus(source);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .AddCancelButton();
|
|
|
+
|
|
|
+ try {
|
|
|
+ WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
|
|
|
+ wm.addView(openWindow, params);
|
|
|
+ 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(AccessibilityView openWindow) {
|
|
|
+ if (openWindow != null) {
|
|
|
+ WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
|
|
|
+ wm.removeViewImmediate(openWindow);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void LoadFile(File f, IBinder windowToken, AccessibilityNodeInfo source, Runnable done) {
|
|
|
+ fManagingEvent = false;
|
|
|
+ log.info("Loading " +f.getName());
|
|
|
+ FileInterfaceFactory.GetFileInterface(this, PasswordPickerFactory.GetPasswordPicker(this, windowToken), f).ReadFile(new OnResponseListener<String>() {
|
|
|
+ @Override
|
|
|
+ public void OnResponse(String result) {
|
|
|
+ done.run();
|
|
|
+ SendPassword(source, result);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void OnError(String msg, Throwable e) {
|
|
|
+ done.run();
|
|
|
+ Toast.makeText(AccessibilityService.this, msg, Toast.LENGTH_LONG).show();
|
|
|
+ log.log(Level.SEVERE, msg, e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ 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(accessibilityNode, password);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ LegacySendPassword(accessibilityNode, password);
|
|
|
+ }
|
|
|
+ fManagingEvent = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SetFocus(AccessibilityNodeInfo source) {
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
|
|
+ source.performAction(AccessibilityNodeInfo.ACTION_CLICK);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void LegacySendPassword(AccessibilityNodeInfo accessibilityNode, String password) {
|
|
|
+ final ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
|
|
+ final ClipData passClip = ClipData.newPlainText("password", password);
|
|
|
+ final ClipData prev = clipboard.getPrimaryClip();
|
|
|
+
|
|
|
+ clipboard.setPrimaryClip(passClip);
|
|
|
+ accessibilityNode.performAction(AccessibilityNodeInfo.ACTION_PASTE);
|
|
|
+ clipboard.setPrimaryClip(prev == null ? ClipData.newPlainText("", "") : prev);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onInterrupt() {
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean onUnbind(Intent intent) {
|
|
|
+ fInstance = null;
|
|
|
+ return super.onUnbind(intent);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static boolean IsRunning() {
|
|
|
+ return fInstance != null;
|
|
|
+ }
|
|
|
+}
|