tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 7bb08bc70a2d1e8461d79b5b56086f87ef7bcbfb
parent f61f1d17c2896038b9dc118c8b6cb6541b1d9b4d
Author: imnotlxy <contact@lxy.cc>
Date:   Tue, 28 Oct 2025 07:51:43 +0000

Bug 1993709 - Implement clear haptic feedback r=geckoview-reviewers,TYLin,m_kato

Differential Revision: https://phabricator.services.mozilla.com/D268306

Diffstat:
Mlayout/base/PresShell.cpp | 11++++++++++-
Mmobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java | 63++++++++++++++++++++++++++++++++-------------------------------
2 files changed, 42 insertions(+), 32 deletions(-)

diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp @@ -8262,6 +8262,14 @@ bool PresShell::EventHandler::MaybeHandleEventWithAccessibleCaret( return false; } + // AccessibleCaretEventHub::HandleEvent may return nsEventStatus_eIgnore to + // allow the context menu to appear correctly on desktop Firefox (see + // AccessibleCaretEventHub::LongTapState::OnLongTap). When this happens, and + // the event hub at the event location is the same as the hub attached to + // the currently‑focused window, HandleEvent would be invoked a second time. + // Avoid the duplicate call by explicitly checking for this condition. + AccessibleCaretEventHub* alreadyHandledEventHub = nullptr; + AutoEventTargetPointResetter autoEventTargetPointResetter(aGUIEvent); // First, try the event hub at the event point to handle a long press to // select a word in an unfocused window. @@ -8281,6 +8289,7 @@ bool PresShell::EventHandler::MaybeHandleEventWithAccessibleCaret( if (!eventHub) { break; } + alreadyHandledEventHub = eventHub.get(); *aEventStatus = eventHub->HandleEvent(aGUIEvent); if (*aEventStatus != nsEventStatus_eConsumeNoDefault) { @@ -8310,7 +8319,7 @@ bool PresShell::EventHandler::MaybeHandleEventWithAccessibleCaret( RefPtr<AccessibleCaretEventHub> eventHub = presShell->GetAccessibleCaretEventHub(); - if (!eventHub) { + if (!eventHub || eventHub.get() == alreadyHandledEventHub) { return false; } *aEventStatus = eventHub->HandleEvent(aGUIEvent); diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAppShell.java @@ -45,6 +45,7 @@ import android.os.Debug; import android.os.LocaleList; import android.os.Looper; import android.os.PowerManager; +import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings; import android.util.DisplayMetrics; @@ -202,12 +203,12 @@ public class GeckoAppShell { private static boolean sUseMaxScreenDepth; private static Float sScreenRefreshRate; - /* Is the value in sVibrationEndTime valid? */ - private static boolean sVibrationMaybePlaying; - - /* Time (in System.nanoTime() units) when the currently-playing vibration - * is scheduled to end. This value is valid only when - * sVibrationMaybePlaying is true. */ + /* + * Time (in System.nanoTime() units) when the currently-playing vibration + * is scheduled to end. This value could be zero when the vibration is + * cancelled. `System.nanoTime() > sVibrationEndTime` means there is not a + * playing vibration now. + */ private static long sVibrationEndTime; private static Sensor gAccelerometerSensor; @@ -962,16 +963,24 @@ public class GeckoAppShell { private static void performHapticFeedback(final boolean aIsLongPress) { // Don't perform haptic feedback if a vibration is currently playing, // because the haptic feedback will nuke the vibration. - if (!sVibrationMaybePlaying || System.nanoTime() >= sVibrationEndTime) { - final int[] pattern; - if (aIsLongPress) { - pattern = new int[] {0, 1, 20, 21}; + if (System.nanoTime() >= sVibrationEndTime) { + final VibrationEffect effect; + if (Build.VERSION.SDK_INT >= 29) { + // API level 29 introduces pre-defined vibration effects for better + // haptic feedback, prefer to use them. + if (aIsLongPress) { + effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK); + } else { + effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK); + } } else { - pattern = new int[] {0, 10, 20, 30}; + if (aIsLongPress) { + effect = VibrationEffect.createWaveform(new long[] {0, 1, 20, 21}, -1); + } else { + effect = VibrationEffect.createWaveform(new long[] {0, 10, 20, 30}, -1); + } } - vibrateOnHapticFeedbackEnabled(pattern); - sVibrationMaybePlaying = false; - sVibrationEndTime = 0; + vibrateOnHapticFeedbackEnabled(effect); } } @@ -979,26 +988,21 @@ public class GeckoAppShell { return (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE); } - // Helper method to convert integer array to long array. - private static long[] convertIntToLongArray(final int[] input) { - final long[] output = new long[input.length]; - for (int i = 0; i < input.length; i++) { - output[i] = input[i]; - } - return output; - } - // Vibrate only if haptic feedback is enabled. - private static void vibrateOnHapticFeedbackEnabled(final int[] milliseconds) { + @SuppressLint("MissingPermission") + private static void vibrateOnHapticFeedbackEnabled(final VibrationEffect effect) { if (Settings.System.getInt( getApplicationContext().getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) > 0) { - if (milliseconds.length == 1) { - vibrate(milliseconds[0]); - } else { - vibrate(convertIntToLongArray(milliseconds), -1); + // Here, sVibrationEndTime is not set. Compared to other kinds of + // vibration, haptic feedbacks are usually shorter and less important, + // which means it's ok to "nuke" them. + try { + vibrator().vibrate(effect); + } catch (final SecurityException ignore) { + Log.w(LOGTAG, "No VIBRATE permission"); } } } @@ -1007,7 +1011,6 @@ public class GeckoAppShell { @WrapForJNI(calledFrom = "gecko") private static void vibrate(final long milliseconds) { sVibrationEndTime = System.nanoTime() + milliseconds * 1000000; - sVibrationMaybePlaying = true; try { vibrator().vibrate(milliseconds); } catch (final SecurityException ignore) { @@ -1027,7 +1030,6 @@ public class GeckoAppShell { } sVibrationEndTime = System.nanoTime() + vibrationDuration * 1000000; - sVibrationMaybePlaying = true; try { vibrator().vibrate(pattern, repeat); } catch (final SecurityException ignore) { @@ -1038,7 +1040,6 @@ public class GeckoAppShell { @SuppressLint("MissingPermission") @WrapForJNI(calledFrom = "gecko") private static void cancelVibrate() { - sVibrationMaybePlaying = false; sVibrationEndTime = 0; try { vibrator().cancel();