@capgo/capacitor-updater 7.37.0 → 7.39.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.
@@ -55,6 +55,20 @@ import org.json.JSONArray;
55
55
  import org.json.JSONException;
56
56
  import org.json.JSONObject;
57
57
 
58
+ // Play Store In-App Updates
59
+ import com.google.android.play.core.appupdate.AppUpdateInfo;
60
+ import com.google.android.play.core.appupdate.AppUpdateManager;
61
+ import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
62
+ import com.google.android.play.core.appupdate.AppUpdateOptions;
63
+ import com.google.android.play.core.install.InstallState;
64
+ import com.google.android.play.core.install.InstallStateUpdatedListener;
65
+ import com.google.android.play.core.install.model.AppUpdateType;
66
+ import com.google.android.play.core.install.model.InstallStatus;
67
+ import com.google.android.play.core.install.model.UpdateAvailability;
68
+ import com.google.android.gms.tasks.Task;
69
+ import android.content.Intent;
70
+ import android.net.Uri;
71
+
58
72
  @CapacitorPlugin(name = "CapacitorUpdater")
59
73
  public class CapacitorUpdaterPlugin extends Plugin {
60
74
 
@@ -72,7 +86,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
72
86
  private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
73
87
  private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
74
88
 
75
- private final String pluginVersion = "7.37.0";
89
+ private final String pluginVersion = "7.39.0";
76
90
  private static final String DELAY_CONDITION_PREFERENCES = "";
77
91
 
78
92
  private SharedPreferences.Editor editor;
@@ -125,6 +139,12 @@ public class CapacitorUpdaterPlugin extends Plugin {
125
139
  private FrameLayout splashscreenLoaderOverlay;
126
140
  private Runnable splashscreenTimeoutRunnable;
127
141
 
142
+ // Play Store In-App Updates
143
+ private AppUpdateManager appUpdateManager;
144
+ private AppUpdateInfo cachedAppUpdateInfo;
145
+ private static final int APP_UPDATE_REQUEST_CODE = 9001;
146
+ private InstallStateUpdatedListener installStateUpdatedListener;
147
+
128
148
  private void notifyBreakingEvents(final String version) {
129
149
  if (version == null || version.isEmpty()) {
130
150
  return;
@@ -735,6 +755,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
735
755
  }
736
756
  }
737
757
  this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
758
+ this.implementation.cleanupOrphanedTempFolders(Thread.currentThread());
738
759
 
739
760
  // Check again before the expensive delta cache cleanup
740
761
  if (Thread.currentThread().isInterrupted()) {
@@ -2190,27 +2211,6 @@ public class CapacitorUpdaterPlugin extends Plugin {
2190
2211
  }
2191
2212
  }
2192
2213
 
2193
- @Override
2194
- public void handleOnDestroy() {
2195
- try {
2196
- logger.info("onActivityDestroyed " + getActivity().getClass().getName());
2197
- this.implementation.activity = getActivity();
2198
-
2199
- // Clean up shake menu
2200
- if (shakeMenu != null) {
2201
- try {
2202
- shakeMenu.stop();
2203
- shakeMenu = null;
2204
- logger.info("Shake menu cleaned up");
2205
- } catch (Exception e) {
2206
- logger.error("Failed to clean up shake menu: " + e.getMessage());
2207
- }
2208
- }
2209
- } catch (Exception e) {
2210
- logger.error("Failed to run handleOnDestroy: " + e.getMessage());
2211
- }
2212
- }
2213
-
2214
2214
  @PluginMethod
2215
2215
  public void setShakeMenu(final PluginCall call) {
2216
2216
  final Boolean enabled = call.getBoolean("enabled");
@@ -2284,4 +2284,330 @@ public class CapacitorUpdaterPlugin extends Plugin {
2284
2284
  this.implementation.appId = appId;
2285
2285
  call.resolve();
2286
2286
  }
2287
+
2288
+ // ============================================================================
2289
+ // Play Store In-App Update Methods
2290
+ // ============================================================================
2291
+
2292
+ // AppUpdateAvailability enum values matching TypeScript definitions
2293
+ private static final int UPDATE_AVAILABILITY_UNKNOWN = 0;
2294
+ private static final int UPDATE_AVAILABILITY_NOT_AVAILABLE = 1;
2295
+ private static final int UPDATE_AVAILABILITY_AVAILABLE = 2;
2296
+ private static final int UPDATE_AVAILABILITY_IN_PROGRESS = 3;
2297
+
2298
+ // AppUpdateResultCode enum values matching TypeScript definitions
2299
+ private static final int RESULT_OK = 0;
2300
+ private static final int RESULT_CANCELED = 1;
2301
+ private static final int RESULT_FAILED = 2;
2302
+ private static final int RESULT_NOT_AVAILABLE = 3;
2303
+ private static final int RESULT_NOT_ALLOWED = 4;
2304
+ private static final int RESULT_INFO_MISSING = 5;
2305
+
2306
+ private AppUpdateManager getAppUpdateManager() {
2307
+ if (appUpdateManager == null) {
2308
+ appUpdateManager = AppUpdateManagerFactory.create(getContext());
2309
+ }
2310
+ return appUpdateManager;
2311
+ }
2312
+
2313
+ private int mapUpdateAvailability(int playStoreAvailability) {
2314
+ switch (playStoreAvailability) {
2315
+ case UpdateAvailability.UPDATE_AVAILABLE:
2316
+ return UPDATE_AVAILABILITY_AVAILABLE;
2317
+ case UpdateAvailability.UPDATE_NOT_AVAILABLE:
2318
+ return UPDATE_AVAILABILITY_NOT_AVAILABLE;
2319
+ case UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS:
2320
+ return UPDATE_AVAILABILITY_IN_PROGRESS;
2321
+ default:
2322
+ return UPDATE_AVAILABILITY_UNKNOWN;
2323
+ }
2324
+ }
2325
+
2326
+ @PluginMethod
2327
+ public void getAppUpdateInfo(final PluginCall call) {
2328
+ logger.info("Getting Play Store update info");
2329
+
2330
+ try {
2331
+ AppUpdateManager manager = getAppUpdateManager();
2332
+ Task<AppUpdateInfo> appUpdateInfoTask = manager.getAppUpdateInfo();
2333
+
2334
+ appUpdateInfoTask.addOnSuccessListener(appUpdateInfo -> {
2335
+ cachedAppUpdateInfo = appUpdateInfo;
2336
+
2337
+ JSObject result = new JSObject();
2338
+ try {
2339
+ PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0);
2340
+ result.put("currentVersionName", pInfo.versionName);
2341
+ result.put("currentVersionCode", String.valueOf(pInfo.versionCode));
2342
+ } catch (PackageManager.NameNotFoundException e) {
2343
+ result.put("currentVersionName", "0.0.0");
2344
+ result.put("currentVersionCode", "0");
2345
+ }
2346
+
2347
+ result.put("updateAvailability", mapUpdateAvailability(appUpdateInfo.updateAvailability()));
2348
+
2349
+ if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
2350
+ result.put("availableVersionCode", String.valueOf(appUpdateInfo.availableVersionCode()));
2351
+ // Play Store doesn't provide version name, only version code
2352
+ result.put("availableVersionName", String.valueOf(appUpdateInfo.availableVersionCode()));
2353
+ result.put("updatePriority", appUpdateInfo.updatePriority());
2354
+ result.put("immediateUpdateAllowed", appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE));
2355
+ result.put("flexibleUpdateAllowed", appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE));
2356
+
2357
+ Integer stalenessDays = appUpdateInfo.clientVersionStalenessDays();
2358
+ if (stalenessDays != null) {
2359
+ result.put("clientVersionStalenessDays", stalenessDays);
2360
+ }
2361
+ } else {
2362
+ result.put("immediateUpdateAllowed", false);
2363
+ result.put("flexibleUpdateAllowed", false);
2364
+ }
2365
+
2366
+ result.put("installStatus", appUpdateInfo.installStatus());
2367
+
2368
+ call.resolve(result);
2369
+ }).addOnFailureListener(e -> {
2370
+ logger.error("Failed to get app update info: " + e.getMessage());
2371
+ call.reject("Failed to get app update info: " + e.getMessage());
2372
+ });
2373
+ } catch (Exception e) {
2374
+ logger.error("Error getting app update info: " + e.getMessage());
2375
+ call.reject("Error getting app update info: " + e.getMessage());
2376
+ }
2377
+ }
2378
+
2379
+ @PluginMethod
2380
+ public void openAppStore(final PluginCall call) {
2381
+ String packageName = call.getString("packageName");
2382
+ if (packageName == null || packageName.isEmpty()) {
2383
+ packageName = getContext().getPackageName();
2384
+ }
2385
+
2386
+ try {
2387
+ // Try to open Play Store app first
2388
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName));
2389
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2390
+ getContext().startActivity(intent);
2391
+ call.resolve();
2392
+ } catch (android.content.ActivityNotFoundException e) {
2393
+ // Fall back to browser
2394
+ try {
2395
+ Intent intent = new Intent(Intent.ACTION_VIEW,
2396
+ Uri.parse("https://play.google.com/store/apps/details?id=" + packageName));
2397
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
2398
+ getContext().startActivity(intent);
2399
+ call.resolve();
2400
+ } catch (Exception ex) {
2401
+ logger.error("Failed to open Play Store: " + ex.getMessage());
2402
+ call.reject("Failed to open Play Store: " + ex.getMessage());
2403
+ }
2404
+ }
2405
+ }
2406
+
2407
+ @PluginMethod
2408
+ public void performImmediateUpdate(final PluginCall call) {
2409
+ if (cachedAppUpdateInfo == null) {
2410
+ logger.error("No update info available. Call getAppUpdateInfo first.");
2411
+ JSObject result = new JSObject();
2412
+ result.put("code", RESULT_INFO_MISSING);
2413
+ call.resolve(result);
2414
+ return;
2415
+ }
2416
+
2417
+ if (cachedAppUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
2418
+ logger.info("No update available");
2419
+ JSObject result = new JSObject();
2420
+ result.put("code", RESULT_NOT_AVAILABLE);
2421
+ call.resolve(result);
2422
+ return;
2423
+ }
2424
+
2425
+ if (!cachedAppUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
2426
+ logger.info("Immediate update not allowed");
2427
+ JSObject result = new JSObject();
2428
+ result.put("code", RESULT_NOT_ALLOWED);
2429
+ call.resolve(result);
2430
+ return;
2431
+ }
2432
+
2433
+ try {
2434
+ Activity activity = getActivity();
2435
+ if (activity == null) {
2436
+ call.reject("Activity not available");
2437
+ return;
2438
+ }
2439
+
2440
+ // Save the call for later resolution
2441
+ bridge.saveCall(call);
2442
+
2443
+ AppUpdateManager manager = getAppUpdateManager();
2444
+ manager.startUpdateFlowForResult(
2445
+ cachedAppUpdateInfo,
2446
+ activity,
2447
+ AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
2448
+ APP_UPDATE_REQUEST_CODE
2449
+ );
2450
+ } catch (Exception e) {
2451
+ logger.error("Failed to start immediate update: " + e.getMessage());
2452
+ JSObject result = new JSObject();
2453
+ result.put("code", RESULT_FAILED);
2454
+ call.resolve(result);
2455
+ }
2456
+ }
2457
+
2458
+ @PluginMethod
2459
+ public void startFlexibleUpdate(final PluginCall call) {
2460
+ if (cachedAppUpdateInfo == null) {
2461
+ logger.error("No update info available. Call getAppUpdateInfo first.");
2462
+ JSObject result = new JSObject();
2463
+ result.put("code", RESULT_INFO_MISSING);
2464
+ call.resolve(result);
2465
+ return;
2466
+ }
2467
+
2468
+ if (cachedAppUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
2469
+ logger.info("No update available");
2470
+ JSObject result = new JSObject();
2471
+ result.put("code", RESULT_NOT_AVAILABLE);
2472
+ call.resolve(result);
2473
+ return;
2474
+ }
2475
+
2476
+ if (!cachedAppUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
2477
+ logger.info("Flexible update not allowed");
2478
+ JSObject result = new JSObject();
2479
+ result.put("code", RESULT_NOT_ALLOWED);
2480
+ call.resolve(result);
2481
+ return;
2482
+ }
2483
+
2484
+ try {
2485
+ Activity activity = getActivity();
2486
+ if (activity == null) {
2487
+ call.reject("Activity not available");
2488
+ return;
2489
+ }
2490
+
2491
+ // Register listener for flexible update state changes
2492
+ AppUpdateManager manager = getAppUpdateManager();
2493
+
2494
+ // Remove any existing listener
2495
+ if (installStateUpdatedListener != null) {
2496
+ manager.unregisterListener(installStateUpdatedListener);
2497
+ }
2498
+
2499
+ installStateUpdatedListener = state -> {
2500
+ JSObject eventData = new JSObject();
2501
+ eventData.put("installStatus", state.installStatus());
2502
+
2503
+ if (state.installStatus() == InstallStatus.DOWNLOADING) {
2504
+ eventData.put("bytesDownloaded", state.bytesDownloaded());
2505
+ eventData.put("totalBytesToDownload", state.totalBytesToDownload());
2506
+ }
2507
+
2508
+ notifyListeners("onFlexibleUpdateStateChange", eventData);
2509
+ };
2510
+
2511
+ manager.registerListener(installStateUpdatedListener);
2512
+
2513
+ // Save the call for later resolution
2514
+ bridge.saveCall(call);
2515
+
2516
+ manager.startUpdateFlowForResult(
2517
+ cachedAppUpdateInfo,
2518
+ activity,
2519
+ AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build(),
2520
+ APP_UPDATE_REQUEST_CODE
2521
+ );
2522
+ } catch (Exception e) {
2523
+ logger.error("Failed to start flexible update: " + e.getMessage());
2524
+ JSObject result = new JSObject();
2525
+ result.put("code", RESULT_FAILED);
2526
+ call.resolve(result);
2527
+ }
2528
+ }
2529
+
2530
+ @PluginMethod
2531
+ public void completeFlexibleUpdate(final PluginCall call) {
2532
+ try {
2533
+ AppUpdateManager manager = getAppUpdateManager();
2534
+ manager.completeUpdate()
2535
+ .addOnSuccessListener(aVoid -> {
2536
+ // The app will restart, so this may not be called
2537
+ call.resolve();
2538
+ })
2539
+ .addOnFailureListener(e -> {
2540
+ logger.error("Failed to complete flexible update: " + e.getMessage());
2541
+ call.reject("Failed to complete flexible update: " + e.getMessage());
2542
+ });
2543
+ } catch (Exception e) {
2544
+ logger.error("Error completing flexible update: " + e.getMessage());
2545
+ call.reject("Error completing flexible update: " + e.getMessage());
2546
+ }
2547
+ }
2548
+
2549
+ @Override
2550
+ protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {
2551
+ super.handleOnActivityResult(requestCode, resultCode, data);
2552
+
2553
+ if (requestCode == APP_UPDATE_REQUEST_CODE) {
2554
+ PluginCall savedCall = bridge.getSavedCall("com.getcapacitor.PluginCall");
2555
+ if (savedCall == null) {
2556
+ // Try to get any saved call (for backward compatibility)
2557
+ return;
2558
+ }
2559
+
2560
+ JSObject result = new JSObject();
2561
+ if (resultCode == Activity.RESULT_OK) {
2562
+ result.put("code", RESULT_OK);
2563
+ } else if (resultCode == Activity.RESULT_CANCELED) {
2564
+ result.put("code", RESULT_CANCELED);
2565
+ } else {
2566
+ result.put("code", RESULT_FAILED);
2567
+ }
2568
+ savedCall.resolve(result);
2569
+ bridge.releaseCall(savedCall);
2570
+ }
2571
+ }
2572
+
2573
+ @Override
2574
+ protected void handleOnDestroy() {
2575
+ // Clean up the install state listener
2576
+ if (installStateUpdatedListener != null && appUpdateManager != null) {
2577
+ try {
2578
+ appUpdateManager.unregisterListener(installStateUpdatedListener);
2579
+ installStateUpdatedListener = null;
2580
+ } catch (Exception e) {
2581
+ logger.error("Failed to unregister install state listener: " + e.getMessage());
2582
+ }
2583
+ }
2584
+
2585
+ handleOnDestroyInternal();
2586
+ }
2587
+
2588
+ private void handleOnDestroyInternal() {
2589
+ // Original handleOnDestroy code
2590
+ try {
2591
+ logger.info("onActivityDestroyed " + getActivity().getClass().getName());
2592
+ this.implementation.activity = getActivity();
2593
+
2594
+ // Check for 'kill' delay condition on activity destroy
2595
+ // Note: onDestroy is not reliably called - also check on next app launch
2596
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
2597
+ this.delayUpdateUtils.setBackgroundTimestamp(0);
2598
+
2599
+ // Clean up shake menu
2600
+ if (shakeMenu != null) {
2601
+ try {
2602
+ shakeMenu.stop();
2603
+ shakeMenu = null;
2604
+ logger.info("Shake menu cleaned up");
2605
+ } catch (Exception e) {
2606
+ logger.error("Failed to clean up shake menu: " + e.getMessage());
2607
+ }
2608
+ }
2609
+ } catch (Exception e) {
2610
+ logger.error("Failed to run handleOnDestroy: " + e.getMessage());
2611
+ }
2612
+ }
2287
2613
  }
@@ -58,6 +58,7 @@ public class CapgoUpdater {
58
58
  private static final String FALLBACK_VERSION = "pastVersion";
59
59
  private static final String NEXT_VERSION = "nextVersion";
60
60
  private static final String bundleDirectory = "versions";
61
+ private static final String TEMP_UNZIP_PREFIX = "capgo_unzip_";
61
62
 
62
63
  public static final String TAG = "Capacitor-updater";
63
64
  public SharedPreferences.Editor editor;
@@ -447,7 +448,7 @@ public class CapgoUpdater {
447
448
 
448
449
  try {
449
450
  if (!isManifest) {
450
- extractedDir = this.unzip(id, downloaded, this.randomString());
451
+ extractedDir = this.unzip(id, downloaded, TEMP_UNZIP_PREFIX + this.randomString());
451
452
  this.notifyDownload(id, 91);
452
453
  final String idName = bundleDirectory + "/" + id;
453
454
  this.flattenAssets(extractedDir, idName);
@@ -584,6 +585,44 @@ public class CapgoUpdater {
584
585
  }
585
586
  }
586
587
 
588
+ public void cleanupOrphanedTempFolders(final Thread threadToCheck) {
589
+ if (this.documentsDir == null) {
590
+ logger.warn("Documents directory is null, skipping temp folder cleanup");
591
+ return;
592
+ }
593
+
594
+ final File[] entries = this.documentsDir.listFiles();
595
+ if (entries == null) {
596
+ return;
597
+ }
598
+
599
+ for (final File entry : entries) {
600
+ // Check if thread was interrupted (cancelled)
601
+ if (threadToCheck != null && threadToCheck.isInterrupted()) {
602
+ logger.warn("cleanupOrphanedTempFolders was cancelled");
603
+ return;
604
+ }
605
+
606
+ if (!entry.isDirectory()) {
607
+ continue;
608
+ }
609
+
610
+ final String folderName = entry.getName();
611
+
612
+ // Only delete folders with the temp unzip prefix
613
+ if (!folderName.startsWith(TEMP_UNZIP_PREFIX)) {
614
+ continue;
615
+ }
616
+
617
+ try {
618
+ this.deleteDirectory(entry, threadToCheck);
619
+ logger.info("Deleted orphaned temp unzip folder: " + folderName);
620
+ } catch (IOException e) {
621
+ logger.error("Failed to delete orphaned temp folder: " + folderName + " " + e.getMessage());
622
+ }
623
+ }
624
+ }
625
+
587
626
  private void safeDelete(final File target) {
588
627
  if (target == null || !target.exists()) {
589
628
  return;