diff --git a/res/layout/touchpad_three_finger_tap_layout.xml b/res/layout/touchpad_three_finger_tap_layout.xml
new file mode 100644
index 00000000000..7f96bf3d1ea
--- /dev/null
+++ b/res/layout/touchpad_three_finger_tap_layout.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 96bbaed9b5b..e69502a4713 100755
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -200,6 +200,8 @@
8dp
- 1.0
- 2.5
+ 8dp
+ 21dp
40dp
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5394856eef5..4d5d5c2e936 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4649,6 +4649,8 @@
Click in the bottom right corner of the touchpad for more options
Pointer speed
+
+ Use three finger tap
Pointer color
@@ -4677,6 +4679,16 @@
trackpad, track pad, mouse, cursor, scroll, swipe, right click, click, pointer
right click, tap
+
+ Middle click
+
+ Launch Gemini
+
+ Go home
+
+ Go back
+
+ View recent apps
Go home
diff --git a/res/xml/input_touchpad_three_finger_tap_customization.xml b/res/xml/input_touchpad_three_finger_tap_customization.xml
new file mode 100644
index 00000000000..f0103aeda73
--- /dev/null
+++ b/res/xml/input_touchpad_three_finger_tap_customization.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
diff --git a/res/xml/trackpad_settings.xml b/res/xml/trackpad_settings.xml
index 2f7c7fcdd4a..cdfd398d151 100644
--- a/res/xml/trackpad_settings.xml
+++ b/res/xml/trackpad_settings.xml
@@ -55,6 +55,13 @@
settings:controller="com.android.settings.inputmethod.TrackpadTapDraggingPreferenceController"
android:order="35"/>
+
+
mKeyGestureTypeNameMap;
+ private final MetricsFeatureProvider mMetricsFeatureProvider;
+ private @Nullable Preference mPreference;
+
+ public TouchpadThreeFingerTapPreferenceController(@NonNull Context context,
+ @NonNull String key) {
+ super(context, key);
+ mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
+
+ mKeyGestureTypeNameMap = Map.ofEntries(
+ Map.entry(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
+ context.getString(R.string.three_finger_tap_launch_gemini)),
+ Map.entry(KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
+ context.getString(R.string.three_finger_tap_go_home)),
+ Map.entry(KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
+ context.getString(R.string.three_finger_tap_go_back)),
+ Map.entry(KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
+ context.getString(R.string.three_finger_tap_recent_apps)),
+ Map.entry(KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED,
+ context.getString(R.string.three_finger_tap_middle_click)));
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ boolean isTouchpad = NewKeyboardSettingsUtils.isTouchpad();
+ return (InputSettings.isTouchpadThreeFingerTapShortcutFeatureFlagEnabled() && isTouchpad)
+ ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public int getSliceHighlightMenuRes() {
+ return R.string.menu_key_system;
+ }
+
+ @Override
+ public @Nullable CharSequence getSummary() {
+ int currentType = Settings.System.getIntForUser(mContext.getContentResolver(),
+ Settings.System.TOUCHPAD_THREE_FINGER_TAP_CUSTOMIZATION,
+ KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED, UserHandle.USER_CURRENT);
+ return mKeyGestureTypeNameMap.get(currentType);
+ }
+
+ @Override
+ public void displayPreference(@NonNull PreferenceScreen screen) {
+ super.displayPreference(screen);
+ mPreference = screen.findPreference(getPreferenceKey());
+ refreshSummary(mPreference);
+ }
+
+ @Override
+ public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
+ @NonNull Lifecycle.Event event) {
+ refreshSummary(mPreference);
+ if (event == Lifecycle.Event.ON_PAUSE) {
+ int currentValue =
+ Settings.System.getIntForUser(mContext.getContentResolver(),
+ Settings.System.TOUCHPAD_THREE_FINGER_TAP_CUSTOMIZATION,
+ KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED, UserHandle.USER_CURRENT);
+ mMetricsFeatureProvider.action(mContext,
+ SettingsEnums.ACTION_TOUCHPAD_THREE_FINGER_TAP_CUSTOMIZATION_CHANGED,
+ currentValue);
+ }
+ }
+}
diff --git a/src/com/android/settings/inputmethod/TouchpadThreeFingerTapSelector.java b/src/com/android/settings/inputmethod/TouchpadThreeFingerTapSelector.java
new file mode 100644
index 00000000000..164098b808b
--- /dev/null
+++ b/src/com/android/settings/inputmethod/TouchpadThreeFingerTapSelector.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2024 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.settings.inputmethod;
+
+import static android.hardware.input.InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP;
+import static android.hardware.input.InputGestureData.createTouchpadTrigger;
+
+import android.content.Context;
+import android.hardware.input.InputGestureData;
+import android.hardware.input.InputManager;
+import android.hardware.input.KeyGestureEvent;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.view.PointerIcon;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+
+public class TouchpadThreeFingerTapSelector extends Preference {
+ private static final InputGestureData.Trigger THREE_FINGER_TAP_TOUCHPAD_TRIGGER =
+ createTouchpadTrigger(TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP);
+ private final InputManager mInputManager;
+
+ public TouchpadThreeFingerTapSelector(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ setLayoutResource(R.layout.touchpad_three_finger_tap_layout);
+ mInputManager = context.getSystemService(InputManager.class);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ LinearLayout buttonHolder = (LinearLayout) holder.findViewById(R.id.button_holder);
+ // Intercept hover events so setting row does not highlight when hovering buttons.
+ buttonHolder.setOnHoverListener((v, e) -> true);
+
+ int currentCustomization = Settings.System.getIntForUser(getContext().getContentResolver(),
+ Settings.System.TOUCHPAD_THREE_FINGER_TAP_CUSTOMIZATION,
+ KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED, UserHandle.USER_CURRENT);
+ initRadioButton(holder, R.id.launch_gemini,
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, currentCustomization);
+ initRadioButton(holder, R.id.go_home, KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
+ currentCustomization);
+ initRadioButton(holder, R.id.go_back, KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
+ currentCustomization);
+ initRadioButton(holder, R.id.recent_apps, KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
+ currentCustomization);
+ initRadioButton(holder, R.id.middle_click,
+ KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED, currentCustomization);
+ }
+
+ private void initRadioButton(@NonNull PreferenceViewHolder holder, int id,
+ int customGestureType, int currentCustomization) {
+ RadioButton radioButton = (RadioButton) holder.findViewById(id);
+ if (radioButton == null) {
+ return;
+ }
+ boolean isUnspecified = customGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED;
+ InputGestureData gesture = isUnspecified ? null : new InputGestureData.Builder()
+ .setTrigger(THREE_FINGER_TAP_TOUCHPAD_TRIGGER)
+ .setKeyGestureType(customGestureType)
+ .build();
+ radioButton.setOnCheckedChangeListener((v, isChecked) -> {
+ if (isChecked) {
+ mInputManager.removeAllCustomInputGestures(InputGestureData.Filter.TOUCHPAD);
+ if (!isUnspecified) {
+ mInputManager.addCustomInputGesture(gesture);
+ }
+ Settings.System.putIntForUser(getContext().getContentResolver(),
+ Settings.System.TOUCHPAD_THREE_FINGER_TAP_CUSTOMIZATION, customGestureType,
+ UserHandle.USER_CURRENT);
+ }
+ });
+ radioButton.setChecked(currentCustomization == customGestureType);
+ radioButton.setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_ARROW));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/inputmethod/TouchpadThreeFingerTapPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/TouchpadThreeFingerTapPreferenceControllerTest.java
new file mode 100644
index 00000000000..b39fb3cb7c6
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/inputmethod/TouchpadThreeFingerTapPreferenceControllerTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2024 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.settings.inputmethod;
+
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
+import static com.android.settings.core.BasePreferenceController.AVAILABLE;
+import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+import android.view.InputDevice;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.shadow.ShadowInputDevice;
+import com.android.settings.testutils.shadow.ShadowSystemSettings;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link TouchpadThreeFingerTapPreferenceController} */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {
+ ShadowSystemSettings.class,
+ ShadowInputDevice.class,
+})
+public class TouchpadThreeFingerTapPreferenceControllerTest {
+ @Rule
+ public MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+ @Mock
+ LifecycleOwner mLifecycleOwner;
+
+ private Context mContext;
+ private TouchpadThreeFingerTapPreferenceController mController;
+ private FakeFeatureFactory mFeatureFactory;
+
+ @Before
+ public void setUp() {
+ mContext = ApplicationProvider.getApplicationContext();
+ mFeatureFactory = FakeFeatureFactory.setupForTest();
+ mController = new TouchpadThreeFingerTapPreferenceController(mContext, "three_finger_tap");
+ ShadowInputDevice.reset();
+ }
+
+ @Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_TOUCHPAD_THREE_FINGER_TAP_SHORTCUT)
+ public void getAvailabilityStatus_flagEnabledHasTouchPad() {
+ int deviceId = 1;
+ ShadowInputDevice.sDeviceIds = new int[]{deviceId};
+ InputDevice device = ShadowInputDevice.makeInputDevicebyIdWithSources(deviceId,
+ InputDevice.SOURCE_TOUCHPAD);
+ ShadowInputDevice.addDevice(deviceId, device);
+
+ assertEquals(mController.getAvailabilityStatus(), AVAILABLE);
+ }
+
+ @Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_TOUCHPAD_THREE_FINGER_TAP_SHORTCUT)
+ public void getAvailabilityStatus_flagEnabledNoTouchPad() {
+ int deviceId = 1;
+ ShadowInputDevice.sDeviceIds = new int[]{deviceId};
+ InputDevice device = ShadowInputDevice.makeInputDevicebyIdWithSources(deviceId,
+ InputDevice.SOURCE_BLUETOOTH_STYLUS);
+ ShadowInputDevice.addDevice(deviceId, device);
+
+ assertEquals(mController.getAvailabilityStatus(), CONDITIONALLY_UNAVAILABLE);
+ }
+
+ @Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_TOUCHPAD_THREE_FINGER_TAP_SHORTCUT)
+ public void getAvailabilityStatus_flagDisabled() {
+ int deviceId = 1;
+ ShadowInputDevice.sDeviceIds = new int[]{deviceId};
+ InputDevice device = ShadowInputDevice.makeInputDevicebyIdWithSources(deviceId,
+ InputDevice.SOURCE_TOUCHPAD);
+ ShadowInputDevice.addDevice(deviceId, device);
+
+ assertEquals(mController.getAvailabilityStatus(), CONDITIONALLY_UNAVAILABLE);
+ }
+
+ @Test
+ public void onPause_logCurrentFillValue() {
+ int customizationValue = 1;
+ Settings.System.putIntForUser(mContext.getContentResolver(),
+ Settings.System.TOUCHPAD_THREE_FINGER_TAP_CUSTOMIZATION, customizationValue,
+ UserHandle.USER_CURRENT);
+
+ mController.onStateChanged(mLifecycleOwner, Lifecycle.Event.ON_PAUSE);
+
+ verify(mFeatureFactory.metricsFeatureProvider).action(
+ any(), eq(SettingsEnums.ACTION_TOUCHPAD_THREE_FINGER_TAP_CUSTOMIZATION_CHANGED),
+ eq(customizationValue));
+ }
+}