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

Commit b50f9440 authored by Hiroki Sato's avatar Hiroki Sato
Browse files

Extract stylus compatibility logic into a separate class

This is a preparation of refactoring InputEventCompatProcessor.
Also this adds unit tests for it.

Bug: 369865835
Test: StylusButtonCompatibilityTest
Flag: EXEMPT refactor
Change-Id: I52767c2d0ce7a8556da5d1989b0b56c1f2a26eb1
parent 0989a990
Loading
Loading
Loading
Loading
+9 −12
Original line number Diff line number Diff line
@@ -17,9 +17,9 @@
package android.view;

import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.view.input.LetterboxScrollProcessor;
import android.view.input.StylusButtonCompatibility;

import com.android.window.flags.Flags;

@@ -36,6 +36,7 @@ public class InputEventCompatProcessor {

    protected Context mContext;
    protected int mTargetSdkVersion;
    private final StylusButtonCompatibility mStylusButtonCompatibility;
    private final LetterboxScrollProcessor mLetterboxScrollProcessor;

    /** List of events to be used to return the processed events */
@@ -48,6 +49,11 @@ public class InputEventCompatProcessor {
    public InputEventCompatProcessor(Context context, Handler handler) {
        mContext = context;
        mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
        if (StylusButtonCompatibility.isCompatibilityNeeded(context)) {
            mStylusButtonCompatibility = new StylusButtonCompatibility();
        } else {
            mStylusButtonCompatibility = null;
        }
        if (Flags.scrollingFromLetterbox()) {
            mLetterboxScrollProcessor = new LetterboxScrollProcessor(mContext, handler);
        } else {
@@ -119,18 +125,9 @@ public class InputEventCompatProcessor {
        return null;
    }


    private InputEvent processStylusButtonCompatibility(InputEvent inputEvent) {
        if (mTargetSdkVersion < Build.VERSION_CODES.M && inputEvent instanceof MotionEvent) {
            MotionEvent motion = (MotionEvent) inputEvent;
            final int mask =
                    MotionEvent.BUTTON_STYLUS_PRIMARY | MotionEvent.BUTTON_STYLUS_SECONDARY;
            final int buttonState = motion.getButtonState();
            final int compatButtonState = (buttonState & mask) >> 4;
            if (compatButtonState != 0) {
                motion.setButtonState(buttonState | compatButtonState);
            }
            return motion;
        if (mStylusButtonCompatibility != null) {
            return mStylusButtonCompatibility.processInputEventForCompatibility(inputEvent);
        }
        return null;
    }
+62 −0
Original line number Diff line number Diff line
/*
 * Copyright 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 android.view.input;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Build;
import android.view.InputEvent;
import android.view.MotionEvent;

/**
 * This rewrites stylus button events for an application targeting older SDK.
 *
 * @hide
 */
public class StylusButtonCompatibility {
    private static final int STYLUS_BUTTONS_MASK =
            MotionEvent.BUTTON_STYLUS_PRIMARY | MotionEvent.BUTTON_STYLUS_SECONDARY;

    /**
     * Returns {@code true} if this compatibility is required based on the given context.
     */
    public static boolean isCompatibilityNeeded(Context context) {
        return context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.M;
    }

    /**
     * Returns a rewritten event if compatibility is applied.
     * Returns a null if the event is not modified.
     */
    @Nullable
    public InputEvent processInputEventForCompatibility(@NonNull InputEvent inputEvent) {
        if (!(inputEvent instanceof MotionEvent motion)) {
            return null;
        }
        final int buttonState = motion.getButtonState();
        // BUTTON_STYLUS_PRIMARY and BUTTON_STYLUS_SECONDARY are mapped to
        // BUTTON_SECONDARY and BUTTON_TERTIARY respectively.
        final int compatButtonState = (buttonState & STYLUS_BUTTONS_MASK) >> 4;
        if (compatButtonState == 0) {
            // No need to rewrite.
            return null;
        }
        motion.setButtonState(buttonState | compatButtonState);
        return motion;
    }
}
+98 −0
Original line number Diff line number Diff line
/*
* Copyright 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 android.view.input

import android.content.Context
import android.os.Build
import android.os.SystemClock
import android.platform.test.annotations.Presubmit
import android.view.MotionEvent
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

/**
 * Tests for [StylusButtonCompatibility].
 *
 * Build/Install/Run:
 * atest FrameworksCoreTests:StylusButtonCompatibilityTest
 */
@SmallTest
@Presubmit
class StylusButtonCompatibilityTest {
    private lateinit var stylusButtonCompatibility: StylusButtonCompatibility
    private lateinit var context: Context

    @Before
    fun setUp() {
        context = InstrumentationRegistry.getInstrumentation().targetContext
        stylusButtonCompatibility = StylusButtonCompatibility()
    }

    @Test
    fun targetSdkMCompatibilityNotNeeded() {
        context.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.M

        assertFalse(StylusButtonCompatibility.isCompatibilityNeeded(context))
    }

    @Test
    fun targetSdkLCompatibilityNotNeeded() {
        context.getApplicationInfo().targetSdkVersion = Build.VERSION_CODES.LOLLIPOP_MR1

        assertTrue(StylusButtonCompatibility.isCompatibilityNeeded(context))
    }

    @Test
    fun primaryStylusButtonAddsSecondaryButton() {
        val event = MotionEvent.obtain(
            SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
            MotionEvent.ACTION_BUTTON_PRESS, /* x= */ 100f, /* y= */ 200f, /* metaState= */ 0
        )
        event.buttonState = MotionEvent.BUTTON_STYLUS_PRIMARY

        val result = stylusButtonCompatibility.processInputEventForCompatibility(event)

        assertNotNull(result)
        assertEquals(
            MotionEvent.BUTTON_SECONDARY or MotionEvent.BUTTON_STYLUS_PRIMARY,
            (result as MotionEvent).buttonState
        )
    }

    @Test
    fun secondaryStylusButtonAddsTertiaryButton() {
        val event = MotionEvent.obtain(
            SystemClock.uptimeMillis(), SystemClock.uptimeMillis(),
            MotionEvent.ACTION_BUTTON_PRESS, /* x= */ 100f, /* y= */ 200f, /* metaState= */ 0
        )
        event.buttonState = MotionEvent.BUTTON_STYLUS_SECONDARY

        val result = stylusButtonCompatibility.processInputEventForCompatibility(event)

        assertNotNull(result)
        assertEquals(
            MotionEvent.BUTTON_TERTIARY or MotionEvent.BUTTON_STYLUS_SECONDARY,
            (result as MotionEvent).buttonState
        )
    }
}