@capgo/capacitor-updater 7.2.14 → 7.2.18

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.
@@ -322,6 +322,14 @@ public class CapacitorUpdater {
322
322
 
323
323
  if (!isManifest) {
324
324
  String checksumDecrypted = Objects.requireNonNullElse(checksumRes, "");
325
+
326
+ // If public key is present but no checksum provided, refuse installation
327
+ if (!this.publicKey.isEmpty() && checksumDecrypted.isEmpty()) {
328
+ Log.e(CapacitorUpdater.TAG, "Public key present but no checksum provided");
329
+ this.sendStats("checksum_required");
330
+ throw new IOException("Checksum required when public key is present: " + id);
331
+ }
332
+
325
333
  if (!sessionKey.isEmpty()) {
326
334
  CryptoCipherV2.decryptFile(downloaded, publicKey, sessionKey);
327
335
  checksumDecrypted = CryptoCipherV2.decryptChecksum(checksumRes, publicKey);
@@ -49,6 +49,7 @@ import okhttp3.OkHttpClient;
49
49
  import okhttp3.Protocol;
50
50
  import org.json.JSONArray;
51
51
  import org.json.JSONException;
52
+ import org.json.JSONObject;
52
53
 
53
54
  @CapacitorPlugin(name = "CapacitorUpdater")
54
55
  public class CapacitorUpdaterPlugin extends Plugin {
@@ -57,7 +58,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
57
58
  private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
58
59
  private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
59
60
 
60
- private final String PLUGIN_VERSION = "7.2.14";
61
+ private final String PLUGIN_VERSION = "7.2.18";
61
62
  private static final String DELAY_CONDITION_PREFERENCES = "";
62
63
 
63
64
  private SharedPreferences.Editor editor;
@@ -86,6 +87,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
86
87
 
87
88
  private int lastNotifiedStatPercent = 0;
88
89
 
90
+ private DelayUpdateUtils delayUpdateUtils;
91
+
89
92
  public Thread startNewThread(final Runnable function, Number waitTime) {
90
93
  Thread bgTask = new Thread(() -> {
91
94
  try {
@@ -140,9 +143,14 @@ public class CapacitorUpdaterPlugin extends Plugin {
140
143
  .readTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
141
144
  .writeTimeout(this.implementation.timeout, TimeUnit.MILLISECONDS)
142
145
  .build();
143
-
144
146
  this.implementation.directUpdate = this.getConfig().getBoolean("directUpdate", false);
145
147
  this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
148
+ this.delayUpdateUtils = new DelayUpdateUtils(
149
+ this.prefs,
150
+ this.editor,
151
+ this.currentVersionNative,
152
+ CapacitorUpdaterPlugin.this::installNext
153
+ );
146
154
  } catch (final PackageManager.NameNotFoundException e) {
147
155
  Log.e(CapacitorUpdater.TAG, "Error instantiating implementation", e);
148
156
  return;
@@ -831,13 +839,21 @@ public class CapacitorUpdaterPlugin extends Plugin {
831
839
  @PluginMethod
832
840
  public void setMultiDelay(final PluginCall call) {
833
841
  try {
834
- final Object delayConditions = call.getData().opt("delayConditions");
842
+ final JSONArray delayConditions = call.getData().optJSONArray("delayConditions");
835
843
  if (delayConditions == null) {
836
844
  Log.e(CapacitorUpdater.TAG, "setMultiDelay called without delayCondition");
837
845
  call.reject("setMultiDelay called without delayCondition");
838
846
  return;
839
847
  }
840
- if (_setMultiDelay(delayConditions.toString())) {
848
+ for (int i = 0; i < delayConditions.length(); i++) {
849
+ final JSONObject object = delayConditions.optJSONObject(i);
850
+ if (object != null && object.optString("kind").equals("background") && object.optString("value").isEmpty()) {
851
+ object.put("value", "0");
852
+ delayConditions.put(i, object);
853
+ }
854
+ }
855
+
856
+ if (this.delayUpdateUtils.setMultiDelay(delayConditions.toString())) {
841
857
  call.resolve();
842
858
  } else {
843
859
  call.reject("Failed to delay update");
@@ -848,95 +864,15 @@ public class CapacitorUpdaterPlugin extends Plugin {
848
864
  }
849
865
  }
850
866
 
851
- private Boolean _setMultiDelay(String delayConditions) {
852
- try {
853
- this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions);
854
- this.editor.commit();
855
- Log.i(CapacitorUpdater.TAG, "Delay update saved");
856
- return true;
857
- } catch (final Exception e) {
858
- Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling '_setMultiDelay()']", e);
859
- return false;
860
- }
861
- }
862
-
863
- private boolean _cancelDelay(String source) {
864
- try {
865
- this.editor.remove(DELAY_CONDITION_PREFERENCES);
866
- this.editor.commit();
867
- Log.i(CapacitorUpdater.TAG, "All delays canceled from " + source);
868
- return true;
869
- } catch (final Exception e) {
870
- Log.e(CapacitorUpdater.TAG, "Failed to cancel update delay", e);
871
- return false;
872
- }
873
- }
874
-
875
867
  @PluginMethod
876
868
  public void cancelDelay(final PluginCall call) {
877
- if (this._cancelDelay("JS")) {
869
+ if (this.delayUpdateUtils.cancelDelay("JS")) {
878
870
  call.resolve();
879
871
  } else {
880
872
  call.reject("Failed to cancel delay");
881
873
  }
882
874
  }
883
875
 
884
- private void _checkCancelDelay(Boolean killed) {
885
- Gson gson = new Gson();
886
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
887
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
888
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
889
- for (DelayCondition condition : delayConditionList) {
890
- String kind = condition.getKind().toString();
891
- String value = condition.getValue();
892
- if (!kind.isEmpty()) {
893
- switch (kind) {
894
- case "background":
895
- if (!killed) {
896
- this._cancelDelay("background check");
897
- }
898
- break;
899
- case "kill":
900
- if (killed) {
901
- this._cancelDelay("kill check");
902
- this.installNext();
903
- }
904
- break;
905
- case "date":
906
- if (!"".equals(value)) {
907
- try {
908
- final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
909
- Date date = sdf.parse(value);
910
- assert date != null;
911
- if (new Date().compareTo(date) > 0) {
912
- this._cancelDelay("date expired");
913
- }
914
- } catch (final Exception e) {
915
- this._cancelDelay("date parsing issue");
916
- }
917
- } else {
918
- this._cancelDelay("delayVal absent");
919
- }
920
- break;
921
- case "nativeVersion":
922
- if (!"".equals(value)) {
923
- try {
924
- final Version versionLimit = new Version(value);
925
- if (this.currentVersionNative.isAtLeast(versionLimit)) {
926
- this._cancelDelay("nativeVersion above limit");
927
- }
928
- } catch (final Exception e) {
929
- this._cancelDelay("nativeVersion parsing issue");
930
- }
931
- } else {
932
- this._cancelDelay("delayVal absent");
933
- }
934
- break;
935
- }
936
- }
937
- }
938
- }
939
-
940
876
  private Boolean _isAutoUpdateEnabled() {
941
877
  final CapConfig config = CapConfig.loadDefault(this.getActivity());
942
878
  String serverUrl = config.getServerUrl();
@@ -1107,7 +1043,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1107
1043
  );
1108
1044
  if (CapacitorUpdaterPlugin.this.implementation.directUpdate) {
1109
1045
  Gson gson = new Gson();
1110
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
1046
+ String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
1111
1047
  Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1112
1048
  ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
1113
1049
  if (delayConditionList != null && !delayConditionList.isEmpty()) {
@@ -1221,7 +1157,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1221
1157
  private void installNext() {
1222
1158
  try {
1223
1159
  Gson gson = new Gson();
1224
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
1160
+ String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
1225
1161
  Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1226
1162
  ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
1227
1163
  if (delayConditionList != null && !delayConditionList.isEmpty()) {
@@ -1302,7 +1238,8 @@ public class CapacitorUpdaterPlugin extends Plugin {
1302
1238
  public void appMovedToForeground() {
1303
1239
  final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
1304
1240
  CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_foreground", current.getVersionName());
1305
- this._checkCancelDelay(false);
1241
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
1242
+ this.delayUpdateUtils.unsetBackgroundTimestamp();
1306
1243
  if (
1307
1244
  CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() &&
1308
1245
  (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive())
@@ -1320,35 +1257,10 @@ public class CapacitorUpdaterPlugin extends Plugin {
1320
1257
  CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_background", current.getVersionName());
1321
1258
  Log.i(CapacitorUpdater.TAG, "Checking for pending update");
1322
1259
  try {
1323
- Gson gson = new Gson();
1324
- String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
1325
- Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
1326
- ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
1327
- String backgroundValue = null;
1328
- for (DelayCondition delayCondition : delayConditionList) {
1329
- if (delayCondition.getKind().toString().equals("background")) {
1330
- String value = delayCondition.getValue();
1331
- backgroundValue = (value != null && !value.isEmpty()) ? value : "0";
1332
- }
1333
- }
1334
- if (backgroundValue != null) {
1335
- taskRunning = true;
1336
- final Long timeout = Long.parseLong(backgroundValue);
1337
- if (backgroundTask != null) {
1338
- backgroundTask.interrupt();
1339
- }
1340
- backgroundTask = startNewThread(
1341
- () -> {
1342
- taskRunning = false;
1343
- _checkCancelDelay(false);
1344
- installNext();
1345
- },
1346
- timeout
1347
- );
1348
- } else {
1349
- this._checkCancelDelay(false);
1350
- this.installNext();
1351
- }
1260
+ // We need to set "backgrounded time"
1261
+ this.delayUpdateUtils.setBackgroundTimestamp(System.currentTimeMillis());
1262
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.BACKGROUND);
1263
+ this.installNext();
1352
1264
  } catch (final Exception e) {
1353
1265
  Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e);
1354
1266
  }
@@ -1379,7 +1291,7 @@ public class CapacitorUpdaterPlugin extends Plugin {
1379
1291
 
1380
1292
  private void appKilled() {
1381
1293
  Log.d(CapacitorUpdater.TAG, "onActivityDestroyed: all activity destroyed");
1382
- this._checkCancelDelay(true);
1294
+ this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
1383
1295
  }
1384
1296
 
1385
1297
  @Override
@@ -0,0 +1,222 @@
1
+ package ee.forgr.capacitor_updater;
2
+
3
+ import android.content.SharedPreferences;
4
+ import android.util.Log;
5
+ import com.google.common.reflect.TypeToken;
6
+ import com.google.gson.Gson;
7
+ import io.github.g00fy2.versioncompare.Version;
8
+ import java.lang.reflect.Type;
9
+ import java.text.SimpleDateFormat;
10
+ import java.util.ArrayList;
11
+ import java.util.Date;
12
+
13
+ public class DelayUpdateUtils {
14
+
15
+ public static final String DELAY_CONDITION_PREFERENCES = "DELAY_CONDITION_PREFERENCES_CAPGO";
16
+ public static final String BACKGROUND_TIMESTAMP_KEY = "BACKGROUND_TIMESTAMP_KEY_CAPGO";
17
+
18
+ private final SharedPreferences prefs;
19
+ private final SharedPreferences.Editor editor;
20
+ private final Version currentVersionNative;
21
+ private final Runnable installNext;
22
+
23
+ public DelayUpdateUtils(SharedPreferences prefs, SharedPreferences.Editor editor, Version currentVersionNative, Runnable installNext) {
24
+ this.prefs = prefs;
25
+ this.editor = editor;
26
+ this.currentVersionNative = currentVersionNative;
27
+ this.installNext = installNext;
28
+ }
29
+
30
+ public enum CancelDelaySource {
31
+ KILLED,
32
+ BACKGROUND,
33
+ FOREGROUND
34
+ }
35
+
36
+ public void checkCancelDelay(CancelDelaySource source) {
37
+ Gson gson = new Gson();
38
+ String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]");
39
+ Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
40
+ ArrayList<DelayCondition> delayConditionList = gson.fromJson(delayUpdatePreferences, type);
41
+ ArrayList<DelayCondition> delayConditionListToKeep = new ArrayList<>(delayConditionList.size());
42
+ int index = 0;
43
+
44
+ for (DelayCondition condition : delayConditionList) {
45
+ DelayUntilNext kind = condition.getKind();
46
+ String value = condition.getValue();
47
+ switch (kind) {
48
+ case DelayUntilNext.background:
49
+ if (source == CancelDelaySource.FOREGROUND) {
50
+ long backgroundedAt = getBackgroundTimestamp();
51
+ long now = System.currentTimeMillis();
52
+ long delta = Math.max(0, now - backgroundedAt);
53
+ long longValue = 0L;
54
+ try {
55
+ longValue = Long.parseLong(value);
56
+ } catch (NumberFormatException e) {
57
+ Log.e(
58
+ CapacitorUpdater.TAG,
59
+ "Background condition (value: " +
60
+ value +
61
+ ") had an invalid value at index " +
62
+ index +
63
+ ". We will likely remove it."
64
+ );
65
+ }
66
+
67
+ if (delta > longValue) {
68
+ Log.i(
69
+ CapacitorUpdater.TAG,
70
+ "Background condition (value: " +
71
+ value +
72
+ ") deleted at index " +
73
+ index +
74
+ ". Delta: " +
75
+ delta +
76
+ ", longValue: " +
77
+ longValue
78
+ );
79
+ }
80
+ } else {
81
+ delayConditionListToKeep.add(condition);
82
+ Log.i(
83
+ CapacitorUpdater.TAG,
84
+ "Background delay (value: " +
85
+ value +
86
+ ") condition kept at index " +
87
+ index +
88
+ " (source: " +
89
+ source.toString() +
90
+ ")"
91
+ );
92
+ }
93
+ break;
94
+ case DelayUntilNext.kill:
95
+ if (source == CancelDelaySource.KILLED) {
96
+ this.installNext.run();
97
+ } else {
98
+ delayConditionListToKeep.add(condition);
99
+ Log.i(
100
+ CapacitorUpdater.TAG,
101
+ "Kill delay (value: " + value + ") condition kept at index " + index + " (source: " + source.toString() + ")"
102
+ );
103
+ }
104
+ break;
105
+ case DelayUntilNext.date:
106
+ if (!"".equals(value)) {
107
+ try {
108
+ final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
109
+ Date date = sdf.parse(value);
110
+ assert date != null;
111
+ if (new Date().compareTo(date) > 0) {
112
+ Log.i(
113
+ CapacitorUpdater.TAG,
114
+ "Date delay (value: " + value + ") condition removed due to expired date at index " + index
115
+ );
116
+ } else {
117
+ delayConditionListToKeep.add(condition);
118
+ Log.i(CapacitorUpdater.TAG, "Date delay (value: " + value + ") condition kept at index " + index);
119
+ }
120
+ } catch (final Exception e) {
121
+ Log.e(
122
+ CapacitorUpdater.TAG,
123
+ "Date delay (value: " + value + ") condition removed due to parsing issue at index " + index,
124
+ e
125
+ );
126
+ }
127
+ } else {
128
+ Log.d(
129
+ CapacitorUpdater.TAG,
130
+ "Date delay (value: " + value + ") condition removed due to empty value at index " + index
131
+ );
132
+ }
133
+ break;
134
+ case DelayUntilNext.nativeVersion:
135
+ if (!"".equals(value)) {
136
+ try {
137
+ final Version versionLimit = new Version(value);
138
+ if (this.currentVersionNative.isAtLeast(versionLimit)) {
139
+ Log.i(
140
+ CapacitorUpdater.TAG,
141
+ "Native version delay (value: " + value + ") condition removed due to above limit at index " + index
142
+ );
143
+ } else {
144
+ delayConditionListToKeep.add(condition);
145
+ Log.i(CapacitorUpdater.TAG, "Native version delay (value: " + value + ") condition kept at index " + index);
146
+ }
147
+ } catch (final Exception e) {
148
+ Log.e(
149
+ CapacitorUpdater.TAG,
150
+ "Native version delay (value: " + value + ") condition removed due to parsing issue at index " + index,
151
+ e
152
+ );
153
+ }
154
+ } else {
155
+ Log.d(
156
+ CapacitorUpdater.TAG,
157
+ "Native version delay (value: " + value + ") condition removed due to empty value at index " + index
158
+ );
159
+ }
160
+ break;
161
+ }
162
+ index++;
163
+ }
164
+
165
+ if (!delayConditionListToKeep.isEmpty()) {
166
+ this.setMultiDelay(gson.toJson(delayConditionListToKeep));
167
+ }
168
+ }
169
+
170
+ public Boolean setMultiDelay(String delayConditions) {
171
+ try {
172
+ this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions);
173
+ this.editor.commit();
174
+ Log.i(CapacitorUpdater.TAG, "Delay update saved");
175
+ return true;
176
+ } catch (final Exception e) {
177
+ Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling '_setMultiDelay()']", e);
178
+ return false;
179
+ }
180
+ }
181
+
182
+ public void setBackgroundTimestamp(long backgroundTimestamp) {
183
+ try {
184
+ this.editor.putLong(BACKGROUND_TIMESTAMP_KEY, backgroundTimestamp);
185
+ this.editor.commit();
186
+ Log.i(CapacitorUpdater.TAG, "Delay update saved");
187
+ } catch (final Exception e) {
188
+ Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling '_setBackgroundTimestamp()']", e);
189
+ }
190
+ }
191
+
192
+ public void unsetBackgroundTimestamp() {
193
+ try {
194
+ this.editor.remove(BACKGROUND_TIMESTAMP_KEY);
195
+ this.editor.commit();
196
+ Log.i(CapacitorUpdater.TAG, "Delay update saved");
197
+ } catch (final Exception e) {
198
+ Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling '_unsetBackgroundTimestamp()']", e);
199
+ }
200
+ }
201
+
202
+ private long getBackgroundTimestamp() {
203
+ try {
204
+ return this.prefs.getLong(BACKGROUND_TIMESTAMP_KEY, 0);
205
+ } catch (final Exception e) {
206
+ Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling '_getBackgroundTimestamp()']", e);
207
+ return 0;
208
+ }
209
+ }
210
+
211
+ public boolean cancelDelay(String source) {
212
+ try {
213
+ this.editor.remove(DELAY_CONDITION_PREFERENCES);
214
+ this.editor.commit();
215
+ Log.i(CapacitorUpdater.TAG, "All delays canceled from " + source);
216
+ return true;
217
+ } catch (final Exception e) {
218
+ Log.e(CapacitorUpdater.TAG, "Failed to cancel update delay", e);
219
+ return false;
220
+ }
221
+ }
222
+ }
@@ -45,11 +45,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
45
45
  CAPPluginMethod(name: "getNextBundle", returnType: CAPPluginReturnPromise)
46
46
  ]
47
47
  public var implementation = CapacitorUpdater()
48
- private let PLUGIN_VERSION: String = "7.2.14"
48
+ private let PLUGIN_VERSION: String = "7.2.18"
49
49
  static let updateUrlDefault = "https://plugin.capgo.app/updates"
50
50
  static let statsUrlDefault = "https://plugin.capgo.app/stats"
51
51
  static let channelUrlDefault = "https://plugin.capgo.app/channel_self"
52
- let DELAY_CONDITION_PREFERENCES = ""
52
+ // Note: DELAY_CONDITION_PREFERENCES is now defined in DelayUpdateUtils.DELAY_CONDITION_PREFERENCES
53
53
  private var updateUrl = ""
54
54
  private var statsUrl = ""
55
55
  private var backgroundTaskID: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid
@@ -67,6 +67,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
67
67
  private var periodCheckDelay = 0
68
68
  let semaphoreReady = DispatchSemaphore(value: 0)
69
69
 
70
+ private var delayUpdateUtils: DelayUpdateUtils!
71
+
70
72
  override public func load() {
71
73
  #if targetEnvironment(simulator)
72
74
  print("\(CapacitorUpdater.TAG) ::::: SIMULATOR :::::")
@@ -109,6 +111,11 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
109
111
  implementation.publicKey = getConfig().getString("publicKey", "")!
110
112
  implementation.notifyDownloadRaw = notifyDownload
111
113
  implementation.PLUGIN_VERSION = self.PLUGIN_VERSION
114
+
115
+ // Initialize DelayUpdateUtils
116
+ self.delayUpdateUtils = DelayUpdateUtils(currentVersionNative: currentVersionNative, installNext: { [weak self] in
117
+ self?.installNext()
118
+ })
112
119
  let config = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor().legacyConfig
113
120
  implementation.appId = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String ?? ""
114
121
  implementation.appId = config?["appId"] as? String ?? implementation.appId
@@ -291,6 +298,18 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
291
298
  DispatchQueue.global(qos: .background).async {
292
299
  do {
293
300
  let next = try self.implementation.download(url: url!, version: version, sessionKey: sessionKey)
301
+ // If public key is present but no checksum provided, refuse installation
302
+ if self.implementation.publicKey != "" && checksum == "" {
303
+ print("\(CapacitorUpdater.TAG) Public key present but no checksum provided")
304
+ self.implementation.sendStats(action: "checksum_required", versionName: next.getVersionName())
305
+ let id = next.getId()
306
+ let resDel = self.implementation.delete(id: id)
307
+ if !resDel {
308
+ print("\(CapacitorUpdater.TAG) Delete failed, id \(id) doesn't exist")
309
+ }
310
+ throw ObjectSavableError.checksum
311
+ }
312
+
294
313
  checksum = try CryptoCipherV2.decryptChecksum(checksum: checksum, publicKey: self.implementation.publicKey)
295
314
  if (checksum != "" || self.implementation.publicKey != "") && next.getChecksum() != checksum {
296
315
  print("\(CapacitorUpdater.TAG) Error checksum", next.getChecksum(), checksum)
@@ -549,96 +568,45 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
549
568
  call.reject("setMultiDelay called without delayCondition")
550
569
  return
551
570
  }
552
- let delayConditions: String = toJson(object: delayConditionList)
553
- if _setMultiDelay(delayConditions: delayConditions) {
554
- call.resolve()
555
- } else {
556
- call.reject("Failed to delay update")
557
- }
558
- }
559
571
 
560
- private func _setMultiDelay(delayConditions: String?) -> Bool {
561
- if delayConditions != nil && "" != delayConditions {
562
- UserDefaults.standard.set(delayConditions, forKey: DELAY_CONDITION_PREFERENCES)
563
- UserDefaults.standard.synchronize()
564
- print("\(CapacitorUpdater.TAG) Delay update saved.")
565
- return true
572
+ // Handle background conditions with empty value (set to "0")
573
+ if var modifiableList = delayConditionList as? [[String: Any]] {
574
+ for i in 0..<modifiableList.count {
575
+ if let kind = modifiableList[i]["kind"] as? String,
576
+ kind == "background",
577
+ let value = modifiableList[i]["value"] as? String,
578
+ value.isEmpty {
579
+ modifiableList[i]["value"] = "0"
580
+ }
581
+ }
582
+ let delayConditions: String = toJson(object: modifiableList)
583
+ if delayUpdateUtils.setMultiDelay(delayConditions: delayConditions) {
584
+ call.resolve()
585
+ } else {
586
+ call.reject("Failed to delay update")
587
+ }
566
588
  } else {
567
- print("\(CapacitorUpdater.TAG) Failed to delay update, [Error calling '_setMultiDelay()']")
568
- return false
589
+ let delayConditions: String = toJson(object: delayConditionList)
590
+ if delayUpdateUtils.setMultiDelay(delayConditions: delayConditions) {
591
+ call.resolve()
592
+ } else {
593
+ call.reject("Failed to delay update")
594
+ }
569
595
  }
570
596
  }
571
597
 
572
- private func _cancelDelay(source: String) {
573
- print("\(CapacitorUpdater.TAG) delay Canceled from \(source)")
574
- UserDefaults.standard.removeObject(forKey: DELAY_CONDITION_PREFERENCES)
575
- UserDefaults.standard.synchronize()
576
- }
598
+ // Note: _setMultiDelay and _cancelDelay methods have been moved to DelayUpdateUtils class
577
599
 
578
600
  @objc func cancelDelay(_ call: CAPPluginCall) {
579
- self._cancelDelay(source: "JS")
580
- call.resolve()
581
- }
582
-
583
- private func _checkCancelDelay(killed: Bool) {
584
- let delayUpdatePreferences = UserDefaults.standard.string(forKey: DELAY_CONDITION_PREFERENCES) ?? "[]"
585
- let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
586
- let kind: String = obj.value(forKey: "kind") as! String
587
- let value: String? = obj.value(forKey: "value") as? String
588
- return DelayCondition(kind: kind, value: value)
589
- }
590
- for condition in delayConditionList {
591
- let kind: String? = condition.getKind()
592
- let value: String? = condition.getValue()
593
- if kind != nil {
594
- switch kind {
595
- case "background":
596
- if !killed {
597
- self._cancelDelay(source: "background check")
598
- }
599
- case "kill":
600
- if killed {
601
- self._cancelDelay(source: "kill check")
602
- // instant install for kill action
603
- self.installNext()
604
- }
605
- case "date":
606
- if value != nil && value != "" {
607
- let dateFormatter = ISO8601DateFormatter()
608
- dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
609
- guard let ExpireDate = dateFormatter.date(from: value!) else {
610
- self._cancelDelay(source: "date parsing issue")
611
- return
612
- }
613
- if ExpireDate < Date() {
614
- self._cancelDelay(source: "date expired")
615
- }
616
- } else {
617
- self._cancelDelay(source: "delayVal absent")
618
- }
619
- case "nativeVersion":
620
- if value != nil && value != "" {
621
- do {
622
- let versionLimit = try Version(value!)
623
- if self.currentVersionNative >= versionLimit {
624
- self._cancelDelay(source: "nativeVersion above limit")
625
- }
626
- } catch {
627
- self._cancelDelay(source: "nativeVersion parsing issue")
628
- }
629
- } else {
630
- self._cancelDelay(source: "delayVal absent")
631
- }
632
- case .none:
633
- print("\(CapacitorUpdater.TAG) _checkCancelDelay switch case none error")
634
- case .some:
635
- print("\(CapacitorUpdater.TAG) _checkCancelDelay switch case some error")
636
- }
637
- }
601
+ if delayUpdateUtils.cancelDelay(source: "JS") {
602
+ call.resolve()
603
+ } else {
604
+ call.reject("Failed to cancel delay")
638
605
  }
639
- // self.checkAppReady() why this here?
640
606
  }
641
607
 
608
+ // Note: _checkCancelDelay method has been moved to DelayUpdateUtils class
609
+
642
610
  private func _isAutoUpdateEnabled() -> Bool {
643
611
  let instanceDescriptor = (self.bridge?.viewController as? CAPBridgeViewController)?.instanceDescriptor()
644
612
  if instanceDescriptor?.serverURL != nil {
@@ -818,10 +786,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
818
786
  return
819
787
  }
820
788
  if self.directUpdate {
789
+ let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
790
+ let delayConditionList: [DelayCondition] = self.fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
791
+ let kind: String = obj.value(forKey: "kind") as! String
792
+ let value: String? = obj.value(forKey: "value") as? String
793
+ return DelayCondition(kind: kind, value: value)
794
+ }
795
+ if !delayConditionList.isEmpty {
796
+ print("\(CapacitorUpdater.TAG) Update delayed until delay conditions met")
797
+ self.endBackGroundTaskWithNotif(msg: "Update delayed until delay conditions met", latestVersionName: latestVersionName, current: next, error: false)
798
+ return
799
+ }
821
800
  _ = self.implementation.set(bundle: next)
822
801
  _ = self._reload()
823
- self.directUpdate = false
824
- self.endBackGroundTaskWithNotif(msg: "update installed", latestVersionName: latestVersionName, current: current, error: false)
802
+ self.endBackGroundTaskWithNotif(msg: "update installed", latestVersionName: latestVersionName, current: next, error: false)
825
803
  } else {
826
804
  self.notifyListeners("updateAvailable", data: ["bundle": next.toJSON()])
827
805
  _ = self.implementation.setNextBundle(next: next.getId())
@@ -844,17 +822,17 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
844
822
 
845
823
  @objc func appKilled() {
846
824
  print("\(CapacitorUpdater.TAG) onActivityDestroyed: all activity destroyed")
847
- self._checkCancelDelay(killed: true)
825
+ self.delayUpdateUtils.checkCancelDelay(source: .killed)
848
826
  }
849
827
 
850
828
  private func installNext() {
851
- let delayUpdatePreferences = UserDefaults.standard.string(forKey: DELAY_CONDITION_PREFERENCES) ?? "[]"
852
- let delayConditionList: [DelayCondition]? = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
829
+ let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
830
+ let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
853
831
  let kind: String = obj.value(forKey: "kind") as! String
854
832
  let value: String? = obj.value(forKey: "value") as? String
855
833
  return DelayCondition(kind: kind, value: value)
856
834
  }
857
- if delayConditionList != nil && delayConditionList?.capacity != 0 {
835
+ if !delayConditionList.isEmpty {
858
836
  print("\(CapacitorUpdater.TAG) Update delayed until delay conditions met")
859
837
  return
860
838
  }
@@ -893,6 +871,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
893
871
  @objc func appMovedToForeground() {
894
872
  let current: BundleInfo = self.implementation.getCurrentBundle()
895
873
  self.implementation.sendStats(action: "app_moved_to_foreground", versionName: current.getVersionName())
874
+ self.delayUpdateUtils.checkCancelDelay(source: .foreground)
875
+ self.delayUpdateUtils.unsetBackgroundTimestamp()
896
876
  if backgroundWork != nil && taskRunning {
897
877
  backgroundWork!.cancel()
898
878
  print("\(CapacitorUpdater.TAG) Background Timer Task canceled, Activity resumed before timer completes")
@@ -929,38 +909,15 @@ public class CapacitorUpdaterPlugin: CAPPlugin, CAPBridgedPlugin {
929
909
  }
930
910
 
931
911
  @objc func appMovedToBackground() {
932
- self.implementation.sendStats(action: "app_moved_to_background")
912
+ let current: BundleInfo = self.implementation.getCurrentBundle()
913
+ self.implementation.sendStats(action: "app_moved_to_background", versionName: current.getVersionName())
933
914
  print("\(CapacitorUpdater.TAG) Check for pending update")
934
- let delayUpdatePreferences = UserDefaults.standard.string(forKey: DELAY_CONDITION_PREFERENCES) ?? "[]"
935
-
936
- let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
937
- let kind: String = obj.value(forKey: "kind") as! String
938
- let value: String? = obj.value(forKey: "value") as? String
939
- return DelayCondition(kind: kind, value: value)
940
- }
941
- var backgroundValue: String?
942
- for delayCondition in delayConditionList {
943
- if delayCondition.getKind() == "background" {
944
- let value: String? = delayCondition.getValue()
945
- backgroundValue = (value != nil && value != "") ? value! : "0"
946
- }
947
- }
948
- if backgroundValue != nil {
949
- self.taskRunning = true
950
- let interval: Double = (Double(backgroundValue!) ?? 0.0) / 1000
951
- self.backgroundWork?.cancel()
952
- self.backgroundWork = DispatchWorkItem(block: {
953
- // IOS never executes this task in background
954
- self.taskRunning = false
955
- self._checkCancelDelay(killed: false)
956
- self.installNext()
957
- })
958
- DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + interval, execute: self.backgroundWork!)
959
- } else {
960
- self._checkCancelDelay(killed: false)
961
- self.installNext()
962
- }
963
915
 
916
+ // Set background timestamp
917
+ let backgroundTimestamp = Int64(Date().timeIntervalSince1970 * 1000) // Convert to milliseconds
918
+ self.delayUpdateUtils.setBackgroundTimestamp(backgroundTimestamp)
919
+ self.delayUpdateUtils.checkCancelDelay(source: .background)
920
+ self.installNext()
964
921
  }
965
922
 
966
923
  @objc func getNextBundle(_ call: CAPPluginCall) {
@@ -0,0 +1,220 @@
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
+ //
8
+ // DelayUpdateUtils.swift
9
+ // Plugin
10
+ //
11
+ // Created by Auto-generated based on Android implementation
12
+ // Copyright © 2024 Capgo. All rights reserved.
13
+ //
14
+
15
+ import Foundation
16
+ import Version
17
+
18
+ public class DelayUpdateUtils {
19
+
20
+ static let DELAY_CONDITION_PREFERENCES = "DELAY_CONDITION_PREFERENCES_CAPGO"
21
+ static let BACKGROUND_TIMESTAMP_KEY = "BACKGROUND_TIMESTAMP_KEY_CAPGO"
22
+
23
+ private let currentVersionNative: Version
24
+ private let installNext: () -> Void
25
+
26
+ public enum CancelDelaySource {
27
+ case killed
28
+ case background
29
+ case foreground
30
+
31
+ var description: String {
32
+ switch self {
33
+ case .killed: return "KILLED"
34
+ case .background: return "BACKGROUND"
35
+ case .foreground: return "FOREGROUND"
36
+ }
37
+ }
38
+ }
39
+
40
+ public init(currentVersionNative: Version, installNext: @escaping () -> Void) {
41
+ self.currentVersionNative = currentVersionNative
42
+ self.installNext = installNext
43
+ }
44
+
45
+ public func checkCancelDelay(source: CancelDelaySource) {
46
+ let delayUpdatePreferences = UserDefaults.standard.string(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES) ?? "[]"
47
+ let delayConditionList: [DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in
48
+ let kind: String = obj.value(forKey: "kind") as! String
49
+ let value: String? = obj.value(forKey: "value") as? String
50
+ return DelayCondition(kind: kind, value: value)
51
+ }
52
+
53
+ var delayConditionListToKeep: [DelayCondition] = []
54
+ var index = 0
55
+
56
+ for condition in delayConditionList {
57
+ let kind = condition.getKind()
58
+ let value = condition.getValue()
59
+
60
+ switch kind {
61
+ case "background":
62
+ if source == .foreground {
63
+ let backgroundedAt = getBackgroundTimestamp()
64
+ let now = Int64(Date().timeIntervalSince1970 * 1000) // Convert to milliseconds
65
+ let delta = max(0, now - backgroundedAt)
66
+
67
+ var longValue: Int64 = 0
68
+ if let value = value, !value.isEmpty {
69
+ longValue = Int64(value) ?? 0
70
+ }
71
+
72
+ if delta > longValue {
73
+ print("\(CapacitorUpdater.TAG) Background condition (value: \(value ?? "")) deleted at index \(index). Delta: \(delta), longValue: \(longValue)")
74
+ } else {
75
+ delayConditionListToKeep.append(condition)
76
+ print("\(CapacitorUpdater.TAG) Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
77
+ }
78
+ } else {
79
+ delayConditionListToKeep.append(condition)
80
+ print("\(CapacitorUpdater.TAG) Background delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
81
+ }
82
+
83
+ case "kill":
84
+ if source == .killed {
85
+ self.installNext()
86
+ } else {
87
+ delayConditionListToKeep.append(condition)
88
+ print("\(CapacitorUpdater.TAG) Kill delay (value: \(value ?? "")) condition kept at index \(index) (source: \(source.description))")
89
+ }
90
+
91
+ case "date":
92
+ if let value = value, !value.isEmpty {
93
+ do {
94
+ let dateFormatter = ISO8601DateFormatter()
95
+ dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
96
+
97
+ if let date = dateFormatter.date(from: value) {
98
+ if Date() > date {
99
+ print("\(CapacitorUpdater.TAG) Date delay (value: \(value)) condition removed due to expired date at index \(index)")
100
+ } else {
101
+ delayConditionListToKeep.append(condition)
102
+ print("\(CapacitorUpdater.TAG) Date delay (value: \(value)) condition kept at index \(index)")
103
+ }
104
+ } else {
105
+ print("\(CapacitorUpdater.TAG) Date delay (value: \(value)) condition removed due to parsing issue at index \(index)")
106
+ }
107
+ } catch {
108
+ print("\(CapacitorUpdater.TAG) Date delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
109
+ }
110
+ } else {
111
+ print("\(CapacitorUpdater.TAG) Date delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
112
+ }
113
+
114
+ case "nativeVersion":
115
+ if let value = value, !value.isEmpty {
116
+ do {
117
+ let versionLimit = try Version(value)
118
+ if currentVersionNative >= versionLimit {
119
+ print("\(CapacitorUpdater.TAG) Native version delay (value: \(value)) condition removed due to above limit at index \(index)")
120
+ } else {
121
+ delayConditionListToKeep.append(condition)
122
+ print("\(CapacitorUpdater.TAG) Native version delay (value: \(value)) condition kept at index \(index)")
123
+ }
124
+ } catch {
125
+ print("\(CapacitorUpdater.TAG) Native version delay (value: \(value)) condition removed due to parsing issue at index \(index): \(error)")
126
+ }
127
+ } else {
128
+ print("\(CapacitorUpdater.TAG) Native version delay (value: \(value ?? "")) condition removed due to empty value at index \(index)")
129
+ }
130
+
131
+ default:
132
+ print("\(CapacitorUpdater.TAG) Unknown delay condition kind: \(kind) at index \(index)")
133
+ }
134
+
135
+ index += 1
136
+ }
137
+
138
+ if !delayConditionListToKeep.isEmpty {
139
+ let json = toJson(object: delayConditionListToKeep.map { $0.toJSON() })
140
+ _ = setMultiDelay(delayConditions: json)
141
+ } else {
142
+ // Clear all delay conditions if none are left to keep
143
+ _ = cancelDelay(source: "checkCancelDelay")
144
+ }
145
+ }
146
+
147
+ public func setMultiDelay(delayConditions: String) -> Bool {
148
+ do {
149
+ UserDefaults.standard.set(delayConditions, forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
150
+ UserDefaults.standard.synchronize()
151
+ print("\(CapacitorUpdater.TAG) Delay update saved")
152
+ return true
153
+ } catch {
154
+ print("\(CapacitorUpdater.TAG) Failed to delay update, [Error calling 'setMultiDelay()']: \(error)")
155
+ return false
156
+ }
157
+ }
158
+
159
+ public func setBackgroundTimestamp(_ backgroundTimestamp: Int64) {
160
+ do {
161
+ UserDefaults.standard.set(backgroundTimestamp, forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
162
+ UserDefaults.standard.synchronize()
163
+ print("\(CapacitorUpdater.TAG) Background timestamp saved")
164
+ } catch {
165
+ print("\(CapacitorUpdater.TAG) Failed to save background timestamp, [Error calling 'setBackgroundTimestamp()']: \(error)")
166
+ }
167
+ }
168
+
169
+ public func unsetBackgroundTimestamp() {
170
+ do {
171
+ UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY)
172
+ UserDefaults.standard.synchronize()
173
+ print("\(CapacitorUpdater.TAG) Background timestamp removed")
174
+ } catch {
175
+ print("\(CapacitorUpdater.TAG) Failed to remove background timestamp, [Error calling 'unsetBackgroundTimestamp()']: \(error)")
176
+ }
177
+ }
178
+
179
+ private func getBackgroundTimestamp() -> Int64 {
180
+ do {
181
+ let timestamp = UserDefaults.standard.object(forKey: DelayUpdateUtils.BACKGROUND_TIMESTAMP_KEY) as? Int64 ?? 0
182
+ return timestamp
183
+ } catch {
184
+ print("\(CapacitorUpdater.TAG) Failed to get background timestamp, [Error calling 'getBackgroundTimestamp()']: \(error)")
185
+ return 0
186
+ }
187
+ }
188
+
189
+ public func cancelDelay(source: String) -> Bool {
190
+ do {
191
+ UserDefaults.standard.removeObject(forKey: DelayUpdateUtils.DELAY_CONDITION_PREFERENCES)
192
+ UserDefaults.standard.synchronize()
193
+ print("\(CapacitorUpdater.TAG) All delays canceled from \(source)")
194
+ return true
195
+ } catch {
196
+ print("\(CapacitorUpdater.TAG) Failed to cancel update delay: \(error)")
197
+ return false
198
+ }
199
+ }
200
+
201
+ // MARK: - Helper methods
202
+
203
+ private func toJson(object: Any) -> String {
204
+ guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else {
205
+ return ""
206
+ }
207
+ return String(data: data, encoding: String.Encoding.utf8) ?? ""
208
+ }
209
+
210
+ private func fromJsonArr(json: String) -> [NSObject] {
211
+ guard let jsonData = json.data(using: .utf8) else {
212
+ return []
213
+ }
214
+ let object = try? JSONSerialization.jsonObject(
215
+ with: jsonData,
216
+ options: .mutableContainers
217
+ ) as? [NSObject]
218
+ return object ?? []
219
+ }
220
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-updater",
3
- "version": "7.2.14",
3
+ "version": "7.2.18",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Live update for capacitor apps",
6
6
  "main": "dist/plugin.cjs.js",