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

Commit 13a6bb7f authored by Hiroki Sato's avatar Hiroki Sato
Browse files

Add a framework for Mouse-to-Touch per-app overrides compatibility

This adds internal APIs for implementing mouse-to-touch compatiblity
and it's per-app overrides.

Actual compatibility logic will be implemented in the next CL in
MouseToTouchProcessor.

Bug: 413207127
Test: MouseToTouchProcessorTest
Flag: com.android.hardware.input.mouse_to_touch_per_app_compat
Change-Id: Id9f216a55af01ac3216937e68769c80231bba7f3
parent 6d36f824
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
@@ -1687,6 +1687,32 @@ public class ActivityInfo extends ComponentInfo implements Parcelable {
    @Disabled
    public static final long OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS = 388014743L;

    /**
     * This change id converts {@link android.view.MotionEvent} from a mouse device into touch
     * events by rewriting its source and tool type when they're delivered to the application.
     *
     * <p>Some apps don't work well with mouse events. The override enabled by this change id allows
     * them to work better with mouse devices by simulating touch events. This is disabled by
     * default, and can be enabled by device manufacturers on a per-application basis, controlled
     * via
     * <a href="https://developer.android.com/guide/practices/device-compatibility-mode#device_manufacturer_per-app_overrides">Device manufacturer per-app overrides</a>.
     *
     * <p>App developers whose apps can correctly handle mouse events but are affected by this
     * override can opt-out by declaring the {@link PackageManager.FEATURE_PC} feature in the
     * application's manifest.
     *
     * <p><b>Syntax to opt-out:</b>
     * <pre>
     * &lt;uses-feature android:name="android.hardware.type.pc"
     *               android:required="false" /&gt;
     * </pre>
     * @hide
     */
    @ChangeId
    @Overridable
    @Disabled
    public static final long OVERRIDE_MOUSE_TO_TOUCH = 413207127L;

    /**
     * Optional set of a certificates identifying apps that are allowed to embed this activity. From
     * the "knownActivityEmbeddingCerts" attribute.
+7 −0
Original line number Diff line number Diff line
@@ -215,3 +215,10 @@ flag {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "mouse_to_touch_per_app_compat"
    namespace: "lse_desktop_experience"
    description: "Enables per-app overrides compatibility for mouse to touch"
    bug: "413207127"
}
+42 −0
Original line number Diff line number Diff line
@@ -18,8 +18,14 @@ package android.view.input;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.compat.CompatChanges;
import android.content.Context;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.view.InputEvent;
import android.view.InputEventCompatProcessor;
@@ -128,6 +134,11 @@ public class InputEventCompatHandler {
            }
        }

        if (MouseToTouchProcessor.isCompatibilityNeeded(context)) {
            chainHead = new InputEventCompatHandler(
                    new MouseToTouchProcessor(context, handler), chainHead);
        }

        if (LetterboxScrollProcessor.isCompatibilityNeeded()) {
            chainHead = new InputEventCompatHandler(
                    new LetterboxScrollProcessor(context, handler), chainHead);
@@ -140,4 +151,35 @@ public class InputEventCompatHandler {

        return chainHead;
    }

    /**
     * Return whether the compatibility of given change ID is required for the given context.
     * If the application declares {@link PackageManager.FEATURE_PC} in the manifest, the
     * compatibility is not required.
     */
    static boolean isPcInputCompatibilityNeeded(Context context, long changeId) {
        if (ActivityThread.isSystem() || !CompatChanges.isChangeEnabled(changeId)) {
            return false;
        }

        // Enabled by the device manufacturer. Check if the app opts out.
        try {
            final PackageInfo pkgInfo = context.getPackageManager()
                    .getPackageInfo(context.getPackageName(), PackageManager.GET_CONFIGURATIONS);
            if (pkgInfo.reqFeatures != null) {
                for (FeatureInfo feature : pkgInfo.reqFeatures) {
                    if (TextUtils.equals(feature.name, PackageManager.FEATURE_PC)) {
                        return false;
                    }
                }
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Cannot obtain package info.", e);
            // pass through.
        }

        Log.i(TAG,
                "Input compatibility " + changeId + " is enabled for " + context.getPackageName());
        return true;
    }
}
+71 −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 android.view.input;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.os.Handler;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.InputEventCompatProcessor;
import android.view.MotionEvent;

import java.util.List;

/**
 * This rewrites {@link MotionEvent} to have {@link MotionEvent#TOOL_TYPE_FINGER} and
 * {@link InputDevice.SOURCE_TOUCHSCREEN} if the event is from mouse (or touchpad) if per-app
 * overrides is enabled on the target application.
 *
 * @hide
 */
public class MouseToTouchProcessor extends InputEventCompatProcessor {
    private static final String TAG = MouseToTouchProcessor.class.getSimpleName();

    /**
     * Return {@code true} if this compatibility is required based on the given context.
     *
     * <p>For debugging, you can toggle this by the following command:
     * - adb shell am compat enable|disable OVERRIDE_MOUSE_TO_TOUCH [pkg_name]
     */
    public static boolean isCompatibilityNeeded(Context context) {
        if (!com.android.hardware.input.Flags.mouseToTouchPerAppCompat()) {
            return false;
        }

        return InputEventCompatHandler.isPcInputCompatibilityNeeded(
                context, ActivityInfo.OVERRIDE_MOUSE_TO_TOUCH);
    }

    public MouseToTouchProcessor(Context context, Handler handler) {
        super(context, handler);
    }

    @Override
    public List<InputEvent> processInputEventForCompatibility(@NonNull InputEvent event) {
        // TODO(b/413207127): Implement the feature.
        return null;
    }

    @Nullable
    @Override
    public InputEvent processInputEventBeforeFinish(@NonNull InputEvent inputEvent) {
        return inputEvent;
    }
}
+121 −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.compat.testing.PlatformCompatChangeRule
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.pm.FeatureInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.Presubmit
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.hardware.input.Flags
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock

/**
 * Tests for [MouseToTouchProcessor].
 *
 * Build/Install/Run:
 * atest FrameworksCoreTests:MouseToTouchProcessorTest
 */
@SmallTest
@Presubmit
class MouseToTouchProcessorTest {
    private lateinit var processor: MouseToTouchProcessor
    private lateinit var context: Context

    @get:Rule
    val setFlagsRule: SetFlagsRule = SetFlagsRule()

    @get:Rule
    val compatChangeRule = PlatformCompatChangeRule()

    @Before
    fun setUp() {
        context = InstrumentationRegistry.getInstrumentation().targetContext
        processor = MouseToTouchProcessor(context, null)
    }

    @Test
    @DisableFlags(Flags.FLAG_MOUSE_TO_TOUCH_PER_APP_COMPAT)
    fun compatibilityNotNeededIfFlagIsDisabled() {
        assertThat(MouseToTouchProcessor.isCompatibilityNeeded(context), equalTo(false))
    }

    @Test
    @EnableFlags(Flags.FLAG_MOUSE_TO_TOUCH_PER_APP_COMPAT)
    @DisableCompatChanges(ActivityInfo.OVERRIDE_MOUSE_TO_TOUCH)
    fun compatibilityNotNeededIfCompatChangesDisabled() {
        assertThat(MouseToTouchProcessor.isCompatibilityNeeded(context), equalTo(false))
    }

    @Test
    @EnableFlags(Flags.FLAG_MOUSE_TO_TOUCH_PER_APP_COMPAT)
    @EnableCompatChanges(ActivityInfo.OVERRIDE_MOUSE_TO_TOUCH)
    fun compatibilityNotNeededIfCompatChangesEnabled() {
        assertThat(MouseToTouchProcessor.isCompatibilityNeeded(context), equalTo(true))
    }

    @Test
    @EnableFlags(Flags.FLAG_MOUSE_TO_TOUCH_PER_APP_COMPAT)
    @EnableCompatChanges(ActivityInfo.OVERRIDE_MOUSE_TO_TOUCH)
    fun compatibilityNotNeededIfFeaturePCPresent() {
        val mockPackageInfo = PackageInfo().apply {
            reqFeatures = arrayOf(FeatureInfo().apply { name = PackageManager.FEATURE_PC })
        }
        val packageManager = mock<PackageManager> {
            on { getPackageInfo(anyOrNull<String>(), any<Int>()) } doReturn mockPackageInfo
        }
        val mockContext = mock<Context> {
            on { getPackageManager() } doReturn packageManager
        }

        assertThat(MouseToTouchProcessor.isCompatibilityNeeded(mockContext), equalTo(false))
    }

    @Test
    @EnableFlags(Flags.FLAG_MOUSE_TO_TOUCH_PER_APP_COMPAT)
    @EnableCompatChanges(ActivityInfo.OVERRIDE_MOUSE_TO_TOUCH)
    fun compatibilityNeededIfFeaturePCNotPresent() {
        val mockPackageInfo = PackageInfo().apply {
            reqFeatures = arrayOf(FeatureInfo().apply { name = PackageManager.FEATURE_TOUCHSCREEN })
        }
        val packageManager = mock<PackageManager> {
            on { getPackageInfo(anyOrNull<String>(), any<Int>()) } doReturn mockPackageInfo
        }
        val mockContext = mock<Context> {
            on { getPackageManager() } doReturn packageManager
        }

        assertThat(MouseToTouchProcessor.isCompatibilityNeeded(mockContext), equalTo(true))
    }
}