File indexing completed on 2024-12-22 04:41:39

0001 /*
0002  * SPDX-FileCopyrightText: 2021 SohnyBohny <sohny.bean@streber24.de>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 package org.kde.kdeconnect.Plugins.MouseReceiverPlugin;
0008 
0009 import android.accessibilityservice.AccessibilityService;
0010 import android.accessibilityservice.GestureDescription;
0011 import android.graphics.Path;
0012 import android.graphics.PixelFormat;
0013 import android.os.Build;
0014 import android.os.Handler;
0015 import android.util.DisplayMetrics;
0016 import android.util.Log;
0017 import android.view.Gravity;
0018 import android.view.View;
0019 import android.view.ViewConfiguration;
0020 import android.view.WindowManager;
0021 import android.view.WindowManager.LayoutParams;
0022 import android.view.accessibility.AccessibilityEvent;
0023 import android.view.accessibility.AccessibilityNodeInfo;
0024 import android.widget.ImageView;
0025 
0026 import androidx.annotation.RequiresApi;
0027 import androidx.core.content.ContextCompat;
0028 
0029 import org.kde.kdeconnect_tp.R;
0030 
0031 import java.util.ArrayDeque;
0032 import java.util.Deque;
0033 
0034 public class MouseReceiverService extends AccessibilityService {
0035     public static MouseReceiverService instance;
0036 
0037     private View cursorView;
0038     private LayoutParams cursorLayout;
0039     private WindowManager windowManager;
0040     private Handler runHandler;
0041     private Runnable hideRunnable;
0042     private GestureDescription.StrokeDescription swipeStoke;
0043     private double scrollSum;
0044 
0045     @Override
0046     public void onCreate() {
0047         super.onCreate();
0048         MouseReceiverService.instance = this;
0049         Log.i("MouseReceiverService", "created");
0050     }
0051 
0052     @Override
0053     protected void onServiceConnected() {
0054         // Create an overlay and display the cursor
0055         windowManager = ContextCompat.getSystemService(this, WindowManager.class);
0056         DisplayMetrics displayMetrics = new DisplayMetrics();
0057         windowManager.getDefaultDisplay().getMetrics(displayMetrics);
0058 
0059         cursorView = View.inflate(getBaseContext(), R.layout.mouse_receiver_cursor, null);
0060         cursorLayout = new LayoutParams(
0061                 LayoutParams.WRAP_CONTENT,
0062                 LayoutParams.WRAP_CONTENT,
0063                 LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
0064                 LayoutParams.FLAG_DISMISS_KEYGUARD | LayoutParams.FLAG_NOT_FOCUSABLE
0065                         | LayoutParams.FLAG_NOT_TOUCHABLE | LayoutParams.FLAG_FULLSCREEN
0066                         | LayoutParams.FLAG_LAYOUT_NO_LIMITS,
0067                 PixelFormat.TRANSLUCENT);
0068 
0069         // allow cursor to move over status bar on devices having a display cutout
0070         // https://developer.android.com/guide/topics/display-cutout/#render_content_in_short_edge_cutout_areas
0071         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
0072             cursorLayout.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
0073         }
0074 
0075         cursorLayout.gravity = Gravity.START | Gravity.TOP;
0076         cursorLayout.x = displayMetrics.widthPixels / 2;
0077         cursorLayout.y = displayMetrics.heightPixels / 2;
0078 
0079         // https://developer.android.com/training/system-ui/navigation.html#behind
0080         cursorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
0081                 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
0082                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
0083 
0084         windowManager.addView(cursorView, cursorLayout);
0085 
0086         hideRunnable = () -> {
0087             cursorView.setVisibility(View.GONE);
0088             Log.i("MouseReceiverService", "Hiding pointer due to inactivity");
0089         };
0090         runHandler = new Handler();
0091 
0092         cursorView.setVisibility(View.GONE);
0093     }
0094 
0095     private void hideAfter5Seconds() {
0096         runHandler.removeCallbacks(hideRunnable);
0097         runHandler.postDelayed(hideRunnable, 5000);
0098     }
0099 
0100     public float getX() {
0101         return cursorLayout.x + cursorView.getWidth() / 2;
0102     }
0103 
0104     public float getY() {
0105         return cursorLayout.y + cursorView.getHeight() / 2;
0106     }
0107 
0108     public void moveView(double dx, double dy) {
0109         DisplayMetrics displayMetrics = new DisplayMetrics();
0110         instance.windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
0111 
0112         cursorLayout.x += dx;
0113         cursorLayout.y += dy;
0114 
0115         if (getX() > displayMetrics.widthPixels)
0116             cursorLayout.x = displayMetrics.widthPixels - cursorView.getWidth() / 2;
0117         if (getY() > displayMetrics.heightPixels)
0118             cursorLayout.y = displayMetrics.heightPixels - cursorView.getHeight() / 2;
0119         if (getX() < 0) cursorLayout.x = -cursorView.getWidth() / 2;
0120         if (getY() < 0) cursorLayout.y = -cursorView.getHeight() / 2;
0121 
0122         new Handler(instance.getMainLooper()).post(() -> {
0123             // Log.i("MouseReceiverService", "performing move");
0124             instance.windowManager.updateViewLayout(instance.cursorView, instance.cursorLayout);
0125             instance.cursorView.setVisibility(View.VISIBLE);
0126         });
0127     }
0128 
0129     public static boolean move(double dx, double dy) {
0130         if (instance == null) return false;
0131 
0132         float fromX = instance.getX();
0133         float fromY = instance.getY();
0134 
0135         instance.moveView(dx, dy);
0136 
0137         instance.hideAfter5Seconds();
0138 
0139         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && instance.isSwiping()) {
0140             return instance.continueSwipe(fromX, fromY);
0141         }
0142 
0143         return true;
0144     }
0145 
0146     @RequiresApi(api = Build.VERSION_CODES.N)
0147     private static GestureDescription createClick(float x, float y, int duration) {
0148         Path clickPath = new Path();
0149         clickPath.moveTo(x, y);
0150         GestureDescription.StrokeDescription clickStroke =
0151                 new GestureDescription.StrokeDescription(clickPath, 0, duration);
0152         GestureDescription.Builder clickBuilder = new GestureDescription.Builder();
0153         clickBuilder.addStroke(clickStroke);
0154         return clickBuilder.build();
0155     }
0156 
0157     @RequiresApi(api = Build.VERSION_CODES.N)
0158     public static boolean click() {
0159         if (instance == null) return false;
0160         // Log.i("MouseReceiverService", "x: " + instance.getX() + " y:" + instance.getY());
0161 
0162         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && instance.isSwiping()) {
0163             return instance.stopSwipe();
0164         }
0165 
0166         return click(instance.getX(), instance.getY());
0167     }
0168 
0169     @RequiresApi(api = Build.VERSION_CODES.N)
0170     public static boolean click(float x, float y) {
0171         if (instance == null) return false;
0172         return instance.dispatchGesture(createClick(x, y, 1 /*ms*/), null, null);
0173     }
0174 
0175     @RequiresApi(api = Build.VERSION_CODES.N)
0176     public static boolean longClick() {
0177         if (instance == null) return false;
0178         return instance.dispatchGesture(createClick(instance.getX(), instance.getY(),
0179                 ViewConfiguration.getLongPressTimeout()), null, null);
0180     }
0181 
0182     @RequiresApi(api = Build.VERSION_CODES.O)
0183     public static boolean longClickSwipe() {
0184         if (instance == null) return false;
0185 
0186         if (instance.isSwiping()) {
0187             return instance.stopSwipe();
0188         } else {
0189             return instance.startSwipe();
0190         }
0191     }
0192 
0193     private boolean isSwiping() {
0194         return swipeStoke != null;
0195     }
0196 
0197     @RequiresApi(api = Build.VERSION_CODES.O)
0198     private boolean startSwipe() {
0199         assert swipeStoke == null;
0200         Path path = new Path();
0201         path.moveTo(getX(), getY());
0202         swipeStoke = new GestureDescription.StrokeDescription(path, 0, 1, true);
0203         GestureDescription.Builder builder = new GestureDescription.Builder();
0204         builder.addStroke(swipeStoke);
0205         ((ImageView) cursorView.findViewById(R.id.mouse_cursor)).setImageResource(R.drawable.mouse_pointer_clicked);
0206         return dispatchGesture(builder.build(), null, null);
0207     }
0208 
0209     @RequiresApi(api = Build.VERSION_CODES.O)
0210     private boolean continueSwipe(float fromX, float fromY) {
0211         Path path = new Path();
0212         path.moveTo(fromX, fromY);
0213         path.lineTo(getX(), getY());
0214         swipeStoke = swipeStoke.continueStroke(path, 0, 5, true);
0215         GestureDescription.Builder builder = new GestureDescription.Builder();
0216         builder.addStroke(swipeStoke);
0217         return dispatchGesture(builder.build(), null, null);
0218     }
0219 
0220     @RequiresApi(api = Build.VERSION_CODES.O)
0221     public boolean stopSwipe() {
0222         Path path = new Path();
0223         path.moveTo(getX(), getY());
0224         if (swipeStoke == null) {
0225             return true;
0226         }
0227         swipeStoke = swipeStoke.continueStroke(path, 0, 1, false);
0228         GestureDescription.Builder builder = new GestureDescription.Builder();
0229         builder.addStroke(swipeStoke);
0230         swipeStoke = null;
0231         ((ImageView) cursorView.findViewById(R.id.mouse_cursor)).setImageResource(R.drawable.mouse_pointer);
0232         return dispatchGesture(builder.build(), null, null);
0233     }
0234 
0235 
0236     public static boolean scroll(double dx, double dy) {
0237         if (instance == null) return false;
0238 
0239         instance.scrollSum += dy;
0240         if (Math.signum(dy) != Math.signum(instance.scrollSum)) instance.scrollSum = dy;
0241         if (Math.abs(instance.scrollSum) < 500) return false;
0242         instance.scrollSum = 0;
0243 
0244         AccessibilityNodeInfo scrollable = instance.findNodeByAciton(instance.getRootInActiveWindow(),
0245                 dy > 0 ? AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD
0246                         : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
0247 
0248         if (scrollable == null) return false;
0249 
0250         return scrollable.performAction(dy > 0
0251                 ? AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId()
0252                 : AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD.getId()
0253         );
0254     }
0255 
0256     // https://codelabs.developers.google.com/codelabs/developing-android-a11y-service/#6
0257     private AccessibilityNodeInfo findNodeByAciton(AccessibilityNodeInfo root, AccessibilityNodeInfo.AccessibilityAction action) {
0258         Deque<AccessibilityNodeInfo> deque = new ArrayDeque<>();
0259         deque.add(root);
0260         while (!deque.isEmpty()) {
0261             AccessibilityNodeInfo node = deque.removeFirst();
0262             if (node.getActionList().contains(action)) {
0263                 return node;
0264             }
0265             for (int i = 0; i < node.getChildCount(); i++) {
0266                 deque.addLast(node.getChild(i));
0267             }
0268         }
0269         return null;
0270     }
0271 
0272     public static boolean backButton() {
0273         if (instance == null) return false;
0274         return instance.performGlobalAction(GLOBAL_ACTION_BACK);
0275     }
0276 
0277     public static boolean homeButton() {
0278         if (instance == null) return false;
0279         return instance.performGlobalAction(GLOBAL_ACTION_HOME);
0280     }
0281 
0282     public static boolean recentButton() {
0283         if (instance == null) return false;
0284         return instance.performGlobalAction(GLOBAL_ACTION_RECENTS);
0285     }
0286 
0287     public static boolean powerButton() {
0288         if (instance == null) return false;
0289 
0290         return instance.performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN);
0291     }
0292 
0293     @Override
0294     public void onDestroy() {
0295         super.onDestroy();
0296 
0297         if (windowManager != null && cursorView != null) {
0298             windowManager.removeView(cursorView);
0299         }
0300     }
0301 
0302     @Override
0303     public void onAccessibilityEvent(AccessibilityEvent event) {
0304 
0305     }
0306 
0307     @Override
0308     public void onInterrupt() {
0309 
0310     }
0311 }