@capgo/capacitor-updater 8.49.0 → 8.49.2

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.
@@ -6,13 +6,22 @@
6
6
 
7
7
  package ee.forgr.capacitor_updater;
8
8
 
9
+ import android.view.ActionMode;
10
+ import android.view.KeyEvent;
11
+ import android.view.KeyboardShortcutGroup;
12
+ import android.view.Menu;
13
+ import android.view.MenuItem;
9
14
  import android.view.MotionEvent;
15
+ import android.view.SearchEvent;
10
16
  import android.view.View;
11
- import com.getcapacitor.Bridge;
17
+ import android.view.Window;
18
+ import android.view.WindowManager;
19
+ import android.view.accessibility.AccessibilityEvent;
12
20
  import com.getcapacitor.BridgeActivity;
13
- import java.lang.reflect.Field;
21
+ import java.lang.ref.WeakReference;
22
+ import java.util.List;
14
23
 
15
- public class ThreeFingerPinchDetector implements View.OnTouchListener {
24
+ public class ThreeFingerPinchDetector {
16
25
 
17
26
  public interface Listener {
18
27
  void onThreeFingerPinchDetected();
@@ -24,9 +33,10 @@ public class ThreeFingerPinchDetector implements View.OnTouchListener {
24
33
 
25
34
  private final Listener listener;
26
35
  private final Logger logger;
27
- private View targetView;
28
- private View.OnTouchListener previousOnTouchListener;
29
- private boolean touchListenerInstalled = false;
36
+ private Window targetWindow;
37
+ private Window.Callback previousWindowCallback;
38
+ private Window.Callback windowCallback;
39
+ private WeakReference<ThreeFingerPinchDetector> detectorReference;
30
40
  private float initialSpan = 0;
31
41
  private boolean tracking = false;
32
42
  private boolean triggered = false;
@@ -38,74 +48,68 @@ public class ThreeFingerPinchDetector implements View.OnTouchListener {
38
48
  }
39
49
 
40
50
  public void start(BridgeActivity activity) {
41
- if (targetView != null) {
51
+ if (targetWindow != null) {
42
52
  stop();
43
53
  }
44
54
 
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
+ Window window = activity.getWindow();
56
+ if (window == null) {
57
+ logger.warn("Three finger pinch detector could not find a target window");
55
58
  return;
56
59
  }
57
60
 
58
- this.targetView = view;
59
- this.previousOnTouchListener = getCurrentOnTouchListener(view);
60
- if (this.previousOnTouchListener != this) {
61
- this.targetView.setOnTouchListener(this);
62
- this.touchListenerInstalled = true;
63
- }
61
+ this.targetWindow = window;
62
+ this.previousWindowCallback = window.getCallback();
63
+ this.detectorReference = new WeakReference<>(this);
64
+ this.windowCallback = new PinchWindowCallback(this.previousWindowCallback, this.detectorReference);
65
+ window.setCallback(this.windowCallback);
66
+ logger.info("Three finger pinch detector installed on activity window");
64
67
  }
65
68
 
66
69
  public void stop() {
67
- if (targetView != null) {
68
- View.OnTouchListener currentOnTouchListener = getCurrentOnTouchListener(targetView);
69
- if (touchListenerInstalled && (currentOnTouchListener == this || currentOnTouchListener == null)) {
70
- targetView.setOnTouchListener(previousOnTouchListener);
70
+ if (targetWindow != null) {
71
+ if (windowCallback instanceof PinchWindowCallback) {
72
+ ((PinchWindowCallback) windowCallback).disable();
71
73
  }
72
- targetView = null;
73
- previousOnTouchListener = null;
74
- touchListenerInstalled = false;
74
+ if (detectorReference != null) {
75
+ detectorReference.clear();
76
+ }
77
+ if (targetWindow.getCallback() == windowCallback) {
78
+ targetWindow.setCallback(previousWindowCallback);
79
+ }
80
+ targetWindow = null;
81
+ previousWindowCallback = null;
82
+ windowCallback = null;
83
+ detectorReference = null;
75
84
  }
76
85
  reset();
77
86
  }
78
87
 
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
-
88
+ private void handleTouch(MotionEvent event) {
86
89
  int action = event.getActionMasked();
87
90
  if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
88
91
  reset();
89
- return consumedByPreviousListener;
92
+ return;
90
93
  }
91
94
 
92
95
  if (event.getPointerCount() != REQUIRED_POINTER_COUNT) {
93
96
  if (action == MotionEvent.ACTION_POINTER_DOWN) {
94
97
  reset();
95
98
  }
96
- return consumedByPreviousListener;
99
+ return;
97
100
  }
98
101
 
99
102
  float span = calculateSpan(event);
100
103
  if (span <= 0) {
101
- return consumedByPreviousListener;
104
+ return;
102
105
  }
103
106
 
104
107
  if (!tracking || action == MotionEvent.ACTION_POINTER_DOWN) {
105
108
  initialSpan = span;
106
109
  tracking = true;
107
110
  triggered = false;
108
- return consumedByPreviousListener;
111
+ logger.info("Three finger pinch tracking started");
112
+ return;
109
113
  }
110
114
 
111
115
  if (!triggered && Math.abs(span - initialSpan) / initialSpan >= MIN_SCALE_DELTA) {
@@ -113,13 +117,12 @@ public class ThreeFingerPinchDetector implements View.OnTouchListener {
113
117
  if (currentTime - lastPinchTime > PINCH_TIMEOUT) {
114
118
  triggered = true;
115
119
  lastPinchTime = currentTime;
120
+ logger.info("Three finger pinch threshold reached");
116
121
  if (listener != null) {
117
122
  listener.onThreeFingerPinchDetected();
118
123
  }
119
124
  }
120
125
  }
121
-
122
- return consumedByPreviousListener;
123
126
  }
124
127
 
125
128
  private float calculateSpan(MotionEvent event) {
@@ -141,29 +144,180 @@ public class ThreeFingerPinchDetector implements View.OnTouchListener {
141
144
  return totalDistance / REQUIRED_POINTER_COUNT;
142
145
  }
143
146
 
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
147
  private void reset() {
165
148
  initialSpan = 0;
166
149
  tracking = false;
167
150
  triggered = false;
168
151
  }
152
+
153
+ private static class PinchWindowCallback implements Window.Callback {
154
+
155
+ private final Window.Callback delegate;
156
+ private final WeakReference<ThreeFingerPinchDetector> detectorReference;
157
+ private boolean enabled = true;
158
+
159
+ PinchWindowCallback(Window.Callback delegate, WeakReference<ThreeFingerPinchDetector> detectorReference) {
160
+ this.delegate = delegate;
161
+ this.detectorReference = detectorReference;
162
+ }
163
+
164
+ void disable() {
165
+ enabled = false;
166
+ if (detectorReference != null) {
167
+ detectorReference.clear();
168
+ }
169
+ }
170
+
171
+ @Override
172
+ public boolean dispatchKeyEvent(KeyEvent event) {
173
+ return delegate != null && delegate.dispatchKeyEvent(event);
174
+ }
175
+
176
+ @Override
177
+ public boolean dispatchKeyShortcutEvent(KeyEvent event) {
178
+ return delegate != null && delegate.dispatchKeyShortcutEvent(event);
179
+ }
180
+
181
+ @Override
182
+ public boolean dispatchTouchEvent(MotionEvent event) {
183
+ boolean handled = delegate != null && delegate.dispatchTouchEvent(event);
184
+ if (enabled) {
185
+ ThreeFingerPinchDetector detector = detectorReference == null ? null : detectorReference.get();
186
+ if (detector != null) {
187
+ detector.handleTouch(event);
188
+ }
189
+ }
190
+ return handled;
191
+ }
192
+
193
+ @Override
194
+ public boolean dispatchTrackballEvent(MotionEvent event) {
195
+ return delegate != null && delegate.dispatchTrackballEvent(event);
196
+ }
197
+
198
+ @Override
199
+ public boolean dispatchGenericMotionEvent(MotionEvent event) {
200
+ return delegate != null && delegate.dispatchGenericMotionEvent(event);
201
+ }
202
+
203
+ @Override
204
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
205
+ return delegate != null && delegate.dispatchPopulateAccessibilityEvent(event);
206
+ }
207
+
208
+ @Override
209
+ public View onCreatePanelView(int featureId) {
210
+ return delegate == null ? null : delegate.onCreatePanelView(featureId);
211
+ }
212
+
213
+ @Override
214
+ public boolean onCreatePanelMenu(int featureId, Menu menu) {
215
+ return delegate != null && delegate.onCreatePanelMenu(featureId, menu);
216
+ }
217
+
218
+ @Override
219
+ public boolean onPreparePanel(int featureId, View view, Menu menu) {
220
+ return delegate != null && delegate.onPreparePanel(featureId, view, menu);
221
+ }
222
+
223
+ @Override
224
+ public boolean onMenuOpened(int featureId, Menu menu) {
225
+ return delegate != null && delegate.onMenuOpened(featureId, menu);
226
+ }
227
+
228
+ @Override
229
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
230
+ return delegate != null && delegate.onMenuItemSelected(featureId, item);
231
+ }
232
+
233
+ @Override
234
+ public void onWindowAttributesChanged(WindowManager.LayoutParams attrs) {
235
+ if (delegate != null) {
236
+ delegate.onWindowAttributesChanged(attrs);
237
+ }
238
+ }
239
+
240
+ @Override
241
+ public void onContentChanged() {
242
+ if (delegate != null) {
243
+ delegate.onContentChanged();
244
+ }
245
+ }
246
+
247
+ @Override
248
+ public void onWindowFocusChanged(boolean hasFocus) {
249
+ if (delegate != null) {
250
+ delegate.onWindowFocusChanged(hasFocus);
251
+ }
252
+ }
253
+
254
+ @Override
255
+ public void onAttachedToWindow() {
256
+ if (delegate != null) {
257
+ delegate.onAttachedToWindow();
258
+ }
259
+ }
260
+
261
+ @Override
262
+ public void onDetachedFromWindow() {
263
+ if (delegate != null) {
264
+ delegate.onDetachedFromWindow();
265
+ }
266
+ }
267
+
268
+ @Override
269
+ public void onPanelClosed(int featureId, Menu menu) {
270
+ if (delegate != null) {
271
+ delegate.onPanelClosed(featureId, menu);
272
+ }
273
+ }
274
+
275
+ @Override
276
+ public boolean onSearchRequested() {
277
+ return delegate != null && delegate.onSearchRequested();
278
+ }
279
+
280
+ @Override
281
+ public boolean onSearchRequested(SearchEvent searchEvent) {
282
+ return delegate != null && delegate.onSearchRequested(searchEvent);
283
+ }
284
+
285
+ @Override
286
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) {
287
+ return delegate == null ? null : delegate.onWindowStartingActionMode(callback);
288
+ }
289
+
290
+ @Override
291
+ public ActionMode onWindowStartingActionMode(ActionMode.Callback callback, int type) {
292
+ return delegate == null ? null : delegate.onWindowStartingActionMode(callback, type);
293
+ }
294
+
295
+ @Override
296
+ public void onActionModeStarted(ActionMode mode) {
297
+ if (delegate != null) {
298
+ delegate.onActionModeStarted(mode);
299
+ }
300
+ }
301
+
302
+ @Override
303
+ public void onActionModeFinished(ActionMode mode) {
304
+ if (delegate != null) {
305
+ delegate.onActionModeFinished(mode);
306
+ }
307
+ }
308
+
309
+ @Override
310
+ public void onProvideKeyboardShortcuts(List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {
311
+ if (delegate != null) {
312
+ delegate.onProvideKeyboardShortcuts(data, menu, deviceId);
313
+ }
314
+ }
315
+
316
+ @Override
317
+ public void onPointerCaptureChanged(boolean hasCapture) {
318
+ if (delegate != null) {
319
+ delegate.onPointerCaptureChanged(hasCapture);
320
+ }
321
+ }
322
+ }
169
323
  }
package/dist/docs.json CHANGED
@@ -846,7 +846,7 @@
846
846
  "text": "4.7.0"
847
847
  }
848
848
  ],
849
- "docs": "Assign this device to a specific update channel at runtime.\n\nChannels allow you to distribute different bundle versions to different groups of users\n(e.g., \"production\", \"beta\", \"staging\"). This method switches the device to a new channel.\n\n**Requirements:**\n- The target channel must allow self-assignment (configured in your Capgo dashboard or backend)\n- The backend may accept or reject the request based on channel settings\n\n**When to use:**\n- After the app is ready and the user has interacted (e.g., opted into beta program)\n- To implement in-app channel switching (beta toggle, tester access, etc.)\n- For user-driven channel changes\n\n**When NOT to use:**\n- At app boot/initialization - use {@link PluginsConfig.CapacitorUpdater.defaultChannel} config instead\n- Before user interaction\n\n**Important: Listen for the `channelPrivate` event**\n\nWhen a user attempts to set a channel that doesn't allow device self-assignment, the method will\nthrow an error AND fire a {@link addListener}('channelPrivate') event. You should listen to this event\nto provide appropriate feedback to users:\n\n```typescript\nCapacitorUpdater.addListener('channelPrivate', (data) => {\n console.warn(`Cannot access channel \"${data.channel}\": ${data.message}`);\n // Show user-friendly message\n});\n```\n\nThis sends a request to the Capgo backend linking your device ID to the specified channel.",
849
+ "docs": "Assign this device to a specific update channel at runtime.\n\nChannels allow you to distribute different bundle versions to different groups of users\n(e.g., \"production\", \"beta\", \"staging\"). This method switches the device to a new channel.\n\n**Device Override UI:** `setChannel()` validates the channel with the backend, then stores the\nselected channel locally on the device. It does not create or update a backend Device Override,\nso the device will not appear as overridden in the Capgo dashboard. Only assignments created\nfrom the dashboard or the Public API are shown in the Device Override UI.\n\n**Requirements:**\n- The target channel must allow self-assignment (configured in your Capgo dashboard or backend)\n- The backend may accept or reject the request based on channel settings\n\n**When to use:**\n- After the app is ready and the user has interacted (e.g., opted into beta program)\n- To implement in-app channel switching (beta toggle, tester access, etc.)\n- For user-driven channel changes\n\n**When NOT to use:**\n- At app boot/initialization - use {@link PluginsConfig.CapacitorUpdater.defaultChannel} config instead\n- Before user interaction\n\n**Important: Listen for the `channelPrivate` event**\n\nWhen a user attempts to set a channel that doesn't allow device self-assignment, the method will\nthrow an error AND fire a {@link addListener}('channelPrivate') event. You should listen to this event\nto provide appropriate feedback to users:\n\n```typescript\nCapacitorUpdater.addListener('channelPrivate', (data) => {\n console.warn(`Cannot access channel \"${data.channel}\": ${data.message}`);\n // Show user-friendly message\n});\n```\n\nThis sends a request to the Capgo backend to validate the specified channel, then stores the\nchannel locally on the device.",
850
850
  "complexTypes": [
851
851
  "ChannelRes",
852
852
  "SetChannelOptions"
@@ -886,7 +886,7 @@
886
886
  "text": "4.7.0"
887
887
  }
888
888
  ],
889
- "docs": "Remove the device's channel assignment and return to the default channel.\n\nThis unlinks the device from any specifically assigned channel, causing it to fall back to:\n- The {@link PluginsConfig.CapacitorUpdater.defaultChannel} if configured, or\n- Your backend's default channel for this app\n\nUse this when:\n- Users opt out of beta/testing programs\n- You want to reset a device to standard update distribution\n- Testing channel switching behavior",
889
+ "docs": "Remove the plugin-managed local channel assignment and return to the default channel.\n\nThis clears only the channel stored locally by {@link setChannel}; it does not delete Dashboard or Public API Device Override records. After the local assignment is cleared, normal channel precedence applies:\n- An existing Dashboard or Public API Device Override, if one exists\n- The {@link PluginsConfig.CapacitorUpdater.defaultChannel} if configured, or\n- Your backend default channel for this app\n\nUse this when:\n- Users opt out of beta/testing programs\n- You want to reset a device to standard update distribution\n- Testing channel switching behavior",
890
890
  "complexTypes": [
891
891
  "UnsetChannelOptions"
892
892
  ],
@@ -1013,7 +1013,7 @@
1013
1013
  "text": "{Error} If the operation fails."
1014
1014
  }
1015
1015
  ],
1016
- "docs": "Get the unique, privacy-friendly identifier for this device.\n\nThis ID is used to identify the device when communicating with update servers.\nIt's automatically generated and stored securely by the plugin.\n\n**Privacy & Security characteristics:**\n- Generated as a UUID (not based on hardware identifiers)\n- Stored securely in platform-specific secure storage\n- Android: Android Keystore (persists across app reinstalls on API 23+)\n- iOS: Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`\n- Not synced to cloud (iOS)\n- Follows Apple and Google privacy best practices\n- Users can clear it via system settings (Android) or keychain access (iOS)\n\n**Persistence:**\nThe device ID persists across app reinstalls to maintain consistent device identity\nfor update tracking and analytics.\n\nUse this to:\n- Debug update delivery issues (check what ID the server sees)\n- Implement device-specific features\n- Correlate server logs with specific devices",
1016
+ "docs": "Get the unique, privacy-friendly identifier for this device.\n\nThis ID is used to identify the device when communicating with update servers.\nIt's automatically generated and stored securely by the plugin.\n\n**Privacy & Security characteristics:**\n- Generated as a UUID (not based on hardware identifiers)\n- Stored securely in platform-specific secure storage\n- Android: mirrored into backup-restorable app preferences for reinstall restore\n- iOS: Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`\n- Not synced to cloud (iOS)\n- Follows Apple and Google privacy best practices\n- Users can clear it via system settings (Android) or keychain access (iOS)\n\n**Persistence:**\nThe device ID persists across app reinstalls to maintain consistent device identity\nfor update tracking and analytics when platform storage is preserved. On Android,\napps with custom backup rules must keep the plugin app preferences eligible for\nbackup/restore; disabling Android backup or clearing app data creates a new ID.\n\nUse this to:\n- Debug update delivery issues (check what ID the server sees)\n- Implement device-specific features\n- Correlate server logs with specific devices",
1017
1017
  "complexTypes": [
1018
1018
  "DeviceId"
1019
1019
  ],
@@ -938,6 +938,11 @@ export interface CapacitorUpdaterPlugin {
938
938
  * Channels allow you to distribute different bundle versions to different groups of users
939
939
  * (e.g., "production", "beta", "staging"). This method switches the device to a new channel.
940
940
  *
941
+ * **Device Override UI:** `setChannel()` validates the channel with the backend, then stores the
942
+ * selected channel locally on the device. It does not create or update a backend Device Override,
943
+ * so the device will not appear as overridden in the Capgo dashboard. Only assignments created
944
+ * from the dashboard or the Public API are shown in the Device Override UI.
945
+ *
941
946
  * **Requirements:**
942
947
  * - The target channel must allow self-assignment (configured in your Capgo dashboard or backend)
943
948
  * - The backend may accept or reject the request based on channel settings
@@ -964,7 +969,8 @@ export interface CapacitorUpdaterPlugin {
964
969
  * });
965
970
  * ```
966
971
  *
967
- * This sends a request to the Capgo backend linking your device ID to the specified channel.
972
+ * This sends a request to the Capgo backend to validate the specified channel, then stores the
973
+ * channel locally on the device.
968
974
  *
969
975
  * @param options The {@link SetChannelOptions} containing the channel name and optional auto-update trigger.
970
976
  * @returns {Promise<ChannelRes>} Channel operation result with status and optional error/message.
@@ -973,11 +979,12 @@ export interface CapacitorUpdaterPlugin {
973
979
  */
974
980
  setChannel(options: SetChannelOptions): Promise<ChannelRes>;
975
981
  /**
976
- * Remove the device's channel assignment and return to the default channel.
982
+ * Remove the plugin-managed local channel assignment and return to the default channel.
977
983
  *
978
- * This unlinks the device from any specifically assigned channel, causing it to fall back to:
984
+ * This clears only the channel stored locally by {@link setChannel}; it does not delete Dashboard or Public API Device Override records. After the local assignment is cleared, normal channel precedence applies:
985
+ * - An existing Dashboard or Public API Device Override, if one exists
979
986
  * - The {@link PluginsConfig.CapacitorUpdater.defaultChannel} if configured, or
980
- * - Your backend's default channel for this app
987
+ * - Your backend default channel for this app
981
988
  *
982
989
  * Use this when:
983
990
  * - Users opt out of beta/testing programs
@@ -1088,7 +1095,7 @@ export interface CapacitorUpdaterPlugin {
1088
1095
  * **Privacy & Security characteristics:**
1089
1096
  * - Generated as a UUID (not based on hardware identifiers)
1090
1097
  * - Stored securely in platform-specific secure storage
1091
- * - Android: Android Keystore (persists across app reinstalls on API 23+)
1098
+ * - Android: mirrored into backup-restorable app preferences for reinstall restore
1092
1099
  * - iOS: Keychain with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`
1093
1100
  * - Not synced to cloud (iOS)
1094
1101
  * - Follows Apple and Google privacy best practices
@@ -1096,7 +1103,9 @@ export interface CapacitorUpdaterPlugin {
1096
1103
  *
1097
1104
  * **Persistence:**
1098
1105
  * The device ID persists across app reinstalls to maintain consistent device identity
1099
- * for update tracking and analytics.
1106
+ * for update tracking and analytics when platform storage is preserved. On Android,
1107
+ * apps with custom backup rules must keep the plugin app preferences eligible for
1108
+ * backup/restore; disabling Android backup or clearing app data creates a new ID.
1100
1109
  *
1101
1110
  * Use this to:
1102
1111
  * - Debug update delivery issues (check what ID the server sees)