From 00ec5248cb2ab62f6fb576667e04f30f5a4c8ed5 Mon Sep 17 00:00:00 2001 From: Pat Manning Date: Tue, 29 Oct 2024 18:05:25 +0000 Subject: [PATCH] Add Settings page for three finger tap customization Test: TouchpadThreeFingerTapFragmentTest Test: TouchpadThreeFingerTapPreferenceControllerTest Flag: com.android.hardware.input.touchpad_three_finger_tap_shortcut Bug: 376249366 Change-Id: I63a9a1cbe5f5ed644c39e1a30aa724ea9e5088cb --- .../touchpad_three_finger_tap_layout.xml | 82 +++++++++++ res/values/dimens.xml | 2 + res/values/strings.xml | 12 ++ ...ouchpad_three_finger_tap_customization.xml | 29 ++++ res/xml/trackpad_settings.xml | 7 + .../TouchpadThreeFingerTapFragment.java | 57 ++++++++ ...padThreeFingerTapPreferenceController.java | 107 ++++++++++++++ .../TouchpadThreeFingerTapSelector.java | 98 +++++++++++++ ...hreeFingerTapPreferenceControllerTest.java | 131 ++++++++++++++++++ 9 files changed, 525 insertions(+) create mode 100644 res/layout/touchpad_three_finger_tap_layout.xml create mode 100644 res/xml/input_touchpad_three_finger_tap_customization.xml create mode 100644 src/com/android/settings/inputmethod/TouchpadThreeFingerTapFragment.java create mode 100644 src/com/android/settings/inputmethod/TouchpadThreeFingerTapPreferenceController.java create mode 100644 src/com/android/settings/inputmethod/TouchpadThreeFingerTapSelector.java create mode 100644 tests/robotests/src/com/android/settings/inputmethod/TouchpadThreeFingerTapPreferenceControllerTest.java 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)); + } +}