From fb42865c866b1663c3d2943a4dc08f52fff57448 Mon Sep 17 00:00:00 2001 From: Luca Peruzzo Date: Wed, 14 Sep 2022 14:01:51 +0200 Subject: [PATCH] feat: multi delay --- README.md | 198 ++++++++++++++++- .../CapacitorUpdaterPlugin.java | 204 +++++++++++++----- .../capacitor_updater/DelayCondition.java | 48 +++++ .../capacitor_updater/DelayUntilNext.java | 8 + ios/Plugin.xcodeproj/project.pbxproj | 8 + ios/Plugin/CapacitorUpdaterPlugin.m | 1 + ios/Plugin/CapacitorUpdaterPlugin.swift | 202 +++++++++++++---- ios/Plugin/DelayCondition.swift | 68 ++++++ ios/Plugin/DelayUntilNext.swift | 24 +++ src/definitions.ts | 40 +++- src/web.ts | 10 +- 11 files changed, 692 insertions(+), 119 deletions(-) create mode 100644 android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java create mode 100644 android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java create mode 100644 ios/Plugin/DelayCondition.swift create mode 100644 ios/Plugin/DelayUntilNext.swift diff --git a/README.md b/README.md index 4e0a5f7d..aeea63f2 100644 --- a/README.md +++ b/README.md @@ -127,12 +127,18 @@ Capacitor Updator works by unzipping a compiled app bundle to the native device * [`reset(...)`](#reset) * [`current()`](#current) * [`reload()`](#reload) +* [`setMultiDelay(...)`](#setmultidelay) * [`setDelay(...)`](#setdelay) +* [`cancelDelay()`](#canceldelay) +* [`getLatest(...)`](#getlatest) * [`addListener('download', ...)`](#addlistenerdownload) +* [`addListener('noNeedUpdate', ...)`](#addlistenernoneedupdate) +* [`addListener('updateAvailable', ...)`](#addlistenerupdateavailable) * [`addListener('downloadComplete', ...)`](#addlistenerdownloadcomplete) * [`addListener('majorAvailable', ...)`](#addlistenermajoravailable) * [`addListener('updateFailed', ...)`](#addlistenerupdatefailed) -* [`getId()`](#getid) +* [`addListener('downloadFailed', ...)`](#addlistenerdownloadfailed) +* [`getDeviceId()`](#getdeviceid) * [`getPluginVersion()`](#getpluginversion) * [`isAutoUpdateEnabled()`](#isautoupdateenabled) * [`addListener(string, ...)`](#addlistenerstring) @@ -161,14 +167,14 @@ Notify Capacitor Updater that the current bundle is working (a rollback will occ ### download(...) ```typescript -download(options: { url: string; version?: string; }) => Promise +download(options: { url: string; version: string; }) => Promise ``` Download a new version from the provided URL, it should be a zip file, with files inside or with a unique id inside with all your files -| Param | Type | -| ------------- | ----------------------------------------------- | -| **`options`** | { url: string; version?: string; } | +| Param | Type | +| ------------- | ---------------------------------------------- | +| **`options`** | { url: string; version: string; } | **Returns:** Promise<BundleInfo> @@ -274,18 +280,69 @@ Reload the view -------------------- +### setMultiDelay(...) + +```typescript +setMultiDelay(options: { delayConditions: DelayCondition[]; }) => Promise +``` + +Set DelayCondition, skip updates until one of the conditions is met + +| Param | Type | Description | +| ------------- | --------------------------------------------------- | ------------------------------------------------------------------------ | +| **`options`** | { delayConditions: DelayCondition[]; } | are the {@link DelayCondition} list to set | + +**Since:** 4.3.0 + +-------------------- + + ### setDelay(...) ```typescript -setDelay(options: { delay: boolean; }) => Promise +setDelay(options: DelayCondition) => Promise +``` + +Set DelayCondition, skip updates until the condition is met + +| Param | Type | Description | +| ------------- | --------------------------------------------------------- | ------------------------------------------------------------------ | +| **`options`** | DelayCondition | is the {@link DelayCondition} to set | + +**Since:** 4.0.0 + +-------------------- + + +### cancelDelay() + +```typescript +cancelDelay() => Promise +``` + +Cancel delay to updates as usual + +**Since:** 4.0.0 + +-------------------- + + +### getLatest(...) + +```typescript +getLatest(options: { delay: boolean; }) => Promise ``` -Set delay to skip updates in the next time the app goes into the background +Get Latest version available from update Url | Param | Type | | ------------- | -------------------------------- | | **`options`** | { delay: boolean; } | +**Returns:** Promise<latestVersion> + +**Since:** 4.0.0 + -------------------- @@ -309,6 +366,46 @@ Listen for download event in the App, let you know when the download is started, -------------------- +### addListener('noNeedUpdate', ...) + +```typescript +addListener(eventName: 'noNeedUpdate', listenerFunc: NoNeedListener) => Promise & PluginListenerHandle +``` + +Listen for no need to update event, usefull when you want force check every time the app is launched + +| Param | Type | +| ------------------ | --------------------------------------------------------- | +| **`eventName`** | 'noNeedUpdate' | +| **`listenerFunc`** | NoNeedListener | + +**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle + +**Since:** 4.0.0 + +-------------------- + + +### addListener('updateAvailable', ...) + +```typescript +addListener(eventName: 'updateAvailable', listenerFunc: UpdateAvailabledListener) => Promise & PluginListenerHandle +``` + +Listen for availbale update event, usefull when you want to force check every time the app is launched + +| Param | Type | +| ------------------ | ----------------------------------------------------------------------------- | +| **`eventName`** | 'updateAvailable' | +| **`listenerFunc`** | UpdateAvailabledListener | + +**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle + +**Since:** 4.0.0 + +-------------------- + + ### addListener('downloadComplete', ...) ```typescript @@ -355,7 +452,7 @@ Listen for Major update event in the App, let you know when major update is bloc addListener(eventName: 'updateFailed', listenerFunc: UpdateFailedListener) => Promise & PluginListenerHandle ``` -Listen for update event in the App, let you know when update is ready to install at next app start +Listen for update fail event in the App, let you know when update has fail to install at next app start | Param | Type | | ------------------ | --------------------------------------------------------------------- | @@ -369,10 +466,30 @@ Listen for update event in the App, let you know when update is ready to install -------------------- -### getId() +### addListener('downloadFailed', ...) + +```typescript +addListener(eventName: 'downloadFailed', listenerFunc: DownloadFailedListener) => Promise & PluginListenerHandle +``` + +Listen for download fail event in the App, let you know when download has fail finished + +| Param | Type | +| ------------------ | ------------------------------------------------------------------------- | +| **`eventName`** | 'downloadFailed' | +| **`listenerFunc`** | DownloadFailedListener | + +**Returns:** Promise<PluginListenerHandle> & PluginListenerHandle + +**Since:** 4.0.0 + +-------------------- + + +### getDeviceId() ```typescript -getId() => Promise<{ id: string; }> +getDeviceId() => Promise<{ id: string; }> ``` Get unique ID used to identify device (sent to auto update server) @@ -443,9 +560,29 @@ removeAllListeners() => Promise | **`id`** | string | | **`version`** | string | | **`downloaded`** | string | +| **`checksum`** | string | | **`status`** | BundleStatus | +#### DelayCondition + +| Prop | Type | Description | +| ----------- | --------------------------------------------------------- | ---------------------------------------- | +| **`kind`** | DelayUntilNext | Set up delay conditions in setMultiDelay | +| **`value`** | string | | + + +#### latestVersion + +| Prop | Type | Description | Since | +| ------------- | -------------------- | ----------------------- | ----- | +| **`version`** | string | Res of getLatest method | 4.0.0 | +| **`major`** | boolean | | | +| **`message`** | string | | | +| **`old`** | string | | | +| **`url`** | string | | | + + #### PluginListenerHandle | Prop | Type | @@ -461,6 +598,20 @@ removeAllListeners() => Promise | **`bundle`** | BundleInfo | | | +#### noNeedEvent + +| Prop | Type | Description | Since | +| ------------ | ------------------------------------------------- | ---------------------------------------------- | ----- | +| **`bundle`** | BundleInfo | Current status of download, between 0 and 100. | 4.0.0 | + + +#### updateAvailableEvent + +| Prop | Type | Description | Since | +| ------------ | ------------------------------------------------- | ---------------------------------------------- | ----- | +| **`bundle`** | BundleInfo | Current status of download, between 0 and 100. | 4.0.0 | + + #### DownloadCompleteEvent | Prop | Type | Description | Since | @@ -482,6 +633,13 @@ removeAllListeners() => Promise | **`bundle`** | BundleInfo | Emit when a update failed to install. | 4.0.0 | +#### DownloadFailedEvent + +| Prop | Type | Description | Since | +| ------------- | ------------------- | -------------------------- | ----- | +| **`version`** | string | Emit when a download fail. | 4.0.0 | + + ### Type Aliases @@ -490,11 +648,26 @@ removeAllListeners() => Promise 'success' | 'error' | 'pending' | 'downloading' +#### DelayUntilNext + +'background' | 'kill' | 'nativeVersion' | 'date' + + #### DownloadChangeListener (state: DownloadEvent): void +#### NoNeedListener + +(state: noNeedEvent): void + + +#### UpdateAvailabledListener + +(state: updateAvailableEvent): void + + #### DownloadCompleteListener (state: DownloadCompleteEvent): void @@ -509,6 +682,11 @@ removeAllListeners() => Promise (state: UpdateFailedEvent): void + +#### DownloadFailedListener + +(state: DownloadFailedEvent): void + ### Listen to download events diff --git a/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java b/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java index 19069c59..73f10367 100644 --- a/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +++ b/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java @@ -39,8 +39,8 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi private static final String updateUrlDefault = "https://api.capgo.app/updates"; private static final String statsUrlDefault = "https://api.capgo.app/stats"; - private static final String DELAY_UPDATE = "delayUpdate"; - private static final String DELAY_UPDATE_VAL = "delayUpdateVal"; + + private static final String DELAY_CONDITION_PREFERENCES = ""; private SharedPreferences.Editor editor; private SharedPreferences prefs; @@ -53,6 +53,8 @@ public class CapacitorUpdaterPlugin extends Plugin implements Application.Activi private String updateUrl = ""; private Version currentVersionNative; private Boolean resetWhenUpdate = true; + private Thread backgroundTask; + private Boolean taskRunning = false; private volatile Thread appReadyCheck; @@ -414,33 +416,60 @@ public void notifyAppReady(final PluginCall call) { } @PluginMethod - public void setDelay(final PluginCall call) { + public void setMultiDelay(final PluginCall call) { try { - final String kind = call.getString("kind"); - final String value = call.getString("value"); - if (kind == null) { - Log.e(CapacitorUpdater.TAG, "setDelay called without kind"); - call.reject("setDelay called without kind"); + final Object delayConditions = call.getData().opt("delayConditions"); + if (delayConditions == null) { + Log.e(CapacitorUpdater.TAG, "setMultiDelay called without delayCondition"); + call.reject("setMultiDelay called without delayCondition"); return; } - this.editor.putString(DELAY_UPDATE, kind); - this.editor.putString(DELAY_UPDATE_VAL, value); + if (_setMultiDelay(delayConditions.toString())) { + call.resolve(); + } else { + call.reject("Failed to delay update"); + } + } catch (final Exception e) { + Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling 'setMultiDelay()']", e); + call.reject("Failed to delay update", e); + } + } + + private Boolean _setMultiDelay(String delayConditions) { + try { + this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions); this.editor.commit(); Log.i(CapacitorUpdater.TAG, "Delay update saved"); - call.resolve(); + return true; + } catch (final Exception e) { + Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling '_setMultiDelay()']", e); + return false; } - catch(final Exception e) { - Log.e(CapacitorUpdater.TAG, "Failed to delay update", e); + } + + @Deprecated + @PluginMethod + public void setDelay(final PluginCall call) { + try { + String kind = call.getString("kind"); + String value = call.getString("value"); + String delayConditions = "[{\"kind\":\"" + kind + "\", \"value\":\"" + (value != null ? value : "") + "\"}]"; + if (_setMultiDelay(delayConditions)) { + call.resolve(); + } else { + call.reject("Failed to delay update"); + } + } catch (final Exception e) { + Log.e(CapacitorUpdater.TAG, "Failed to delay update, [Error calling 'setDelay()']", e); call.reject("Failed to delay update", e); } } private boolean _cancelDelay(String source) { try { - this.editor.remove(DELAY_UPDATE); - this.editor.remove(DELAY_UPDATE_VAL); + this.editor.remove(DELAY_CONDITION_PREFERENCES); this.editor.commit(); - Log.i(CapacitorUpdater.TAG, "delay canceled from " + source); + Log.i(CapacitorUpdater.TAG, "All delays canceled from " + source); return true; } catch (final Exception e) { Log.e(CapacitorUpdater.TAG, "Failed to cancel update delay", e); @@ -458,37 +487,56 @@ public void cancelDelay(final PluginCall call) { } private void _checkCancelDelay(Boolean killed) { - final String delayUpdate = this.prefs.getString(DELAY_UPDATE, ""); - if (!"".equals(delayUpdate)) { - if ("background".equals(delayUpdate) && !killed) { - this._cancelDelay("background check"); - } else if ("kill".equals(delayUpdate) && killed) { - this._cancelDelay("kill check"); - } - final String delayVal = this.prefs.getString(DELAY_UPDATE_VAL, ""); - if ("".equals(delayVal)) { - this._cancelDelay("delayVal absent"); - } else if ("date".equals(delayUpdate)) { - try { - final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - Date date = sdf.parse(delayVal); - if (date.compareTo(new Date()) > 0) { - this._cancelDelay("date expired"); - } - } - catch(final Exception e) { - this._cancelDelay("date parsing issue"); - } - - } else if ("nativeVersion".equals(delayUpdate)) { - try { - final Version versionLimit = new Version(delayVal); - if (this.currentVersionNative.isAtLeast(versionLimit)) { - this._cancelDelay("nativeVersion above limit"); - } - } - catch(final Exception e) { - this._cancelDelay("nativeVersion parsing issue"); + Gson gson = new Gson(); + String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]"); + Type type = new TypeToken>() {}.getType(); + ArrayList delayConditionList = gson.fromJson(delayUpdatePreferences, type); + for (DelayCondition condition : delayConditionList) { + String kind = condition.getKind().toString(); + String value = condition.getValue(); + if (!"".equals(kind)) { + switch (kind) { + case "background": + if (!killed) { + this._cancelDelay("background check"); + } + break; + case "kill": + if (killed) { + this._cancelDelay("kill check"); + this.installNext(); + } + break; + case "date": + if (!"".equals(value)) { + try { + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + Date date = sdf.parse(value); + assert date != null; + if (new Date().compareTo(date) > 0) { + this._cancelDelay("date expired"); + } + } catch (final Exception e) { + this._cancelDelay("date parsing issue"); + } + } else { + this._cancelDelay("delayVal absent"); + } + break; + case "nativeVersion": + if (!"".equals(value)) { + try { + final Version versionLimit = new Version(value); + if (this.currentVersionNative.isAtLeast(versionLimit)) { + this._cancelDelay("nativeVersion above limit"); + } + } catch (final Exception e) { + this._cancelDelay("nativeVersion parsing issue"); + } + } else { + this._cancelDelay("delayVal absent"); + } + break; } } } @@ -526,8 +574,7 @@ private boolean isValidURL(String urlStr) { try { URL url = new URL(urlStr); return true; - } - catch (MalformedURLException e) { + } catch (MalformedURLException e) { return false; } } @@ -658,16 +705,62 @@ public void run() { public void onActivityStopped(@NonNull final Activity activity) { Log.i(CapacitorUpdater.TAG, "Checking for pending update"); try { - final String delayUpdate = this.prefs.getString(DELAY_UPDATE, ""); - this._checkCancelDelay(false); - if (!"".equals(delayUpdate)) { + Gson gson = new Gson(); + String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]"); + Type type = new TypeToken>() {}.getType(); + ArrayList delayConditionList = gson.fromJson(delayUpdatePreferences, type); + String backgroundValue = null; + for (DelayCondition delayCondition : delayConditionList) { + if (delayCondition.getKind().toString().equals("background")) { + String value = delayCondition.getValue(); + backgroundValue = (value != null || !value.equals("")) ? value : "0"; + } + } + if (backgroundValue != null) { + taskRunning = true; + final Long timeout = Long.parseLong(backgroundValue); + if(backgroundTask != null) { + backgroundTask.interrupt(); + } + backgroundTask = new Thread( + new Runnable() { + @Override + public void run() { + try { + Thread.sleep(timeout); + taskRunning = false; + _checkCancelDelay(false); + installNext(); + } catch (InterruptedException e) { + Log.i(CapacitorUpdater.TAG, "Background Task canceled, Activity resumed before timer completes"); + } + } + } + ); + backgroundTask.start(); + } else { + this._checkCancelDelay(false); + this.installNext(); + } + } catch (final Exception e) { + Log.e(CapacitorUpdater.TAG, "Error during onActivityStopped", e); + } + } + + private void installNext() { + try { + Gson gson = new Gson(); + String delayUpdatePreferences = prefs.getString(DELAY_CONDITION_PREFERENCES, "[]"); + Type type = new TypeToken>() {}.getType(); + ArrayList delayConditionList = gson.fromJson(delayUpdatePreferences, type); + if (delayConditionList != null && delayConditionList.size() != 0) { Log.i(CapacitorUpdater.TAG, "Update delayed to next backgrounding"); return; } final BundleInfo current = this.implementation.getCurrentBundle(); final BundleInfo next = this.implementation.getNextBundle(); - if (next != null && !next.isErrorStatus() && next.getId() != current.getId()) { + if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) { // There is a next bundle waiting for activation Log.d(CapacitorUpdater.TAG, "Next bundle is: " + next.getVersionName()); if (this.implementation.set(next) && this._reload()) { @@ -735,10 +828,11 @@ public void run() { } } - // not use but necessary here to remove warnings @Override public void onActivityResumed(@NonNull final Activity activity) { - // TODO: Implement background updating based on `backgroundUpdate` and `backgroundUpdateDelay` capacitor.config.ts settings + if (backgroundTask != null && taskRunning) { + backgroundTask.interrupt(); + } } @Override diff --git a/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java b/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java new file mode 100644 index 00000000..71b992c4 --- /dev/null +++ b/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java @@ -0,0 +1,48 @@ +package ee.forgr.capacitor_updater; + +import java.util.Objects; + +public class DelayCondition { + + private DelayUntilNext kind; + private String value; + + public DelayCondition(DelayUntilNext kind, String value) { + this.kind = kind; + this.value = value; + } + + public DelayUntilNext getKind() { + return kind; + } + + public void setKind(DelayUntilNext kind) { + this.kind = kind; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DelayCondition)) return false; + DelayCondition that = (DelayCondition) o; + return getKind() == that.getKind() && Objects.equals(getValue(), that.getValue()); + } + + @Override + public int hashCode() { + return Objects.hash(getKind(), getValue()); + } + + @Override + public String toString() { + return "DelayCondition{" + "kind=" + kind + ", value='" + value + '\'' + '}'; + } +} diff --git a/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java b/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java new file mode 100644 index 00000000..2365831f --- /dev/null +++ b/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java @@ -0,0 +1,8 @@ +package ee.forgr.capacitor_updater; + +public enum DelayUntilNext { + background, + kill, + nativeVersion, + date +} diff --git a/ios/Plugin.xcodeproj/project.pbxproj b/ios/Plugin.xcodeproj/project.pbxproj index 8e5719d8..b6a75c42 100644 --- a/ios/Plugin.xcodeproj/project.pbxproj +++ b/ios/Plugin.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 50E1A94820377CB70090CE1A /* CapacitorUpdaterPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E1A94720377CB70090CE1A /* CapacitorUpdaterPlugin.swift */; }; 863289042823450E0057FC90 /* BundleInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 863289032823450E0057FC90 /* BundleInfo.swift */; }; 863289062823453C0057FC90 /* BundleStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 863289052823453C0057FC90 /* BundleStatus.swift */; }; + B3F3EE2328CF27FA00663217 /* DelayCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F3EE2228CF27FA00663217 /* DelayCondition.swift */; }; + B3F3EE2528CF287200663217 /* DelayUntilNext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F3EE2428CF287200663217 /* DelayUntilNext.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -47,6 +49,8 @@ 863289052823453C0057FC90 /* BundleStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleStatus.swift; sourceTree = ""; }; 91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.release.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig"; sourceTree = ""; }; 96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig"; sourceTree = ""; }; + B3F3EE2228CF27FA00663217 /* DelayCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayCondition.swift; sourceTree = ""; }; + B3F3EE2428CF287200663217 /* DelayUntilNext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayUntilNext.swift; sourceTree = ""; }; F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig"; sourceTree = ""; }; F6753A823D3815DB436415E3 /* Pods_PluginTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PluginTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -103,6 +107,8 @@ 50ADFF8B201F53D600D50D53 /* CapacitorUpdaterPlugin.h */, 50ADFFA72020EE4F00D50D53 /* CapacitorUpdaterPlugin.m */, 50ADFF8C201F53D600D50D53 /* Info.plist */, + B3F3EE2228CF27FA00663217 /* DelayCondition.swift */, + B3F3EE2428CF287200663217 /* DelayUntilNext.swift */, ); path = Plugin; sourceTree = ""; @@ -313,6 +319,8 @@ buildActionMask = 2147483647; files = ( 50E1A94820377CB70090CE1A /* CapacitorUpdaterPlugin.swift in Sources */, + B3F3EE2328CF27FA00663217 /* DelayCondition.swift in Sources */, + B3F3EE2528CF287200663217 /* DelayUntilNext.swift in Sources */, 863289062823453C0057FC90 /* BundleStatus.swift in Sources */, 2F98D68224C9AAE500613A4C /* CapacitorUpdater.swift in Sources */, 863289042823450E0057FC90 /* BundleInfo.swift in Sources */, diff --git a/ios/Plugin/CapacitorUpdaterPlugin.m b/ios/Plugin/CapacitorUpdaterPlugin.m index 398e85c7..9ab318e8 100644 --- a/ios/Plugin/CapacitorUpdaterPlugin.m +++ b/ios/Plugin/CapacitorUpdaterPlugin.m @@ -13,6 +13,7 @@ CAP_PLUGIN_METHOD(reload, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(notifyAppReady, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setDelay, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(setMultiDelay, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(cancelDelay, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLatest, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getDeviceId, CAPPluginReturnPromise); diff --git a/ios/Plugin/CapacitorUpdaterPlugin.swift b/ios/Plugin/CapacitorUpdaterPlugin.swift index 0cdc00cf..b4a4084b 100644 --- a/ios/Plugin/CapacitorUpdaterPlugin.swift +++ b/ios/Plugin/CapacitorUpdaterPlugin.swift @@ -11,8 +11,7 @@ public class CapacitorUpdaterPlugin: CAPPlugin { private var implementation = CapacitorUpdater() static let updateUrlDefault = "https://api.capgo.app/updates" static let statsUrlDefault = "https://api.capgo.app/stats" - let DELAY_UPDATE = "delayUpdate" - let DELAY_UPDATE_VAL = "delayUpdateVal" + let DELAY_CONDITION_PREFERENCES = ""; private var updateUrl = "" private var statsUrl = "" private var currentVersionNative: Version = "0.0.0" @@ -22,6 +21,8 @@ public class CapacitorUpdaterPlugin: CAPPlugin { private var resetWhenUpdate = true private var autoDeleteFailed = false private var autoDeletePrevious = false + private var backgroundWork: DispatchWorkItem? + private var taskRunning = false; override public func load() { print("\(self.implementation.TAG) init for device \(self.implementation.deviceID)") @@ -241,24 +242,47 @@ public class CapacitorUpdaterPlugin: CAPPlugin { call.resolve() } + @objc func setMultiDelay(_ call: CAPPluginCall) { + guard let delayConditionList = call.getValue("delayConditions") else { + print("\(self.implementation.TAG) setMultiDelay called without delayCondition"); + call.reject("setMultiDelay called without delayCondition"); + return; + } + let delayConditions: String = toJson(object: delayConditionList) + if(_setMultiDelay(delayConditions: delayConditions)){ + call.resolve() + }else{ + call.reject("Failed to delay update") + } + } + + @available(*, deprecated, message: "use SetMultiDelay instead") @objc func setDelay(_ call: CAPPluginCall) { - guard let kind = call.getString("kind") else { - print("\(self.implementation.TAG) setDelay called without kind") - call.reject("setDelay called without kind") - return + let kind: String = call.getString("kind", "") + let value: String? = call.getString("value", "") + let delayConditions: String = "[{\"kind\":\"\(kind)\", \"value\":\"\(value ?? "")\"}]" + if(_setMultiDelay(delayConditions: delayConditions)){ + call.resolve() + }else{ + call.reject("Failed to delay update") + } + } + + private func _setMultiDelay(delayConditions: String?) -> Bool{ + if(delayConditions != nil && "" != delayConditions) { + UserDefaults.standard.set(delayConditions, forKey: DELAY_CONDITION_PREFERENCES) + UserDefaults.standard.synchronize() + print("\(self.implementation.TAG) Delay update saved.") + return true + } else { + print("\(self.implementation.TAG) Failed to delay update, [Error calling '_setMultiDelay()']") + return false } - let val = call.getString("value") ?? "" - UserDefaults.standard.set(kind, forKey: DELAY_UPDATE) - UserDefaults.standard.set(val, forKey: DELAY_UPDATE_VAL) - UserDefaults.standard.synchronize() - print("\(self.implementation.TAG) Delay update saved.", kind, val) - call.resolve() } private func _cancelDelay(source: String) -> Void { print("\(self.implementation.TAG) delay Canceled from \(source)") - UserDefaults.standard.removeObject(forKey: DELAY_UPDATE) - UserDefaults.standard.removeObject(forKey: DELAY_UPDATE_VAL) + UserDefaults.standard.removeObject(forKey: DELAY_CONDITION_PREFERENCES) UserDefaults.standard.synchronize() } @@ -268,39 +292,65 @@ public class CapacitorUpdaterPlugin: CAPPlugin { } private func _checkCancelDelay(killed: Bool) -> Void { - let delayUpdate = UserDefaults.standard.string(forKey: DELAY_UPDATE) - if (delayUpdate != nil) { - if (delayUpdate == "background" && !killed) { - self._cancelDelay(source: "background check") - } else if (delayUpdate == "kill" && killed) { - self._cancelDelay(source: "kill check") - } - guard let delayVal = UserDefaults.standard.string(forKey: DELAY_UPDATE_VAL) else { - self._cancelDelay(source: "delayVal absent") - return - } - if (delayUpdate == "date") { - let dateFormatter = ISO8601DateFormatter() - guard let ExpireDate = dateFormatter.date(from: delayVal) else { - self._cancelDelay(source: "date parsing issue") - return - } - if (ExpireDate < Date()) { - self._cancelDelay(source: "date expired") - } - } else if (delayUpdate == "nativeVersion") { - do { - let versionLimit = try Version(delayVal) - if (self.currentVersionNative >= versionLimit) { - self._cancelDelay(source: "nativeVersion above limit") + let delayUpdatePreferences = UserDefaults.standard.string(forKey: DELAY_CONDITION_PREFERENCES) ?? "[]" + let delayConditionList:[DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in + let kind: String = obj.value(forKey: "kind") as! String + let value: String? = obj.value(forKey: "value") as? String + return DelayCondition(kind: kind, value: value) + } + for condition in delayConditionList { + let kind: String? = condition.getKind() + let value: String? = condition.getValue() + if(kind != nil){ + switch(kind){ + case "background": + if (!killed) { + self._cancelDelay(source: "background check") + } + break; + case "kill": + if (killed) { + self._cancelDelay(source: "kill check") + // instant install for kill action + self.installNext() } - } catch { - self._cancelDelay(source: "nativeVersion parsing issue") + break; + case "date": + if(value != nil && value != ""){ + let dateFormatter = ISO8601DateFormatter() + guard let ExpireDate = dateFormatter.date(from: value!) else { + self._cancelDelay(source: "date parsing issue") + return + } + if (ExpireDate < Date()) { + self._cancelDelay(source: "date expired") + } + }else { + self._cancelDelay(source: "delayVal absent"); + } + break; + case "nativeVersion": + if(value != nil && value != ""){ + do { + let versionLimit = try Version(value!) + if (self.currentVersionNative >= versionLimit) { + self._cancelDelay(source: "nativeVersion above limit") + } + } catch { + self._cancelDelay(source: "nativeVersion parsing issue") + } + }else { + self._cancelDelay(source: "delayVal absent"); + } + break; + case .none: + print("\(self.implementation.TAG) _checkCancelDelay switch case none error") + case .some(_): + print("\(self.implementation.TAG) _checkCancelDelay switch case some error") } } } - - self.checkAppReady() + // self.checkAppReady() why this here? } private func _isAutoUpdateEnabled() -> Bool { @@ -361,6 +411,10 @@ public class CapacitorUpdaterPlugin: CAPPlugin { } @objc func appMovedToForeground() { + if (backgroundWork != nil && taskRunning) { + backgroundWork!.cancel(); + print("\(self.implementation.TAG) Background Timer Task canceled, Activity resumed before timer completes"); + } if (self._isAutoUpdateEnabled()) { DispatchQueue.global(qos: .background).async { print("\(self.implementation.TAG) Check for update via \(self.updateUrl)") @@ -420,13 +474,51 @@ public class CapacitorUpdaterPlugin: CAPPlugin { @objc func appMovedToBackground() { print("\(self.implementation.TAG) Check for pending update") - let delayUpdate = UserDefaults.standard.string(forKey: DELAY_UPDATE) - self._checkCancelDelay(killed: false) - if (delayUpdate != nil) { + let delayUpdatePreferences = UserDefaults.standard.string(forKey: DELAY_CONDITION_PREFERENCES) ?? "[]" + + let delayConditionList:[DelayCondition] = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in + let kind: String = obj.value(forKey: "kind") as! String + let value: String? = obj.value(forKey: "value") as? String + return DelayCondition(kind: kind, value: value) + } + var backgroundValue: String? + for delayCondition in delayConditionList { + if(delayCondition.getKind() == "background") { + let value: String? = delayCondition.getValue() + backgroundValue = (value != nil && value != "") ? value! : "0" + } + } + if(backgroundValue != nil){ + self.taskRunning = true + let interval: Double = (Double(backgroundValue!) ?? 0.0) / 1000 + self.backgroundWork?.cancel() + self.backgroundWork = DispatchWorkItem(block: { + // IOS never executes this task in background + self.taskRunning = false + self._checkCancelDelay(killed: false) + self.installNext() + }) + DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + interval, execute: self.backgroundWork!) + }else{ + self._checkCancelDelay(killed: false); + self.installNext() + } + + + + } + + private func installNext(){ + let delayUpdatePreferences = UserDefaults.standard.string(forKey: DELAY_CONDITION_PREFERENCES) ?? "[]" + let delayConditionList:[DelayCondition]? = fromJsonArr(json: delayUpdatePreferences).map { obj -> DelayCondition in + let kind: String = obj.value(forKey: "kind") as! String + let value: String? = obj.value(forKey: "value") as? String + return DelayCondition(kind: kind, value: value) + } + if (delayConditionList != nil && delayConditionList?.capacity != 0) { print("\(self.implementation.TAG) Update delayed to next backgrounding") return } - let current: BundleInfo = self.implementation.getCurrentBundle() let next: BundleInfo? = self.implementation.getNextBundle() @@ -440,4 +532,20 @@ public class CapacitorUpdaterPlugin: CAPPlugin { } } } + + @objc private func toJson(object:Any) -> String { + guard let data = try? JSONSerialization.data(withJSONObject: object, options: []) else { + return "" + } + return String(data: data, encoding: String.Encoding.utf8) ?? "" + } + + @objc private func fromJsonArr(json:String) -> [NSObject] { + let jsonData = json.data(using: .utf8)! + let object = try? JSONSerialization.jsonObject( + with: jsonData, + options: .mutableContainers + ) as? [NSObject] + return object ?? [] + } } diff --git a/ios/Plugin/DelayCondition.swift b/ios/Plugin/DelayCondition.swift new file mode 100644 index 00000000..7e571826 --- /dev/null +++ b/ios/Plugin/DelayCondition.swift @@ -0,0 +1,68 @@ +// +// DelayCondition.swift +// Plugin +// +// Created by Luca Peruzzo on 12/09/22. +// Copyright © 2022 Capgo. All rights reserved. +// + +import Foundation + +private func delayUntilNextValue(value: String) -> DelayUntilNext { + switch(value) { + case "background": return .background + case "kill": return .kill + case "nativeVersion": return .nativeVersion + case "date": return .date + default: + return .background + } +} + +@objc public class DelayCondition: NSObject, Decodable, Encodable { + private let kind: DelayUntilNext; + private let value: String?; + + convenience init(kind: String, value: String?) { + self.init(kind: delayUntilNextValue(value: kind), value: value) + } + + init(kind: DelayUntilNext, value: String?) { + self.kind = kind + self.value = value + } + + public required init(from decoder:Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + kind = try values.decode(DelayUntilNext.self, forKey: .kind) + value = try values.decode(String.self, forKey: .value) + } + + enum CodingKeys: String, CodingKey { + case kind, value + } + + public func getKind() -> String{ + return self.kind.description + } + + public func getValue() -> String?{ + return self.value + } + + public func toJSON() -> [String: String] { + return [ + "kind": self.getKind(), + "value": self.getValue() ?? "", + ] + } + + public static func == (lhs: DelayCondition, rhs: DelayCondition) -> Bool { + return lhs.getKind() == rhs.getKind() && lhs.getValue() == rhs.getValue() + } + + public func toString() -> String { + return "{ \"kind\": \"\(self.getKind())\", \"value\": \"\(self.getValue() ?? "")\"}" + } + +} diff --git a/ios/Plugin/DelayUntilNext.swift b/ios/Plugin/DelayUntilNext.swift new file mode 100644 index 00000000..a4892fa9 --- /dev/null +++ b/ios/Plugin/DelayUntilNext.swift @@ -0,0 +1,24 @@ +// +// DelayUntilNext.swift +// Plugin +// +// Created by Luca Peruzzo on 12/09/22. +// Copyright © 2022 Capgo. All rights reserved. +// + +import Foundation +enum DelayUntilNext: Decodable, Encodable, CustomStringConvertible { + case background + case kill + case nativeVersion + case date + + var description: String{ + switch self { + case .background: return "background" + case .kill: return "kill" + case .nativeVersion: return "nativeVersion" + case .date: return "date" + } + } +} diff --git a/src/definitions.ts b/src/definitions.ts index 5c5f8743..4a724729 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -161,6 +161,15 @@ export interface BundleInfo { status: BundleStatus; } +export interface DelayCondition { + /** + * Set up delay conditions in setMultiDelay + * @param value is useless for @param kind "kill", optional for "background" (default value: "0") and required for "nativeVersion" and "date" + */ + kind: DelayUntilNext; + value?: string; +} + export type BundleStatus = 'success' | 'error' | 'pending' | 'downloading'; export type DelayUntilNext = 'background' | 'kill' | 'nativeVersion' | 'date'; @@ -252,15 +261,38 @@ export interface CapacitorUpdaterPlugin { reload(): Promise; /** - * Set delay to skip updates in the next time the app goes into the background + * Set DelayCondition, skip updates until one of the conditions is met + * + * @returns {Promise} an Promise resolved directly + * @param options are the {@link DelayCondition} list to set + * + * @example + * setMultiDelay({ delayConditions: [{ kind: 'kill' }, { kind: 'background', value: '300000' }] }) + * // installs the update after the user kills the app or after a background of 300000 ms (5 minutes) + * + * @example + * setMultiDelay({ delayConditions: [{ kind: 'date', value: '2022-09-14T06:14:11.920Z' }] }) + * // installs the update after the specific iso8601 date is expired + * + * @example + * setMultiDelay({ delayConditions: [{ kind: 'background' }] }) + * // installs the update after the the first background (default behaviour without setting delay) + * + * @throws An error if the something went wrong + * @since 4.3.0 + */ + setMultiDelay(options: { delayConditions: DelayCondition[] }): Promise; + + /** + * Set DelayCondition, skip updates until the condition is met * + * @deprecated use setMultiDelay instead passing a single value in array * @returns {Promise} an Promise resolved directly - * @param kind is the kind of delay to set - * @param value is the delay value acording to the type + * @param options is the {@link DelayCondition} to set * @throws An error if the something went wrong * @since 4.0.0 */ - setDelay(options: {kind: DelayUntilNext, value?: string}): Promise; + setDelay(options: DelayCondition): Promise; /** * Cancel delay to updates as usual diff --git a/src/web.ts b/src/web.ts index 5a06cd5a..b67f05b5 100644 --- a/src/web.ts +++ b/src/web.ts @@ -1,6 +1,6 @@ import { WebPlugin } from '@capacitor/core'; -import type { CapacitorUpdaterPlugin, BundleInfo, latestVersion, DelayUntilNext } from './definitions'; +import type { CapacitorUpdaterPlugin, BundleInfo, latestVersion, DelayCondition } from './definitions'; const BUNDLE_BUILTIN: BundleInfo = { status: 'success', @@ -65,8 +65,12 @@ export class CapacitorUpdaterWeb extends WebPlugin implements CapacitorUpdaterPl console.warn('Cannot notify App Ready in web'); return BUNDLE_BUILTIN; } - async setDelay(options: { kind: DelayUntilNext, value: string }): Promise { - console.warn('Cannot setDelay in web', options); + async setMultiDelay(options: { delayConditions: DelayCondition[] }): Promise { + console.warn('Cannot setMultiDelay in web', options?.delayConditions); + return; + } + async setDelay(option: DelayCondition): Promise { + console.warn('Cannot setDelay in web', option); return; } async cancelDelay(): Promise {