@capgo/capacitor-updater 8.47.9 → 8.47.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -323,8 +323,9 @@ CapacitorUpdater can be configured with these options:
323
323
  | **`keepUrlPathAfterReload`** | <code>boolean</code> | Configure the plugin to keep the URL path after a reload. WARNING: When a reload is triggered, 'window.history' will be cleared. | <code>false</code> | 6.8.0 |
324
324
  | **`disableJSLogging`** | <code>boolean</code> | Disable the JavaScript logging of the plugin. if true, the plugin will not log to the JavaScript console. only the native log will be done | <code>false</code> | 7.3.0 |
325
325
  | **`osLogging`** | <code>boolean</code> | Enable OS-level logging. When enabled, logs are written to the system log which can be inspected in production builds. - **iOS**: Uses os_log instead of Swift.print, logs accessible via Console.app or Instruments - **Android**: Logs to Logcat (android.util.Log) When set to false, system logging is disabled on both platforms (only JavaScript console logging will occur if enabled). This is useful for debugging production apps (App Store/TestFlight builds on iOS, or production APKs on Android). | <code>true</code> | 8.42.0 |
326
- | **`shakeMenu`** | <code>boolean</code> | Enable the shake gesture while a preview session is active. Outside preview sessions this preview menu is ignored, unless {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector} is enabled. | <code>false</code> | 7.5.0 |
327
- | **`allowShakeChannelSelector`** | <code>boolean</code> | Enable the shake gesture to show a channel selector menu for switching between update channels. If {@link PluginsConfig.CapacitorUpdater.shakeMenu} is also enabled while a preview session is active, the shake menu includes both preview actions and channel switching. Only available for Android and iOS. | <code>false</code> | 8.43.0 |
326
+ | **`shakeMenu`** | <code>boolean</code> | Enable the native preview menu gesture while a preview session is active. Outside preview sessions this preview menu is ignored, unless {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector} is enabled. | <code>false</code> | 7.5.0 |
327
+ | **`shakeMenuGesture`** | <code><a href="#shakemenugesture">ShakeMenuGesture</a></code> | Choose which native gesture opens the preview/channel menu. This applies to both {@link PluginsConfig.CapacitorUpdater.shakeMenu} and {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector}. Only available for Android and iOS. | <code>'shake'</code> | 8.48.0 |
328
+ | **`allowShakeChannelSelector`** | <code>boolean</code> | Enable the native menu gesture to show a channel selector menu for switching between update channels. If {@link PluginsConfig.CapacitorUpdater.shakeMenu} is also enabled while a preview session is active, the shake menu includes both preview actions and channel switching. The native gesture can be changed with {@link PluginsConfig.CapacitorUpdater.shakeMenuGesture}. Only available for Android and iOS. | <code>false</code> | 8.43.0 |
328
329
 
329
330
  ### Examples
330
331
 
@@ -370,6 +371,7 @@ In `capacitor.config.json`:
370
371
  "disableJSLogging": undefined,
371
372
  "osLogging": undefined,
372
373
  "shakeMenu": undefined,
374
+ "shakeMenuGesture": undefined,
373
375
  "allowShakeChannelSelector": undefined
374
376
  }
375
377
  }
@@ -422,6 +424,7 @@ const config: CapacitorConfig = {
422
424
  disableJSLogging: undefined,
423
425
  osLogging: undefined,
424
426
  shakeMenu: undefined,
427
+ shakeMenuGesture: undefined,
425
428
  allowShakeChannelSelector: undefined,
426
429
  },
427
430
  },
@@ -1826,9 +1829,9 @@ Use this to:
1826
1829
  setShakeMenu(options: SetShakeMenuOptions) => Promise<void>
1827
1830
  ```
1828
1831
 
1829
- Enable or disable the shake gesture menu.
1832
+ Enable or disable the native preview menu gesture.
1830
1833
 
1831
- During preview sessions, users can shake their device to:
1834
+ During preview sessions, users can use the configured native gesture to:
1832
1835
  - Reload the current preview
1833
1836
  - Leave the test app and return to the fallback bundle
1834
1837
  - Switch update channel, when {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector} is also enabled
@@ -1838,7 +1841,9 @@ shown outside preview sessions when {@link PluginsConfig.CapacitorUpdater.allowS
1838
1841
 
1839
1842
  **Important:** Disable this in production builds or only enable for internal testers.
1840
1843
 
1841
- Can also be configured via {@link PluginsConfig.CapacitorUpdater.shakeMenu}.
1844
+ This can also be configured via {@link PluginsConfig.CapacitorUpdater.shakeMenu}.
1845
+ The native gesture can be configured via {@link PluginsConfig.CapacitorUpdater.shakeMenuGesture}
1846
+ or `options.gesture`.
1842
1847
 
1843
1848
  | Param | Type |
1844
1849
  | ------------- | ------------------------------------------------------------------- |
@@ -1855,7 +1860,7 @@ Can also be configured via {@link PluginsConfig.CapacitorUpdater.shakeMenu}.
1855
1860
  isShakeMenuEnabled() => Promise<ShakeMenuEnabled>
1856
1861
  ```
1857
1862
 
1858
- Check if the shake gesture debug menu is currently enabled.
1863
+ Check if the native preview menu gesture is currently enabled.
1859
1864
 
1860
1865
  Returns the current state of the shake menu feature that can be toggled via
1861
1866
  {@link setShakeMenu} or configured via {@link PluginsConfig.CapacitorUpdater.shakeMenu}.
@@ -1878,13 +1883,15 @@ Use this to:
1878
1883
  setShakeChannelSelector(options: SetShakeChannelSelectorOptions) => Promise<void>
1879
1884
  ```
1880
1885
 
1881
- Enable or disable the shake channel selector at runtime.
1886
+ Enable or disable the channel selector menu gesture at runtime.
1882
1887
 
1883
- When enabled, shaking the device can show a channel selector, including outside preview sessions.
1888
+ When enabled, the configured native gesture can show a channel selector, including outside preview sessions.
1884
1889
  If {@link setShakeMenu} is also enabled while a preview session is active, the shake menu includes
1885
1890
  both preview actions and channel switching.
1886
1891
 
1887
- Can also be configured via {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector}.
1892
+ This can also be configured via {@link PluginsConfig.CapacitorUpdater.allowShakeChannelSelector}.
1893
+ The native gesture can be configured via {@link PluginsConfig.CapacitorUpdater.shakeMenuGesture}
1894
+ or {@link setShakeMenu}.
1888
1895
 
1889
1896
  | Param | Type |
1890
1897
  | ------------- | ----------------------------------------------------------------------------------------- |
@@ -2547,16 +2554,18 @@ State information for flexible update progress (Android only).
2547
2554
 
2548
2555
  ##### SetShakeMenuOptions
2549
2556
 
2550
- | Prop | Type |
2551
- | ------------- | -------------------- |
2552
- | **`enabled`** | <code>boolean</code> |
2557
+ | Prop | Type | Description | Default |
2558
+ | ------------- | ------------------------------------------------------------- | ----------------------------------------------------- | -------------------- |
2559
+ | **`enabled`** | <code>boolean</code> | | |
2560
+ | **`gesture`** | <code><a href="#shakemenugesture">ShakeMenuGesture</a></code> | Native gesture used to open the preview/channel menu. | <code>'shake'</code> |
2553
2561
 
2554
2562
 
2555
2563
  ##### ShakeMenuEnabled
2556
2564
 
2557
- | Prop | Type |
2558
- | ------------- | -------------------- |
2559
- | **`enabled`** | <code>boolean</code> |
2565
+ | Prop | Type | Description | Since |
2566
+ | ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
2567
+ | **`enabled`** | <code>boolean</code> | | |
2568
+ | **`gesture`** | <code><a href="#shakemenugesture">ShakeMenuGesture</a></code> | The currently configured native gesture used to open the preview/channel menu. Undefined means consumers should treat the gesture as the default `shake` behavior. | 8.48.0 |
2560
2569
 
2561
2570
 
2562
2571
  ##### SetShakeChannelSelectorOptions
@@ -2669,6 +2678,15 @@ Payload emitted by {@link CapacitorUpdaterPlugin.addListener} with `breakingAvai
2669
2678
  <code><a href="#majoravailableevent">MajorAvailableEvent</a></code>
2670
2679
 
2671
2680
 
2681
+ ##### ShakeMenuGesture
2682
+
2683
+ Native gesture options that open the shake menu.
2684
+
2685
+ Supported values are `shake` and `threeFingerPinch`.
2686
+
2687
+ <code>'shake' | 'threeFingerPinch'</code>
2688
+
2689
+
2672
2690
  #### Enums
2673
2691
 
2674
2692
 
@@ -79,6 +79,9 @@ import org.json.JSONObject;
79
79
  @CapacitorPlugin(name = "CapacitorUpdater")
80
80
  public class CapacitorUpdaterPlugin extends Plugin {
81
81
 
82
+ static final String SHAKE_MENU_GESTURE_SHAKE = "shake";
83
+ static final String SHAKE_MENU_GESTURE_THREE_FINGER_PINCH = "threeFingerPinch";
84
+
82
85
  private static final String AUTO_UPDATE_MODE_OFF = "off";
83
86
  private static final String AUTO_UPDATE_MODE_BACKGROUND = "atBackground";
84
87
  private static final String AUTO_UPDATE_MODE_INSTALL = "atInstall";
@@ -160,6 +163,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
160
163
  private volatile boolean onLaunchDirectUpdateUsed = false;
161
164
  Boolean shakeMenuEnabled = false;
162
165
  Boolean shakeChannelSelectorEnabled = false;
166
+ String shakeMenuGesture = SHAKE_MENU_GESTURE_SHAKE;
163
167
  volatile Boolean previewSessionEnabled = false;
164
168
  private Boolean previewSessionAlertPending = false;
165
169
  private volatile Boolean isLeavingPreviewForIncomingLink = false;
@@ -526,6 +530,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
526
530
  this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
527
531
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
528
532
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
533
+ this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
529
534
  this.previewSessionEnabled = Boolean.TRUE.equals(this.allowPreview) && this.prefs.getBoolean(PREVIEW_SESSION_PREF_KEY, false);
530
535
  if (!Boolean.TRUE.equals(this.allowPreview) && this.prefs.getBoolean(PREVIEW_SESSION_PREF_KEY, false)) {
531
536
  this.clearPreviewSessionBecauseDisabled();
@@ -1504,6 +1509,28 @@ public class CapacitorUpdaterPlugin extends Plugin {
1504
1509
  }
1505
1510
  }
1506
1511
 
1512
+ static String normalizedShakeMenuGesture(final String value) {
1513
+ if (value == null || value.trim().isEmpty()) {
1514
+ return SHAKE_MENU_GESTURE_SHAKE;
1515
+ }
1516
+ final String normalized = value.trim();
1517
+ if (SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(normalized)) {
1518
+ return SHAKE_MENU_GESTURE_THREE_FINGER_PINCH;
1519
+ }
1520
+ return SHAKE_MENU_GESTURE_SHAKE;
1521
+ }
1522
+
1523
+ static boolean isSupportedShakeMenuGesture(final String value) {
1524
+ if (value == null) {
1525
+ return true;
1526
+ }
1527
+ final String normalized = value.trim();
1528
+ if (normalized.isEmpty()) {
1529
+ return false;
1530
+ }
1531
+ return SHAKE_MENU_GESTURE_SHAKE.equals(normalized) || SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(normalized);
1532
+ }
1533
+
1507
1534
  static String autoUpdateModeForLegacyDirectUpdateMode(final String directUpdateMode) {
1508
1535
  switch (directUpdateMode) {
1509
1536
  case AUTO_UPDATE_MODE_INSTALL:
@@ -2519,7 +2546,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2519
2546
  }
2520
2547
 
2521
2548
  final BundleInfo previewFallbackBundle = this.implementation.getPreviewFallbackBundle();
2522
- this.endPreviewSession();
2549
+ this.endPreviewSession(true);
2523
2550
  final BundleInfo restoredNextBundle = this.implementation.getNextBundle();
2524
2551
  this.deletePreviewBundleIfUnused(previewBundle, previewFallbackBundle, restoredNextBundle);
2525
2552
  return true;
@@ -2736,6 +2763,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2736
2763
  this.hidePreviewTransitionLoader("preview-session-disabled");
2737
2764
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2738
2765
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
2766
+ this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
2739
2767
  this.syncShakeMenuLifecycle();
2740
2768
  this.clearPreviewSessionPreferences();
2741
2769
  }
@@ -2959,6 +2987,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
2959
2987
  this.implementation.previewSession = false;
2960
2988
  this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
2961
2989
  this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
2990
+ this.shakeMenuGesture = normalizedShakeMenuGesture(this.getConfig().getString("shakeMenuGesture", SHAKE_MENU_GESTURE_SHAKE));
2962
2991
  this.syncShakeMenuLifecycle();
2963
2992
  this.restorePreviewPreviousAppId();
2964
2993
  this.restorePreviewPreviousDefaultChannel();
@@ -2982,14 +3011,26 @@ public class CapacitorUpdaterPlugin extends Plugin {
2982
3011
  private void ensureShakeMenuStarted() {
2983
3012
  if (getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
2984
3013
  try {
2985
- shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
2986
- logger.info("Shake menu initialized");
3014
+ shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger, this.shakeMenuGesture);
3015
+ logger.info("Shake menu initialized with " + this.shakeMenuGesture + " gesture");
2987
3016
  } catch (Exception e) {
2988
3017
  logger.error("Failed to initialize shake menu: " + e.getMessage());
2989
3018
  }
2990
3019
  }
2991
3020
  }
2992
3021
 
3022
+ private void restartShakeMenuListener() {
3023
+ if (shakeMenu != null) {
3024
+ try {
3025
+ shakeMenu.stop();
3026
+ } catch (Exception e) {
3027
+ logger.error("Failed to restart shake menu listener: " + e.getMessage());
3028
+ }
3029
+ shakeMenu = null;
3030
+ }
3031
+ this.syncShakeMenuLifecycle();
3032
+ }
3033
+
2993
3034
  private void syncShakeMenuLifecycle() {
2994
3035
  if (this.shouldListenForShake()) {
2995
3036
  this.ensureShakeMenuStarted();
@@ -4423,10 +4464,29 @@ public class CapacitorUpdaterPlugin extends Plugin {
4423
4464
  return;
4424
4465
  }
4425
4466
 
4467
+ final String gesture = call.getString("gesture", null);
4468
+ final boolean gestureChanged;
4469
+ if (gesture != null) {
4470
+ if (!isSupportedShakeMenuGesture(gesture)) {
4471
+ logger.error("Unsupported shake menu gesture: " + gesture);
4472
+ call.reject("Unsupported shake menu gesture. Use \"shake\" or \"threeFingerPinch\".");
4473
+ return;
4474
+ }
4475
+ final String normalizedGesture = normalizedShakeMenuGesture(gesture);
4476
+ gestureChanged = !normalizedGesture.equals(this.shakeMenuGesture);
4477
+ this.shakeMenuGesture = normalizedGesture;
4478
+ } else {
4479
+ gestureChanged = false;
4480
+ }
4481
+
4426
4482
  this.shakeMenuEnabled = enabled;
4427
- logger.info("Shake menu " + (enabled ? "enabled" : "disabled"));
4483
+ logger.info("Shake menu " + (enabled ? "enabled" : "disabled") + " with " + this.shakeMenuGesture + " gesture");
4428
4484
 
4429
- this.syncShakeMenuLifecycle();
4485
+ if (gestureChanged) {
4486
+ this.restartShakeMenuListener();
4487
+ } else {
4488
+ this.syncShakeMenuLifecycle();
4489
+ }
4430
4490
 
4431
4491
  call.resolve();
4432
4492
  }
@@ -4436,6 +4496,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
4436
4496
  try {
4437
4497
  final JSObject ret = new JSObject();
4438
4498
  ret.put("enabled", this.shakeMenuEnabled);
4499
+ ret.put("gesture", this.shakeMenuGesture);
4439
4500
  call.resolve(ret);
4440
4501
  } catch (final Exception e) {
4441
4502
  logger.error("Could not get shake menu status " + e.getMessage());
@@ -17,8 +17,8 @@ public class ShakeDetector implements SensorEventListener {
17
17
  void onShakeDetected();
18
18
  }
19
19
 
20
- private static final float SHAKE_THRESHOLD = 12.0f; // Acceleration threshold for shake detection
21
- private static final int SHAKE_TIMEOUT = 500; // Minimum time between shake events (ms)
20
+ private static final float SHAKE_THRESHOLD = 16.0f; // Acceleration threshold for shake detection
21
+ private static final int SHAKE_TIMEOUT = 1000; // Minimum time between shake events (ms)
22
22
 
23
23
  private Listener listener;
24
24
  private SensorManager sensorManager;
@@ -34,7 +34,7 @@ public class ShakeDetector implements SensorEventListener {
34
34
  this.accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
35
35
 
36
36
  if (accelerometer != null) {
37
- sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
37
+ sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI);
38
38
  }
39
39
  }
40
40
 
@@ -24,7 +24,7 @@ import java.util.List;
24
24
  import java.util.Map;
25
25
  import org.json.JSONArray;
26
26
 
27
- public class ShakeMenu implements ShakeDetector.Listener {
27
+ public class ShakeMenu implements ShakeDetector.Listener, ThreeFingerPinchDetector.Listener {
28
28
 
29
29
  private interface PreviewMenuAction {
30
30
  boolean run();
@@ -33,28 +33,46 @@ public class ShakeMenu implements ShakeDetector.Listener {
33
33
  private CapacitorUpdaterPlugin plugin;
34
34
  private BridgeActivity activity;
35
35
  private ShakeDetector shakeDetector;
36
+ private ThreeFingerPinchDetector pinchDetector;
36
37
  private boolean isShowing = false;
37
38
  private Logger logger;
38
39
 
39
- public ShakeMenu(CapacitorUpdaterPlugin plugin, BridgeActivity activity, Logger logger) {
40
+ public ShakeMenu(CapacitorUpdaterPlugin plugin, BridgeActivity activity, Logger logger, String gesture) {
40
41
  this.plugin = plugin;
41
42
  this.activity = activity;
42
43
  this.logger = logger;
43
44
 
44
- SensorManager sensorManager = (SensorManager) activity.getSystemService(Activity.SENSOR_SERVICE);
45
- this.shakeDetector = new ShakeDetector(this);
46
- this.shakeDetector.start(sensorManager);
45
+ if (CapacitorUpdaterPlugin.SHAKE_MENU_GESTURE_THREE_FINGER_PINCH.equals(gesture)) {
46
+ this.pinchDetector = new ThreeFingerPinchDetector(this, logger);
47
+ this.pinchDetector.start(activity);
48
+ } else {
49
+ SensorManager sensorManager = (SensorManager) activity.getSystemService(Activity.SENSOR_SERVICE);
50
+ this.shakeDetector = new ShakeDetector(this);
51
+ this.shakeDetector.start(sensorManager);
52
+ }
47
53
  }
48
54
 
49
55
  public void stop() {
50
56
  if (shakeDetector != null) {
51
57
  shakeDetector.stop();
52
58
  }
59
+ if (pinchDetector != null) {
60
+ pinchDetector.stop();
61
+ }
53
62
  }
54
63
 
55
64
  @Override
56
65
  public void onShakeDetected() {
57
- logger.info("Shake detected");
66
+ onMenuGestureDetected("Shake");
67
+ }
68
+
69
+ @Override
70
+ public void onThreeFingerPinchDetected() {
71
+ onMenuGestureDetected("Three finger pinch");
72
+ }
73
+
74
+ private void onMenuGestureDetected(String gestureName) {
75
+ logger.info(gestureName + " detected");
58
76
 
59
77
  boolean canShowPreviewMenu = Boolean.TRUE.equals(plugin.shakeMenuEnabled) && plugin.hasActivePreviewSession();
60
78
  boolean canShowChannelSelector = Boolean.TRUE.equals(plugin.shakeChannelSelectorEnabled);
@@ -0,0 +1,169 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ package ee.forgr.capacitor_updater;
8
+
9
+ import android.view.MotionEvent;
10
+ import android.view.View;
11
+ import com.getcapacitor.Bridge;
12
+ import com.getcapacitor.BridgeActivity;
13
+ import java.lang.reflect.Field;
14
+
15
+ public class ThreeFingerPinchDetector implements View.OnTouchListener {
16
+
17
+ public interface Listener {
18
+ void onThreeFingerPinchDetected();
19
+ }
20
+
21
+ private static final int REQUIRED_POINTER_COUNT = 3;
22
+ private static final float MIN_SCALE_DELTA = 0.30f;
23
+ private static final long PINCH_TIMEOUT = 1000;
24
+
25
+ private final Listener listener;
26
+ private final Logger logger;
27
+ private View targetView;
28
+ private View.OnTouchListener previousOnTouchListener;
29
+ private boolean touchListenerInstalled = false;
30
+ private float initialSpan = 0;
31
+ private boolean tracking = false;
32
+ private boolean triggered = false;
33
+ private long lastPinchTime = 0;
34
+
35
+ public ThreeFingerPinchDetector(Listener listener, Logger logger) {
36
+ this.listener = listener;
37
+ this.logger = logger;
38
+ }
39
+
40
+ public void start(BridgeActivity activity) {
41
+ if (targetView != null) {
42
+ stop();
43
+ }
44
+
45
+ View view = null;
46
+ Bridge bridge = activity.getBridge();
47
+ if (bridge != null && bridge.getWebView() != null) {
48
+ view = bridge.getWebView();
49
+ }
50
+ if (view == null && activity.getWindow() != null) {
51
+ view = activity.getWindow().getDecorView().getRootView();
52
+ }
53
+ if (view == null) {
54
+ logger.warn("Three finger pinch detector could not find a target view");
55
+ return;
56
+ }
57
+
58
+ this.targetView = view;
59
+ this.previousOnTouchListener = getCurrentOnTouchListener(view);
60
+ if (this.previousOnTouchListener != this) {
61
+ this.targetView.setOnTouchListener(this);
62
+ this.touchListenerInstalled = true;
63
+ }
64
+ }
65
+
66
+ public void stop() {
67
+ if (targetView != null) {
68
+ View.OnTouchListener currentOnTouchListener = getCurrentOnTouchListener(targetView);
69
+ if (touchListenerInstalled && (currentOnTouchListener == this || currentOnTouchListener == null)) {
70
+ targetView.setOnTouchListener(previousOnTouchListener);
71
+ }
72
+ targetView = null;
73
+ previousOnTouchListener = null;
74
+ touchListenerInstalled = false;
75
+ }
76
+ reset();
77
+ }
78
+
79
+ @Override
80
+ public boolean onTouch(View view, MotionEvent event) {
81
+ boolean consumedByPreviousListener = false;
82
+ if (previousOnTouchListener != null && previousOnTouchListener != this) {
83
+ consumedByPreviousListener = previousOnTouchListener.onTouch(view, event);
84
+ }
85
+
86
+ int action = event.getActionMasked();
87
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
88
+ reset();
89
+ return consumedByPreviousListener;
90
+ }
91
+
92
+ if (event.getPointerCount() != REQUIRED_POINTER_COUNT) {
93
+ if (action == MotionEvent.ACTION_POINTER_DOWN) {
94
+ reset();
95
+ }
96
+ return consumedByPreviousListener;
97
+ }
98
+
99
+ float span = calculateSpan(event);
100
+ if (span <= 0) {
101
+ return consumedByPreviousListener;
102
+ }
103
+
104
+ if (!tracking || action == MotionEvent.ACTION_POINTER_DOWN) {
105
+ initialSpan = span;
106
+ tracking = true;
107
+ triggered = false;
108
+ return consumedByPreviousListener;
109
+ }
110
+
111
+ if (!triggered && Math.abs(span - initialSpan) / initialSpan >= MIN_SCALE_DELTA) {
112
+ long currentTime = System.currentTimeMillis();
113
+ if (currentTime - lastPinchTime > PINCH_TIMEOUT) {
114
+ triggered = true;
115
+ lastPinchTime = currentTime;
116
+ if (listener != null) {
117
+ listener.onThreeFingerPinchDetected();
118
+ }
119
+ }
120
+ }
121
+
122
+ return consumedByPreviousListener;
123
+ }
124
+
125
+ private float calculateSpan(MotionEvent event) {
126
+ float centerX = 0;
127
+ float centerY = 0;
128
+ for (int i = 0; i < REQUIRED_POINTER_COUNT; i++) {
129
+ centerX += event.getX(i);
130
+ centerY += event.getY(i);
131
+ }
132
+ centerX /= REQUIRED_POINTER_COUNT;
133
+ centerY /= REQUIRED_POINTER_COUNT;
134
+
135
+ float totalDistance = 0;
136
+ for (int i = 0; i < REQUIRED_POINTER_COUNT; i++) {
137
+ float dx = event.getX(i) - centerX;
138
+ float dy = event.getY(i) - centerY;
139
+ totalDistance += Math.sqrt(dx * dx + dy * dy);
140
+ }
141
+ return totalDistance / REQUIRED_POINTER_COUNT;
142
+ }
143
+
144
+ private View.OnTouchListener getCurrentOnTouchListener(View view) {
145
+ try {
146
+ Field listenerInfoField = View.class.getDeclaredField("mListenerInfo");
147
+ listenerInfoField.setAccessible(true);
148
+ Object listenerInfo = listenerInfoField.get(view);
149
+ if (listenerInfo == null) {
150
+ return null;
151
+ }
152
+ Field onTouchListenerField = listenerInfo.getClass().getDeclaredField("mOnTouchListener");
153
+ onTouchListenerField.setAccessible(true);
154
+ Object listener = onTouchListenerField.get(listenerInfo);
155
+ if (listener instanceof View.OnTouchListener) {
156
+ return (View.OnTouchListener) listener;
157
+ }
158
+ } catch (ReflectiveOperationException | RuntimeException exception) {
159
+ logger.warn("Three finger pinch detector could not inspect the current touch listener: " + exception.getMessage());
160
+ }
161
+ return null;
162
+ }
163
+
164
+ private void reset() {
165
+ initialSpan = 0;
166
+ tracking = false;
167
+ triggered = false;
168
+ }
169
+ }