@capgo/capacitor-updater 7.38.0 → 7.40.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.
@@ -9,10 +9,12 @@ package ee.forgr.capacitor_updater;
9
9
  import android.app.Activity;
10
10
  import android.app.ActivityManager;
11
11
  import android.content.Context;
12
+ import android.content.Intent;
12
13
  import android.content.SharedPreferences;
13
14
  import android.content.pm.PackageInfo;
14
15
  import android.content.pm.PackageManager;
15
16
  import android.graphics.Color;
17
+ import android.net.Uri;
16
18
  import android.os.Build;
17
19
  import android.os.Handler;
18
20
  import android.os.Looper;
@@ -30,6 +32,17 @@ import com.getcapacitor.PluginHandle;
30
32
  import com.getcapacitor.PluginMethod;
31
33
  import com.getcapacitor.annotation.CapacitorPlugin;
32
34
  import com.getcapacitor.plugin.WebView;
35
+ import com.google.android.gms.tasks.Task;
36
+ // Play Store In-App Updates
37
+ import com.google.android.play.core.appupdate.AppUpdateInfo;
38
+ import com.google.android.play.core.appupdate.AppUpdateManager;
39
+ import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
40
+ import com.google.android.play.core.appupdate.AppUpdateOptions;
41
+ import com.google.android.play.core.install.InstallState;
42
+ import com.google.android.play.core.install.InstallStateUpdatedListener;
43
+ import com.google.android.play.core.install.model.AppUpdateType;
44
+ import com.google.android.play.core.install.model.InstallStatus;
45
+ import com.google.android.play.core.install.model.UpdateAvailability;
33
46
  import io.github.g00fy2.versioncompare.Version;
34
47
  import java.io.IOException;
35
48
  import java.net.MalformedURLException;
@@ -72,7 +85,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
72
85
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
73
86
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
74
87
 
75
- private final String pluginVersion = "7.38.0";
88
+ private final String pluginVersion = "7.40.0";
76
89
  private static final String DELAY_CONDITION_PREFERENCES = "";
77
90
 
78
91
  private SharedPreferences.Editor editor;
@@ -125,6 +138,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
125
138
  private FrameLayout splashscreenLoaderOverlay;
126
139
  private Runnable splashscreenTimeoutRunnable;
127
140
 
141
+ // Play Store In-App Updates
142
+ private AppUpdateManager appUpdateManager;
143
+ private AppUpdateInfo cachedAppUpdateInfo;
144
+ private static final int APP_UPDATE_REQUEST_CODE = 9001;
145
+ private InstallStateUpdatedListener installStateUpdatedListener;
146
+
128
147
  private void notifyBreakingEvents(final String version) {
129
148
  if (version == null || version.isEmpty()) {
130
149
  return;
@@ -2111,9 +2130,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
2111
2130
  }
2112
2131
 
2113
2132
  private boolean isMainActivity() {
2114
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
2115
- return false;
2116
- }
2117
2133
  try {
2118
2134
  Context mContext = this.getContext();
2119
2135
  ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
@@ -2191,27 +2207,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
2191
2207
  }
2192
2208
  }
2193
2209
 
2194
- @Override
2195
- public void handleOnDestroy() {
2196
- try {
2197
- logger.info("onActivityDestroyed " + getActivity().getClass().getName());
2198
- this.implementation.activity = getActivity();
2199
-
2200
- // Clean up shake menu
2201
- if (shakeMenu != null) {
2202
- try {
2203
- shakeMenu.stop();
2204
- shakeMenu = null;
2205
- logger.info("Shake menu cleaned up");
2206
- } catch (Exception e) {
2207
- logger.error("Failed to clean up shake menu: " + e.getMessage());
2208
- }
2209
- }
2210
- } catch (Exception e) {
2211
- logger.error("Failed to run handleOnDestroy: " + e.getMessage());
2212
- }
2213
- }
2214
-
2215
2210
  @PluginMethod
2216
2211
  public void setShakeMenu(final PluginCall call) {
2217
2212
  final Boolean enabled = call.getBoolean("enabled");
@@ -2285,4 +2280,332 @@ public class CapacitorUpdaterPlugin extends Plugin {
2285
2280
  this.implementation.appId = appId;
2286
2281
  call.resolve();
2287
2282
  }
2283
+
2284
+ // ============================================================================
2285
+ // Play Store In-App Update Methods
2286
+ // ============================================================================
2287
+
2288
+ // AppUpdateAvailability enum values matching TypeScript definitions
2289
+ private static final int UPDATE_AVAILABILITY_UNKNOWN = 0;
2290
+ private static final int UPDATE_AVAILABILITY_NOT_AVAILABLE = 1;
2291
+ private static final int UPDATE_AVAILABILITY_AVAILABLE = 2;
2292
+ private static final int UPDATE_AVAILABILITY_IN_PROGRESS = 3;
2293
+
2294
+ // AppUpdateResultCode enum values matching TypeScript definitions
2295
+ private static final int RESULT_OK = 0;
2296
+ private static final int RESULT_CANCELED = 1;
2297
+ private static final int RESULT_FAILED = 2;
2298
+ private static final int RESULT_NOT_AVAILABLE = 3;
2299
+ private static final int RESULT_NOT_ALLOWED = 4;
2300
+ private static final int RESULT_INFO_MISSING = 5;
2301
+
2302
+ private AppUpdateManager getAppUpdateManager() {
2303
+ if (appUpdateManager == null) {
2304
+ appUpdateManager = AppUpdateManagerFactory.create(getContext());
2305
+ }
2306
+ return appUpdateManager;
2307
+ }
2308
+
2309
+ private int mapUpdateAvailability(int playStoreAvailability) {
2310
+ switch (playStoreAvailability) {
2311
+ case UpdateAvailability.UPDATE_AVAILABLE:
2312
+ return UPDATE_AVAILABILITY_AVAILABLE;
2313
+ case UpdateAvailability.UPDATE_NOT_AVAILABLE:
2314
+ return UPDATE_AVAILABILITY_NOT_AVAILABLE;
2315
+ case UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS:
2316
+ return UPDATE_AVAILABILITY_IN_PROGRESS;
2317
+ default:
2318
+ return UPDATE_AVAILABILITY_UNKNOWN;
2319
+ }
2320
+ }
2321
+
2322
+ @PluginMethod
2323
+ public void getAppUpdateInfo(final PluginCall call) {
2324
+ logger.info("Getting Play Store update info");
2325
+
2326
+ try {
2327
+ AppUpdateManager manager = getAppUpdateManager();
2328
+ Task<AppUpdateInfo> appUpdateInfoTask = manager.getAppUpdateInfo();
2329
+
2330
+ appUpdateInfoTask
2331
+ .addOnSuccessListener((appUpdateInfo) -> {
2332
+ cachedAppUpdateInfo = appUpdateInfo;
2333
+
2334
+ JSObject result = new JSObject();
2335
+ try {
2336
+ PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0);
2337
+ result.put("currentVersionName", pInfo.versionName);
2338
+ result.put("currentVersionCode", String.valueOf(pInfo.versionCode));
2339
+ } catch (PackageManager.NameNotFoundException e) {
2340
+ result.put("currentVersionName", "0.0.0");
2341
+ result.put("currentVersionCode", "0");
2342
+ }
2343
+
2344
+ result.put("updateAvailability", mapUpdateAvailability(appUpdateInfo.updateAvailability()));
2345
+
2346
+ if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
2347
+ result.put("availableVersionCode", String.valueOf(appUpdateInfo.availableVersionCode()));
2348
+ // Play Store doesn't provide version name, only version code
2349
+ result.put("availableVersionName", String.valueOf(appUpdateInfo.availableVersionCode()));
2350
+ result.put("updatePriority", appUpdateInfo.updatePriority());
2351
+ result.put("immediateUpdateAllowed", appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE));
2352
+ result.put("flexibleUpdateAllowed", appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE));
2353
+
2354
+ Integer stalenessDays = appUpdateInfo.clientVersionStalenessDays();
2355
+ if (stalenessDays != null) {
2356
+ result.put("clientVersionStalenessDays", stalenessDays);
2357
+ }
2358
+ } else {
2359
+ result.put("immediateUpdateAllowed", false);
2360
+ result.put("flexibleUpdateAllowed", false);
2361
+ }
2362
+
2363
+ result.put("installStatus", appUpdateInfo.installStatus());
2364
+
2365
+ call.resolve(result);
2366
+ })
2367
+ .addOnFailureListener((e) -> {
2368
+ logger.error("Failed to get app update info: " + e.getMessage());
2369
+ call.reject("Failed to get app update info: " + e.getMessage());
2370
+ });
2371
+ } catch (Exception e) {
2372
+ logger.error("Error getting app update info: " + e.getMessage());
2373
+ call.reject("Error getting app update info: " + e.getMessage());
2374
+ }
2375
+ }
2376
+
2377
+ @PluginMethod
2378
+ public void openAppStore(final PluginCall call) {
2379
+ String packageName = call.getString("packageName");
2380
+ if (packageName == null || packageName.isEmpty()) {
2381
+ packageName = getContext().getPackageName();
2382
+ }
2383
+
2384
+ try {
2385
+ // Try to open Play Store app first
2386
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName));
2387
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2388
+ getContext().startActivity(intent);
2389
+ call.resolve();
2390
+ } catch (android.content.ActivityNotFoundException e) {
2391
+ // Fall back to browser
2392
+ try {
2393
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + packageName));
2394
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2395
+ getContext().startActivity(intent);
2396
+ call.resolve();
2397
+ } catch (Exception ex) {
2398
+ logger.error("Failed to open Play Store: " + ex.getMessage());
2399
+ call.reject("Failed to open Play Store: " + ex.getMessage());
2400
+ }
2401
+ }
2402
+ }
2403
+
2404
+ @PluginMethod
2405
+ public void performImmediateUpdate(final PluginCall call) {
2406
+ if (cachedAppUpdateInfo == null) {
2407
+ logger.error("No update info available. Call getAppUpdateInfo first.");
2408
+ JSObject result = new JSObject();
2409
+ result.put("code", RESULT_INFO_MISSING);
2410
+ call.resolve(result);
2411
+ return;
2412
+ }
2413
+
2414
+ if (cachedAppUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
2415
+ logger.info("No update available");
2416
+ JSObject result = new JSObject();
2417
+ result.put("code", RESULT_NOT_AVAILABLE);
2418
+ call.resolve(result);
2419
+ return;
2420
+ }
2421
+
2422
+ if (!cachedAppUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
2423
+ logger.info("Immediate update not allowed");
2424
+ JSObject result = new JSObject();
2425
+ result.put("code", RESULT_NOT_ALLOWED);
2426
+ call.resolve(result);
2427
+ return;
2428
+ }
2429
+
2430
+ try {
2431
+ Activity activity = getActivity();
2432
+ if (activity == null) {
2433
+ call.reject("Activity not available");
2434
+ return;
2435
+ }
2436
+
2437
+ // Save the call for later resolution
2438
+ bridge.saveCall(call);
2439
+
2440
+ AppUpdateManager manager = getAppUpdateManager();
2441
+ manager.startUpdateFlowForResult(
2442
+ cachedAppUpdateInfo,
2443
+ activity,
2444
+ AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
2445
+ APP_UPDATE_REQUEST_CODE
2446
+ );
2447
+ } catch (Exception e) {
2448
+ logger.error("Failed to start immediate update: " + e.getMessage());
2449
+ JSObject result = new JSObject();
2450
+ result.put("code", RESULT_FAILED);
2451
+ call.resolve(result);
2452
+ }
2453
+ }
2454
+
2455
+ @PluginMethod
2456
+ public void startFlexibleUpdate(final PluginCall call) {
2457
+ if (cachedAppUpdateInfo == null) {
2458
+ logger.error("No update info available. Call getAppUpdateInfo first.");
2459
+ JSObject result = new JSObject();
2460
+ result.put("code", RESULT_INFO_MISSING);
2461
+ call.resolve(result);
2462
+ return;
2463
+ }
2464
+
2465
+ if (cachedAppUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
2466
+ logger.info("No update available");
2467
+ JSObject result = new JSObject();
2468
+ result.put("code", RESULT_NOT_AVAILABLE);
2469
+ call.resolve(result);
2470
+ return;
2471
+ }
2472
+
2473
+ if (!cachedAppUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
2474
+ logger.info("Flexible update not allowed");
2475
+ JSObject result = new JSObject();
2476
+ result.put("code", RESULT_NOT_ALLOWED);
2477
+ call.resolve(result);
2478
+ return;
2479
+ }
2480
+
2481
+ try {
2482
+ Activity activity = getActivity();
2483
+ if (activity == null) {
2484
+ call.reject("Activity not available");
2485
+ return;
2486
+ }
2487
+
2488
+ // Register listener for flexible update state changes
2489
+ AppUpdateManager manager = getAppUpdateManager();
2490
+
2491
+ // Remove any existing listener
2492
+ if (installStateUpdatedListener != null) {
2493
+ manager.unregisterListener(installStateUpdatedListener);
2494
+ }
2495
+
2496
+ installStateUpdatedListener = (state) -> {
2497
+ JSObject eventData = new JSObject();
2498
+ eventData.put("installStatus", state.installStatus());
2499
+
2500
+ if (state.installStatus() == InstallStatus.DOWNLOADING) {
2501
+ eventData.put("bytesDownloaded", state.bytesDownloaded());
2502
+ eventData.put("totalBytesToDownload", state.totalBytesToDownload());
2503
+ }
2504
+
2505
+ notifyListeners("onFlexibleUpdateStateChange", eventData);
2506
+ };
2507
+
2508
+ manager.registerListener(installStateUpdatedListener);
2509
+
2510
+ // Save the call for later resolution
2511
+ bridge.saveCall(call);
2512
+
2513
+ manager.startUpdateFlowForResult(
2514
+ cachedAppUpdateInfo,
2515
+ activity,
2516
+ AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build(),
2517
+ APP_UPDATE_REQUEST_CODE
2518
+ );
2519
+ } catch (Exception e) {
2520
+ logger.error("Failed to start flexible update: " + e.getMessage());
2521
+ JSObject result = new JSObject();
2522
+ result.put("code", RESULT_FAILED);
2523
+ call.resolve(result);
2524
+ }
2525
+ }
2526
+
2527
+ @PluginMethod
2528
+ public void completeFlexibleUpdate(final PluginCall call) {
2529
+ try {
2530
+ AppUpdateManager manager = getAppUpdateManager();
2531
+ manager
2532
+ .completeUpdate()
2533
+ .addOnSuccessListener((aVoid) -> {
2534
+ // The app will restart, so this may not be called
2535
+ call.resolve();
2536
+ })
2537
+ .addOnFailureListener((e) -> {
2538
+ logger.error("Failed to complete flexible update: " + e.getMessage());
2539
+ call.reject("Failed to complete flexible update: " + e.getMessage());
2540
+ });
2541
+ } catch (Exception e) {
2542
+ logger.error("Error completing flexible update: " + e.getMessage());
2543
+ call.reject("Error completing flexible update: " + e.getMessage());
2544
+ }
2545
+ }
2546
+
2547
+ @Override
2548
+ protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {
2549
+ super.handleOnActivityResult(requestCode, resultCode, data);
2550
+
2551
+ if (requestCode == APP_UPDATE_REQUEST_CODE) {
2552
+ PluginCall savedCall = bridge.getSavedCall("com.getcapacitor.PluginCall");
2553
+ if (savedCall == null) {
2554
+ // Try to get any saved call (for backward compatibility)
2555
+ return;
2556
+ }
2557
+
2558
+ JSObject result = new JSObject();
2559
+ if (resultCode == Activity.RESULT_OK) {
2560
+ result.put("code", RESULT_OK);
2561
+ } else if (resultCode == Activity.RESULT_CANCELED) {
2562
+ result.put("code", RESULT_CANCELED);
2563
+ } else {
2564
+ result.put("code", RESULT_FAILED);
2565
+ }
2566
+ savedCall.resolve(result);
2567
+ bridge.releaseCall(savedCall);
2568
+ }
2569
+ }
2570
+
2571
+ @Override
2572
+ protected void handleOnDestroy() {
2573
+ // Clean up the install state listener
2574
+ if (installStateUpdatedListener != null && appUpdateManager != null) {
2575
+ try {
2576
+ appUpdateManager.unregisterListener(installStateUpdatedListener);
2577
+ installStateUpdatedListener = null;
2578
+ } catch (Exception e) {
2579
+ logger.error("Failed to unregister install state listener: " + e.getMessage());
2580
+ }
2581
+ }
2582
+
2583
+ handleOnDestroyInternal();
2584
+ }
2585
+
2586
+ private void handleOnDestroyInternal() {
2587
+ // Original handleOnDestroy code
2588
+ try {
2589
+ logger.info("onActivityDestroyed " + getActivity().getClass().getName());
2590
+ this.implementation.activity = getActivity();
2591
+
2592
+ // Check for 'kill' delay condition on activity destroy
2593
+ // Note: onDestroy is not reliably called - also check on next app launch
2594
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
2595
+ this.delayUpdateUtils.setBackgroundTimestamp(0);
2596
+
2597
+ // Clean up shake menu
2598
+ if (shakeMenu != null) {
2599
+ try {
2600
+ shakeMenu.stop();
2601
+ shakeMenu = null;
2602
+ logger.info("Shake menu cleaned up");
2603
+ } catch (Exception e) {
2604
+ logger.error("Failed to clean up shake menu: " + e.getMessage());
2605
+ }
2606
+ }
2607
+ } catch (Exception e) {
2608
+ logger.error("Failed to run handleOnDestroy: " + e.getMessage());
2609
+ }
2610
+ }
2288
2611
  }
@@ -8,7 +8,6 @@ package ee.forgr.capacitor_updater;
8
8
 
9
9
  import android.content.Context;
10
10
  import android.content.SharedPreferences;
11
- import android.os.Build;
12
11
  import android.security.keystore.KeyGenParameterSpec;
13
12
  import android.security.keystore.KeyProperties;
14
13
  import java.io.IOException;
@@ -56,11 +55,6 @@ public class DeviceIdHelper {
56
55
  * @return Device ID as a lowercase UUID string
57
56
  */
58
57
  public static String getOrCreateDeviceId(Context context, SharedPreferences legacyPrefs) {
59
- // API 23+ required for Android Keystore
60
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
61
- return getFallbackDeviceId(legacyPrefs);
62
- }
63
-
64
58
  try {
65
59
  // Try to get device ID from Keystore storage
66
60
  String deviceId = getDeviceIdFromKeystore(context);