@capgo/capacitor-updater 8.0.1 → 8.1.0

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.
Files changed (55) hide show
  1. package/CapgoCapacitorUpdater.podspec +7 -5
  2. package/Package.swift +9 -7
  3. package/README.md +984 -215
  4. package/android/build.gradle +24 -12
  5. package/android/proguard-rules.pro +22 -5
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +110 -22
  7. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1310 -488
  9. package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +640 -203
  10. package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipherV2.java → CryptoCipher.java} +119 -33
  11. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +497 -133
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +80 -25
  16. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
  17. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
  19. package/dist/docs.json +873 -154
  20. package/dist/esm/definitions.d.ts +881 -114
  21. package/dist/esm/definitions.js.map +1 -1
  22. package/dist/esm/history.d.ts +1 -0
  23. package/dist/esm/history.js +283 -0
  24. package/dist/esm/history.js.map +1 -0
  25. package/dist/esm/index.d.ts +1 -0
  26. package/dist/esm/index.js +1 -0
  27. package/dist/esm/index.js.map +1 -1
  28. package/dist/esm/web.d.ts +12 -1
  29. package/dist/esm/web.js +29 -2
  30. package/dist/esm/web.js.map +1 -1
  31. package/dist/plugin.cjs.js +311 -2
  32. package/dist/plugin.cjs.js.map +1 -1
  33. package/dist/plugin.js +311 -2
  34. package/dist/plugin.js.map +1 -1
  35. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +69 -0
  36. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
  37. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +37 -10
  38. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +1 -1
  39. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1605 -0
  40. package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +523 -230
  41. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +267 -0
  42. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
  43. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  44. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +53 -0
  45. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  46. package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
  47. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
  48. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
  49. package/package.json +21 -19
  50. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -975
  51. package/ios/Plugin/CryptoCipherV2.swift +0 -310
  52. /package/{LICENCE → LICENSE} +0 -0
  53. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  54. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  55. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
@@ -1,24 +1,26 @@
1
1
  package ee.forgr.capacitor_updater;
2
2
 
3
3
  import android.content.Context;
4
- import android.util.Log;
5
4
  import androidx.work.BackoffPolicy;
6
5
  import androidx.work.Configuration;
7
6
  import androidx.work.Constraints;
8
7
  import androidx.work.Data;
8
+ import androidx.work.ExistingWorkPolicy;
9
9
  import androidx.work.NetworkType;
10
10
  import androidx.work.OneTimeWorkRequest;
11
11
  import androidx.work.WorkManager;
12
12
  import androidx.work.WorkRequest;
13
- import java.util.HashSet;
14
- import java.util.Set;
15
13
  import java.util.concurrent.TimeUnit;
16
14
 
17
15
  public class DownloadWorkerManager {
18
16
 
19
- private static final String TAG = "DownloadWorkerManager";
17
+ private static Logger logger;
18
+
19
+ public static void setLogger(Logger loggerInstance) {
20
+ logger = loggerInstance;
21
+ }
22
+
20
23
  private static volatile boolean isInitialized = false;
21
- private static final Set<String> activeVersions = new HashSet<>();
22
24
 
23
25
  private static synchronized void initializeIfNeeded(Context context) {
24
26
  if (!isInitialized) {
@@ -32,8 +34,18 @@ public class DownloadWorkerManager {
32
34
  }
33
35
  }
34
36
 
35
- public static synchronized boolean isVersionDownloading(String version) {
36
- return activeVersions.contains(version);
37
+ public static boolean isVersionDownloading(Context context, String version) {
38
+ initializeIfNeeded(context.getApplicationContext());
39
+ try {
40
+ return WorkManager.getInstance(context)
41
+ .getWorkInfosByTag(version)
42
+ .get()
43
+ .stream()
44
+ .anyMatch((workInfo) -> !workInfo.getState().isFinished());
45
+ } catch (Exception e) {
46
+ logger.error("Error checking download status: " + e.getMessage());
47
+ return false;
48
+ }
37
49
  }
38
50
 
39
51
  public static void enqueueDownload(
@@ -46,16 +58,23 @@ public class DownloadWorkerManager {
46
58
  String sessionKey,
47
59
  String checksum,
48
60
  String publicKey,
49
- boolean isManifest
61
+ boolean isManifest,
62
+ boolean isEmulator,
63
+ String appId,
64
+ String pluginVersion,
65
+ boolean isProd,
66
+ String statsUrl,
67
+ String deviceId,
68
+ String versionBuild,
69
+ String versionCode,
70
+ String versionOs,
71
+ String customId,
72
+ String defaultChannel
50
73
  ) {
51
74
  initializeIfNeeded(context.getApplicationContext());
52
75
 
53
- // If version is already downloading, don't start another one
54
- if (isVersionDownloading(version)) {
55
- Log.i(TAG, "Version " + version + " is already downloading");
56
- return;
57
- }
58
- activeVersions.add(version);
76
+ // Use unique work name for this bundle to prevent duplicates
77
+ String uniqueWorkName = "bundle_" + id + "_" + version;
59
78
 
60
79
  // Create input data
61
80
  Data inputData = new Data.Builder()
@@ -68,34 +87,70 @@ public class DownloadWorkerManager {
68
87
  .putString(DownloadService.CHECKSUM, checksum)
69
88
  .putBoolean(DownloadService.IS_MANIFEST, isManifest)
70
89
  .putString(DownloadService.PUBLIC_KEY, publicKey)
90
+ .putString(DownloadService.APP_ID, appId)
91
+ .putString(DownloadService.pluginVersion, pluginVersion)
92
+ .putString(DownloadService.STATS_URL, statsUrl)
93
+ .putString(DownloadService.DEVICE_ID, deviceId)
94
+ .putString(DownloadService.VERSION_BUILD, versionBuild)
95
+ .putString(DownloadService.VERSION_CODE, versionCode)
96
+ .putString(DownloadService.VERSION_OS, versionOs)
97
+ .putString(DownloadService.CUSTOM_ID, customId)
98
+ .putString(DownloadService.DEFAULT_CHANNEL, defaultChannel)
99
+ .putBoolean(DownloadService.IS_PROD, isProd)
100
+ .putBoolean(DownloadService.IS_EMULATOR, isEmulator)
71
101
  .build();
72
102
 
73
- // Create network constraints
74
- Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
103
+ // Create network constraints - be more lenient on emulators
104
+ Constraints.Builder constraintsBuilder = new Constraints.Builder();
105
+ if (isEmulator) {
106
+ logger.info("Emulator detected - using lenient network constraints");
107
+ // On emulators, use NOT_REQUIRED to avoid background network issues
108
+ constraintsBuilder.setRequiredNetworkType(NetworkType.NOT_REQUIRED);
109
+ } else {
110
+ constraintsBuilder.setRequiredNetworkType(NetworkType.CONNECTED);
111
+ }
112
+ Constraints constraints = constraintsBuilder.build();
75
113
 
76
114
  // Create work request with tags for tracking
77
- OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DownloadService.class)
115
+ OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(DownloadService.class)
78
116
  .setConstraints(constraints)
79
117
  .setInputData(inputData)
80
118
  .addTag(id)
81
- .addTag(version) // Add version tag for tracking
82
- .addTag("capacitor_updater_download")
83
- .setBackoffCriteria(BackoffPolicy.LINEAR, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
84
- .build();
119
+ .addTag(version)
120
+ .addTag("capacitor_updater_download");
121
+
122
+ // More aggressive retry policy for emulators
123
+ if (isEmulator) {
124
+ workRequestBuilder.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS);
125
+ } else {
126
+ workRequestBuilder.setBackoffCriteria(BackoffPolicy.LINEAR, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS);
127
+ }
128
+
129
+ OneTimeWorkRequest workRequest = workRequestBuilder.build();
85
130
 
86
- // Enqueue work
87
- WorkManager.getInstance(context).enqueue(workRequest);
131
+ // Use beginUniqueWork to prevent duplicate downloads
132
+ WorkManager.getInstance(context)
133
+ .beginUniqueWork(
134
+ uniqueWorkName,
135
+ ExistingWorkPolicy.KEEP, // Don't start if already running
136
+ workRequest
137
+ )
138
+ .enqueue();
88
139
  }
89
140
 
90
141
  public static void cancelVersionDownload(Context context, String version) {
91
142
  initializeIfNeeded(context.getApplicationContext());
92
143
  WorkManager.getInstance(context).cancelAllWorkByTag(version);
93
- activeVersions.remove(version);
144
+ }
145
+
146
+ public static void cancelBundleDownload(Context context, String id, String version) {
147
+ String uniqueWorkName = "bundle_" + id + "_" + version;
148
+ initializeIfNeeded(context.getApplicationContext());
149
+ WorkManager.getInstance(context).cancelUniqueWork(uniqueWorkName);
94
150
  }
95
151
 
96
152
  public static void cancelAllDownloads(Context context) {
97
153
  initializeIfNeeded(context.getApplicationContext());
98
154
  WorkManager.getInstance(context).cancelAllWorkByTag("capacitor_updater_download");
99
- activeVersions.clear();
100
155
  }
101
156
  }
@@ -0,0 +1,338 @@
1
+ package ee.forgr.capacitor_updater;
2
+
3
+ import android.util.ArrayMap;
4
+ import android.util.Log;
5
+ import androidx.annotation.NonNull;
6
+ import androidx.annotation.Nullable;
7
+ import com.getcapacitor.*;
8
+ import java.util.ArrayList;
9
+ import java.util.Arrays;
10
+ import java.util.Date;
11
+ import java.util.Locale;
12
+ import java.util.Map;
13
+ import org.jetbrains.annotations.Contract;
14
+
15
+ public class Logger {
16
+
17
+ private Bridge bridge;
18
+
19
+ public enum LogLevel {
20
+ silent,
21
+ error,
22
+ warn,
23
+ info,
24
+ debug
25
+ }
26
+
27
+ public static class Options {
28
+
29
+ LogLevel level;
30
+ Map<String, String> labels;
31
+
32
+ Options() {
33
+ level = LogLevel.info;
34
+ labels = null;
35
+ }
36
+
37
+ Options(@NonNull Options other) {
38
+ level = other.level;
39
+ labels = other.labels;
40
+ }
41
+
42
+ Options(LogLevel level) {
43
+ this.level = level;
44
+ this.labels = null;
45
+ }
46
+
47
+ Options(Map<String, String> labels) {
48
+ this.level = LogLevel.info;
49
+ this.labels = labels;
50
+ }
51
+
52
+ Options(LogLevel level, Map<String, String> labels) {
53
+ this.level = level;
54
+ this.labels = labels;
55
+ }
56
+ }
57
+
58
+ public LogLevel level;
59
+ private final Map<LogLevel, String> labels = new ArrayMap<>();
60
+ private String tag;
61
+ private final ArrayMap<String, Long> timers = new ArrayMap<>();
62
+ private final String kDefaultTimerLabel = "default";
63
+
64
+ public void setBridge(Bridge bridge) {
65
+ this.bridge = bridge;
66
+ }
67
+
68
+ public Logger(String tag) {
69
+ super();
70
+ init(tag, new Options());
71
+ }
72
+
73
+ public Logger(String tag, Options options) {
74
+ super();
75
+ init(tag, options);
76
+ }
77
+
78
+ private void init(String tag, @NonNull Options options) {
79
+ this.level = options.level;
80
+ this.labels.putAll(
81
+ Map.of(LogLevel.silent, "", LogLevel.error, "🔴", LogLevel.warn, "🟠", LogLevel.info, "🟢", LogLevel.debug, "\uD83D\uDD0E")
82
+ );
83
+
84
+ if (options.labels != null) {
85
+ setLabels(options.labels);
86
+ }
87
+
88
+ this.tag = tag;
89
+ }
90
+
91
+ @Nullable
92
+ public Object getLevelWithName(String name) {
93
+ try {
94
+ return LogLevel.valueOf(name);
95
+ } catch (IllegalArgumentException ex) {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ public String getLevelName() {
101
+ return level.name();
102
+ }
103
+
104
+ public void setLevelName(String name) {
105
+ try {
106
+ level = LogLevel.valueOf(name);
107
+ } catch (IllegalArgumentException e) {
108
+ // Ignore it
109
+ }
110
+ }
111
+
112
+ public Map<String, String> getLabels() {
113
+ ArrayMap<String, String> result = new ArrayMap<>();
114
+
115
+ for (Map.Entry<LogLevel, String> entry : labels.entrySet()) {
116
+ result.put(entry.getKey().name(), entry.getValue());
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ public void setLabels(@NonNull Map<String, String> labels) {
123
+ for (Map.Entry<String, String> entry : labels.entrySet()) {
124
+ Object level = getLevelWithName(entry.getKey());
125
+
126
+ if (level != null) {
127
+ this.labels.put((LogLevel) level, entry.getValue());
128
+ }
129
+ }
130
+ }
131
+
132
+ public String getTag() {
133
+ return tag;
134
+ }
135
+
136
+ public void setTag(@NonNull String tag) {
137
+ if (!tag.isEmpty()) {
138
+ this.tag = tag;
139
+ }
140
+ }
141
+
142
+ public void error(String message) {
143
+ logWithTagAtLevel(LogLevel.error, "", tag, message);
144
+ }
145
+
146
+ public void warn(String message) {
147
+ logWithTagAtLevel(LogLevel.warn, "", tag, message);
148
+ }
149
+
150
+ public void info(String message) {
151
+ logWithTagAtLevel(LogLevel.info, "", tag, message);
152
+ }
153
+
154
+ public void log(String message) {
155
+ info(message);
156
+ }
157
+
158
+ public void debug(String message) {
159
+ logWithTagAtLevel(LogLevel.debug, "", tag, message);
160
+ }
161
+
162
+ public void logAtLevel(LogLevel level, String message) {
163
+ logWithTagAtLevel(level, "", tag, message);
164
+ }
165
+
166
+ public void logAtLevel(String level, String message) {
167
+ logWithTagAtLevel(resolveLevelName(level), "", tag, message);
168
+ }
169
+
170
+ public void logWithTagAtLevel(LogLevel level, String tag, String message) {
171
+ logWithTagAtLevel(level, "", tag, message);
172
+ }
173
+
174
+ public void logWithTagAtLevel(String level, String tag, String message) {
175
+ logWithTagAtLevel(resolveLevelName(level), "", tag, message);
176
+ }
177
+
178
+ private LogLevel resolveLevelName(String name) {
179
+ Object level = getLevelWithName(name);
180
+
181
+ if (level != null) {
182
+ return (LogLevel) level;
183
+ }
184
+
185
+ return LogLevel.info;
186
+ }
187
+
188
+ public void logWithTagAtLevel(LogLevel level, String label, String tag, String message) {
189
+ if (this.level.compareTo(level) < 0) {
190
+ return;
191
+ }
192
+
193
+ if (label.isEmpty()) {
194
+ label = labels.get(level);
195
+ }
196
+
197
+ // If the label is ASCII, surround it with []
198
+ String format;
199
+
200
+ if (label != null) {
201
+ format = label.charAt(0) <= 127 ? "[%s]: %s" : "%s %s";
202
+ } else {
203
+ label = "";
204
+ format = "%s%s";
205
+ }
206
+
207
+ String formattedMessage = String.format(format, label, message);
208
+
209
+ if (tag.isEmpty()) {
210
+ tag = this.tag;
211
+ }
212
+
213
+ // Always log to Android system log
214
+ switch (level) {
215
+ case error:
216
+ Log.e(tag, formattedMessage);
217
+ break;
218
+ case warn:
219
+ Log.w(tag, formattedMessage);
220
+ break;
221
+ case info:
222
+ Log.i(tag, formattedMessage);
223
+ break;
224
+ case debug:
225
+ Log.d(tag, formattedMessage);
226
+ break;
227
+ }
228
+
229
+ // Send to JavaScript if webView is available
230
+ if (bridge != null && bridge.getWebView() != null) {
231
+ bridge.eval(
232
+ "console." + level.name() + "(\"[" + tag.replace("\"", "\\\"") + "] " + formattedMessage.replace("\"", "\\\"") + "\")",
233
+ null
234
+ );
235
+ }
236
+ }
237
+
238
+ public void dir(Object value) {
239
+ String message;
240
+
241
+ if (value != null) {
242
+ if (value.getClass().isArray()) {
243
+ Object[] arr = (Object[]) value;
244
+ message = Arrays.deepToString(arr);
245
+ } else {
246
+ try {
247
+ message = value.toString();
248
+ } catch (Exception ex) {
249
+ message = String.format("<%s>", ex.getMessage());
250
+ }
251
+ }
252
+ } else {
253
+ message = "null";
254
+ }
255
+
256
+ info(message);
257
+ }
258
+
259
+ public void time() {
260
+ time(kDefaultTimerLabel);
261
+ }
262
+
263
+ public void time(String label) {
264
+ timers.put(label, new Date().getTime());
265
+ }
266
+
267
+ public void timeLog() {
268
+ timeLog(kDefaultTimerLabel);
269
+ }
270
+
271
+ public void timeLog(String label) {
272
+ label = resolveTimerLabel(label);
273
+
274
+ if (timers.containsKey(label)) {
275
+ Long start = timers.get(label);
276
+
277
+ // This will always be true
278
+ if (start != null) {
279
+ long now = new Date().getTime();
280
+ long diff = now - start;
281
+ info(String.format(Locale.getDefault(), "%s: %s", label, formatMilliseconds(diff)));
282
+ }
283
+ } else {
284
+ warn(String.format("timer '%s' does not exist", label));
285
+ }
286
+ }
287
+
288
+ public void timeEnd() {
289
+ timeEnd(kDefaultTimerLabel);
290
+ }
291
+
292
+ public void timeEnd(String label) {
293
+ label = resolveTimerLabel(label);
294
+ timeLog(label);
295
+ timers.remove(label);
296
+ }
297
+
298
+ @Contract(pure = true)
299
+ private String resolveTimerLabel(@NonNull String label) {
300
+ return label.isEmpty() ? kDefaultTimerLabel : label;
301
+ }
302
+
303
+ @NonNull
304
+ private String formatMilliseconds(long milliseconds) {
305
+ long seconds = Math.floorDiv(milliseconds, 1000);
306
+ long minutes = Math.floorDiv(seconds, 60);
307
+ long hours = Math.floorDiv(minutes, 60);
308
+ long millis = milliseconds % 1000;
309
+
310
+ if (seconds < 1) {
311
+ return String.format(Locale.getDefault(), "%dms", milliseconds);
312
+ }
313
+
314
+ if (minutes < 1) {
315
+ return String.format(Locale.getDefault(), "%d.%ds", seconds, millis);
316
+ }
317
+
318
+ seconds = seconds % 60;
319
+
320
+ if (hours < 1) {
321
+ return String.format(Locale.getDefault(), "%d:%02d.%03d (min:sec.ms)", minutes, seconds, millis);
322
+ }
323
+
324
+ minutes = minutes % 60;
325
+ return String.format(Locale.getDefault(), "%d:%02d:%02d (hr:min:sec)", hours, minutes, seconds);
326
+ }
327
+
328
+ public void trace() {
329
+ StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
330
+ ArrayList<String> stack = new ArrayList<>();
331
+
332
+ for (StackTraceElement element : stackTrace) {
333
+ stack.add(element.toString());
334
+ }
335
+
336
+ info("trace\n" + String.join("\n", stack));
337
+ }
338
+ }
@@ -0,0 +1,72 @@
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.hardware.Sensor;
10
+ import android.hardware.SensorEvent;
11
+ import android.hardware.SensorEventListener;
12
+ import android.hardware.SensorManager;
13
+
14
+ public class ShakeDetector implements SensorEventListener {
15
+
16
+ public interface Listener {
17
+ void onShakeDetected();
18
+ }
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)
22
+
23
+ private Listener listener;
24
+ private SensorManager sensorManager;
25
+ private Sensor accelerometer;
26
+ private long lastShakeTime = 0;
27
+
28
+ public ShakeDetector(Listener listener) {
29
+ this.listener = listener;
30
+ }
31
+
32
+ public void start(SensorManager sensorManager) {
33
+ this.sensorManager = sensorManager;
34
+ this.accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
35
+
36
+ if (accelerometer != null) {
37
+ sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
38
+ }
39
+ }
40
+
41
+ public void stop() {
42
+ if (sensorManager != null) {
43
+ sensorManager.unregisterListener(this);
44
+ }
45
+ }
46
+
47
+ @Override
48
+ public void onSensorChanged(SensorEvent event) {
49
+ if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
50
+ float x = event.values[0];
51
+ float y = event.values[1];
52
+ float z = event.values[2];
53
+
54
+ // Calculate the acceleration magnitude (excluding gravity)
55
+ float acceleration = (float) Math.sqrt(x * x + y * y + z * z) - SensorManager.GRAVITY_EARTH;
56
+
57
+ // Check if acceleration exceeds threshold and enough time has passed
58
+ long currentTime = System.currentTimeMillis();
59
+ if (Math.abs(acceleration) > SHAKE_THRESHOLD && currentTime - lastShakeTime > SHAKE_TIMEOUT) {
60
+ lastShakeTime = currentTime;
61
+ if (listener != null) {
62
+ listener.onShakeDetected();
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ @Override
69
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
70
+ // Not needed for shake detection
71
+ }
72
+ }