Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 92841633 authored by Wenyu Zhang's avatar Wenyu Zhang Committed by Android (Google) Code Review
Browse files

Merge "a11y: Add autoclick indicator" into main

parents ad73834f 4463c552
Loading
Loading
Loading
Loading
+105 −0
Original line number Diff line number Diff line
@@ -16,11 +16,14 @@

package com.android.server.accessibility;

import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;

import android.accessibilityservice.AccessibilityTrace;
import android.annotation.NonNull;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.graphics.PixelFormat;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
@@ -30,6 +33,7 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;

/**
@@ -64,6 +68,9 @@ public class AutoclickController extends BaseEventStreamTransformation {
    // Lazily created on the first mouse motion event.
    private ClickScheduler mClickScheduler;
    private ClickDelayObserver mClickDelayObserver;
    private AutoclickIndicatorScheduler mAutoclickIndicatorScheduler;
    private AutoclickIndicatorView mAutoclickIndicatorView;
    private WindowManager mWindowManager;

    public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) {
        mTrace = trace;
@@ -84,6 +91,10 @@ public class AutoclickController extends BaseEventStreamTransformation {
                        new ClickScheduler(handler, AccessibilityManager.AUTOCLICK_DELAY_DEFAULT);
                mClickDelayObserver = new ClickDelayObserver(mUserId, handler);
                mClickDelayObserver.start(mContext.getContentResolver(), mClickScheduler);

                if (Flags.enableAutoclickIndicator()) {
                    initiateAutoclickIndicator(handler);
                }
            }

            handleMouseMotion(event, policyFlags);
@@ -94,6 +105,27 @@ public class AutoclickController extends BaseEventStreamTransformation {
        super.onMotionEvent(event, rawEvent, policyFlags);
    }

    private void initiateAutoclickIndicator(Handler handler) {
        mAutoclickIndicatorScheduler = new AutoclickIndicatorScheduler(handler);
        mAutoclickIndicatorView = new AutoclickIndicatorView(mContext);

        final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
        layoutParams.flags =
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
        layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
        layoutParams.setFitInsetsTypes(0);
        layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
        layoutParams.format = PixelFormat.TRANSLUCENT;
        layoutParams.setTitle(AutoclickIndicatorView.class.getSimpleName());
        layoutParams.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;

        mWindowManager = mContext.getSystemService(WindowManager.class);
        mWindowManager.addView(mAutoclickIndicatorView, layoutParams);
    }

    @Override
    public void onKeyEvent(KeyEvent event, int policyFlags) {
        if (mTrace.isA11yTracingEnabledForTypes(AccessibilityTrace.FLAGS_INPUT_FILTER)) {
@@ -130,6 +162,12 @@ public class AutoclickController extends BaseEventStreamTransformation {
            mClickScheduler.cancel();
            mClickScheduler = null;
        }

        if (mAutoclickIndicatorScheduler != null) {
            mAutoclickIndicatorScheduler.cancel();
            mAutoclickIndicatorScheduler = null;
            mWindowManager.removeView(mAutoclickIndicatorView);
        }
    }

    private void handleMouseMotion(MotionEvent event, int policyFlags) {
@@ -225,6 +263,62 @@ public class AutoclickController extends BaseEventStreamTransformation {
        }
    }

    private final class AutoclickIndicatorScheduler implements Runnable {
        private final Handler mHandler;
        private long mScheduledShowIndicatorTime;
        private boolean mIndicatorCallbackActive = false;

        public AutoclickIndicatorScheduler(Handler handler) {
            mHandler = handler;
        }

        @Override
        public void run() {
            long now = SystemClock.uptimeMillis();
            // Indicator was rescheduled after task was posted. Post new run task at updated time.
            if (now < mScheduledShowIndicatorTime) {
                mHandler.postDelayed(this, mScheduledShowIndicatorTime - now);
                return;
            }

            mAutoclickIndicatorView.redrawIndicator();
            mIndicatorCallbackActive = false;
        }

        public void update() {
            // TODO(b/383901288): update delay time once determined by UX.
            long SHOW_INDICATOR_DELAY_TIME = 150;
            long scheduledShowIndicatorTime =
                    SystemClock.uptimeMillis() + SHOW_INDICATOR_DELAY_TIME;
            // If there already is a scheduled show indicator at time before the updated time, just
            // update scheduled time.
            if (mIndicatorCallbackActive
                    && scheduledShowIndicatorTime > mScheduledShowIndicatorTime) {
                mScheduledShowIndicatorTime = scheduledShowIndicatorTime;
                return;
            }

            if (mIndicatorCallbackActive) {
                mHandler.removeCallbacks(this);
            }

            mIndicatorCallbackActive = true;
            mScheduledShowIndicatorTime = scheduledShowIndicatorTime;

            mHandler.postDelayed(this, SHOW_INDICATOR_DELAY_TIME);
        }

        public void cancel() {
            if (!mIndicatorCallbackActive) {
                return;
            }

            mIndicatorCallbackActive = false;
            mScheduledShowIndicatorTime = -1;
            mHandler.removeCallbacks(this);
        }
    }

    /**
     * Schedules and performs click event sequence that should be initiated when mouse pointer stops
     * moving. The click is first scheduled when a mouse movement is detected, and then further
@@ -305,6 +399,13 @@ public class AutoclickController extends BaseEventStreamTransformation {

            if (moved) {
                rescheduleClick(mDelay);

                if (Flags.enableAutoclickIndicator()) {
                    final int pointerIndex = event.getActionIndex();
                    mAutoclickIndicatorView.setCoordination(
                            event.getX(pointerIndex), event.getY(pointerIndex));
                    mAutoclickIndicatorScheduler.update();
                }
            }
        }

@@ -385,6 +486,10 @@ public class AutoclickController extends BaseEventStreamTransformation {
                mLastMotionEvent = null;
            }
            mScheduledClickTime = -1;

            if (Flags.enableAutoclickIndicator() && mAutoclickIndicatorView != null) {
                mAutoclickIndicatorView.clearIndicator();
            }
        }

        /**
+84 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.accessibility;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.DisplayMetrics;
import android.view.View;

// A visual indicator for the autoclick feature.
public class AutoclickIndicatorView extends View {
    private static final String TAG = AutoclickIndicatorView.class.getSimpleName();

    // TODO(b/383901288): allow users to customize the indicator area.
    static final float RADIUS = 50;

    private final Paint mPaint;

    // x and y coordinates of the visual indicator.
    private float mX;
    private float mY;

    // Status of whether the visual indicator should display or not.
    private boolean showIndicator = false;

    public AutoclickIndicatorView(Context context) {
        super(context);

        mPaint = new Paint();
        // TODO(b/383901288): update styling once determined by UX.
        mPaint.setARGB(255, 52, 103, 235);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(10);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (showIndicator) {
            canvas.drawCircle(mX, mY, RADIUS, mPaint);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Get the screen dimensions.
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        int screenWidth = displayMetrics.widthPixels;
        int screenHeight = displayMetrics.heightPixels;

        setMeasuredDimension(screenWidth, screenHeight);
    }

    public void setCoordination(float x, float y) {
        mX = x;
        mY = y;
    }

    public void redrawIndicator() {
        showIndicator = true;
        invalidate();
    }

    public void clearIndicator() {
        showIndicator = false;
        invalidate();
    }
}