@capgo/capacitor-updater 4.41.0 → 4.43.5
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.
- package/CapgoCapacitorUpdater.podspec +7 -5
- package/Package.swift +40 -0
- package/README.md +1913 -303
- package/android/build.gradle +41 -8
- package/android/proguard-rules.pro +45 -0
- package/android/src/main/AndroidManifest.xml +1 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/AppLifecycleObserver.java +88 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +223 -195
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +23 -23
- package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +13 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +2720 -1242
- package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1854 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +359 -121
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +28 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +44 -49
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +296 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +215 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +858 -117
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +156 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +45 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +360 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +603 -0
- package/dist/docs.json +3022 -765
- package/dist/esm/definitions.d.ts +1717 -198
- package/dist/esm/definitions.js +103 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/history.d.ts +1 -0
- package/dist/esm/history.js +283 -0
- package/dist/esm/history.js.map +1 -0
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +5 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +43 -42
- package/dist/esm/web.js +122 -37
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +512 -37
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +512 -37
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +87 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
- package/ios/Sources/CapacitorUpdaterPlugin/BundleInfo.swift +177 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +12 -12
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +2020 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1959 -0
- package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +313 -0
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +257 -0
- package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
- package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +392 -0
- package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
- package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +441 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +1 -2
- package/package.json +49 -41
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +0 -1131
- package/ios/Plugin/BundleInfo.swift +0 -113
- package/ios/Plugin/CapacitorUpdater.swift +0 -850
- package/ios/Plugin/CapacitorUpdaterPlugin.h +0 -10
- package/ios/Plugin/CapacitorUpdaterPlugin.m +0 -27
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -678
- package/ios/Plugin/CryptoCipher.swift +0 -240
- /package/{LICENCE → LICENSE} +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
|
@@ -7,1298 +7,2776 @@
|
|
|
7
7
|
package ee.forgr.capacitor_updater;
|
|
8
8
|
|
|
9
9
|
import android.app.Activity;
|
|
10
|
-
import android.app.ActivityManager;
|
|
11
|
-
import android.app.Application;
|
|
12
10
|
import android.content.Context;
|
|
11
|
+
import android.content.Intent;
|
|
13
12
|
import android.content.SharedPreferences;
|
|
14
13
|
import android.content.pm.PackageInfo;
|
|
15
14
|
import android.content.pm.PackageManager;
|
|
15
|
+
import android.graphics.Color;
|
|
16
|
+
import android.net.Uri;
|
|
16
17
|
import android.os.Build;
|
|
17
|
-
import android.os.
|
|
18
|
-
import android.
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
18
|
+
import android.os.Handler;
|
|
19
|
+
import android.os.Looper;
|
|
20
|
+
import android.view.Gravity;
|
|
21
|
+
import android.view.View;
|
|
22
|
+
import android.view.ViewGroup;
|
|
23
|
+
import android.widget.FrameLayout;
|
|
24
|
+
import android.widget.ProgressBar;
|
|
22
25
|
import com.getcapacitor.CapConfig;
|
|
23
26
|
import com.getcapacitor.JSArray;
|
|
24
27
|
import com.getcapacitor.JSObject;
|
|
25
28
|
import com.getcapacitor.Plugin;
|
|
26
29
|
import com.getcapacitor.PluginCall;
|
|
30
|
+
import com.getcapacitor.PluginHandle;
|
|
27
31
|
import com.getcapacitor.PluginMethod;
|
|
28
32
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
29
33
|
import com.getcapacitor.plugin.WebView;
|
|
30
|
-
import com.google.
|
|
31
|
-
|
|
34
|
+
import com.google.android.gms.tasks.Task;
|
|
35
|
+
// Play Store In-App Updates
|
|
36
|
+
import com.google.android.play.core.appupdate.AppUpdateInfo;
|
|
37
|
+
import com.google.android.play.core.appupdate.AppUpdateManager;
|
|
38
|
+
import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
|
|
39
|
+
import com.google.android.play.core.appupdate.AppUpdateOptions;
|
|
40
|
+
import com.google.android.play.core.install.InstallState;
|
|
41
|
+
import com.google.android.play.core.install.InstallStateUpdatedListener;
|
|
42
|
+
import com.google.android.play.core.install.model.AppUpdateType;
|
|
43
|
+
import com.google.android.play.core.install.model.InstallStatus;
|
|
44
|
+
import com.google.android.play.core.install.model.UpdateAvailability;
|
|
32
45
|
import io.github.g00fy2.versioncompare.Version;
|
|
33
46
|
import java.io.IOException;
|
|
34
|
-
import java.lang.reflect.Type;
|
|
35
47
|
import java.net.MalformedURLException;
|
|
36
48
|
import java.net.URL;
|
|
37
|
-
import java.text.SimpleDateFormat;
|
|
38
49
|
import java.util.ArrayList;
|
|
50
|
+
import java.util.Arrays;
|
|
39
51
|
import java.util.Date;
|
|
40
|
-
import java.util.
|
|
52
|
+
import java.util.HashSet;
|
|
41
53
|
import java.util.List;
|
|
54
|
+
import java.util.Map;
|
|
55
|
+
import java.util.Objects;
|
|
56
|
+
import java.util.Set;
|
|
57
|
+
import java.util.Timer;
|
|
58
|
+
import java.util.TimerTask;
|
|
42
59
|
import java.util.UUID;
|
|
60
|
+
import java.util.concurrent.Phaser;
|
|
61
|
+
import java.util.concurrent.Semaphore;
|
|
62
|
+
import java.util.concurrent.TimeUnit;
|
|
63
|
+
import java.util.concurrent.TimeoutException;
|
|
64
|
+
import java.util.concurrent.atomic.AtomicReference;
|
|
65
|
+
// Removed OkHttpClient and Protocol imports - using shared client in DownloadService instead
|
|
66
|
+
import org.json.JSONArray;
|
|
43
67
|
import org.json.JSONException;
|
|
68
|
+
import org.json.JSONObject;
|
|
44
69
|
|
|
45
70
|
@CapacitorPlugin(name = "CapacitorUpdater")
|
|
46
|
-
public class CapacitorUpdaterPlugin
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"https://
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"New native major version detected: " + this.currentVersionNative
|
|
177
|
-
);
|
|
178
|
-
this.implementation.reset(true);
|
|
179
|
-
final List<BundleInfo> installed = this.implementation.list();
|
|
180
|
-
for (final BundleInfo bundle : installed) {
|
|
71
|
+
public class CapacitorUpdaterPlugin extends Plugin {
|
|
72
|
+
|
|
73
|
+
private Logger logger;
|
|
74
|
+
|
|
75
|
+
private static final String updateUrlDefault = "https://plugin.capgo.app/updates";
|
|
76
|
+
private static final String statsUrlDefault = "https://plugin.capgo.app/stats";
|
|
77
|
+
private static final String channelUrlDefault = "https://plugin.capgo.app/channel_self";
|
|
78
|
+
private static final String KEEP_URL_FLAG_KEY = "__capgo_keep_url_path_after_reload";
|
|
79
|
+
private static final String CUSTOM_ID_PREF_KEY = "CapacitorUpdater.customId";
|
|
80
|
+
private static final String UPDATE_URL_PREF_KEY = "CapacitorUpdater.updateUrl";
|
|
81
|
+
private static final String STATS_URL_PREF_KEY = "CapacitorUpdater.statsUrl";
|
|
82
|
+
private static final String CHANNEL_URL_PREF_KEY = "CapacitorUpdater.channelUrl";
|
|
83
|
+
private static final String DEFAULT_CHANNEL_PREF_KEY = "CapacitorUpdater.defaultChannel";
|
|
84
|
+
private static final String[] BREAKING_EVENT_NAMES = { "breakingAvailable", "majorAvailable" };
|
|
85
|
+
private static final String LAST_FAILED_BUNDLE_PREF_KEY = "CapacitorUpdater.lastFailedBundle";
|
|
86
|
+
|
|
87
|
+
private final String pluginVersion = "4.43.5";
|
|
88
|
+
private static final String DELAY_CONDITION_PREFERENCES = "";
|
|
89
|
+
|
|
90
|
+
private SharedPreferences.Editor editor;
|
|
91
|
+
private SharedPreferences prefs;
|
|
92
|
+
protected CapgoUpdater implementation;
|
|
93
|
+
private Boolean persistCustomId = false;
|
|
94
|
+
private Boolean persistModifyUrl = false;
|
|
95
|
+
|
|
96
|
+
private Integer appReadyTimeout = 10000;
|
|
97
|
+
private Integer periodCheckDelay = 0;
|
|
98
|
+
private Boolean autoDeleteFailed = true;
|
|
99
|
+
private Boolean autoDeletePrevious = true;
|
|
100
|
+
private Boolean autoUpdate = false;
|
|
101
|
+
private String updateUrl = "";
|
|
102
|
+
private Version currentVersionNative;
|
|
103
|
+
private String currentBuildVersion;
|
|
104
|
+
private Thread backgroundTask;
|
|
105
|
+
private Boolean taskRunning = false;
|
|
106
|
+
private Boolean keepUrlPathAfterReload = false;
|
|
107
|
+
private Boolean autoSplashscreen = false;
|
|
108
|
+
private Boolean autoSplashscreenLoader = false;
|
|
109
|
+
private Integer autoSplashscreenTimeout = 10000;
|
|
110
|
+
private Boolean autoSplashscreenTimedOut = false;
|
|
111
|
+
private String directUpdateMode = "false";
|
|
112
|
+
private Boolean wasRecentlyInstalledOrUpdated = false;
|
|
113
|
+
private Boolean onLaunchDirectUpdateUsed = false;
|
|
114
|
+
Boolean shakeMenuEnabled = false;
|
|
115
|
+
Boolean shakeChannelSelectorEnabled = false;
|
|
116
|
+
private Boolean allowManualBundleError = false;
|
|
117
|
+
Boolean allowSetDefaultChannel = true;
|
|
118
|
+
|
|
119
|
+
String getUpdateUrl() {
|
|
120
|
+
return this.updateUrl;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Used for activity-based foreground/background detection on Android < 14
|
|
124
|
+
private Boolean isPreviousMainActivity = true;
|
|
125
|
+
|
|
126
|
+
private volatile Thread backgroundDownloadTask;
|
|
127
|
+
private volatile Thread appReadyCheck;
|
|
128
|
+
private volatile long downloadStartTimeMs = 0;
|
|
129
|
+
private static final long DOWNLOAD_TIMEOUT_MS = 3600000; // 1 hour timeout
|
|
130
|
+
|
|
131
|
+
// private static final CountDownLatch semaphoreReady = new CountDownLatch(1);
|
|
132
|
+
private static final Phaser semaphoreReady = new Phaser(1);
|
|
133
|
+
|
|
134
|
+
// Lock to ensure cleanup completes before downloads start
|
|
135
|
+
private final Object cleanupLock = new Object();
|
|
136
|
+
private volatile boolean cleanupComplete = false;
|
|
137
|
+
private volatile Thread cleanupThread = null;
|
|
138
|
+
|
|
139
|
+
private int lastNotifiedStatPercent = 0;
|
|
140
|
+
|
|
141
|
+
private DelayUpdateUtils delayUpdateUtils;
|
|
142
|
+
|
|
143
|
+
private ShakeMenu shakeMenu;
|
|
144
|
+
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
145
|
+
private FrameLayout splashscreenLoaderOverlay;
|
|
146
|
+
private Runnable splashscreenTimeoutRunnable;
|
|
147
|
+
|
|
148
|
+
// App lifecycle observer using ProcessLifecycleOwner for reliable foreground/background detection
|
|
149
|
+
private AppLifecycleObserver appLifecycleObserver;
|
|
150
|
+
|
|
151
|
+
// Play Store In-App Updates
|
|
152
|
+
private AppUpdateManager appUpdateManager;
|
|
153
|
+
private AppUpdateInfo cachedAppUpdateInfo;
|
|
154
|
+
private static final int APP_UPDATE_REQUEST_CODE = 9001;
|
|
155
|
+
private InstallStateUpdatedListener installStateUpdatedListener;
|
|
156
|
+
|
|
157
|
+
private void notifyBreakingEvents(final String version) {
|
|
158
|
+
if (version == null || version.isEmpty()) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (final String eventName : BREAKING_EVENT_NAMES) {
|
|
162
|
+
final JSObject payload = new JSObject();
|
|
163
|
+
payload.put("version", version);
|
|
164
|
+
CapacitorUpdaterPlugin.this.notifyListeners(eventName, payload);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private void persistLastFailedBundle(BundleInfo bundle) {
|
|
169
|
+
if (this.prefs == null) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
final SharedPreferences.Editor localEditor = this.prefs.edit();
|
|
173
|
+
if (bundle == null) {
|
|
174
|
+
localEditor.remove(LAST_FAILED_BUNDLE_PREF_KEY);
|
|
175
|
+
} else {
|
|
176
|
+
final JSONObject json = new JSONObject(bundle.toJSONMap());
|
|
177
|
+
localEditor.putString(LAST_FAILED_BUNDLE_PREF_KEY, json.toString());
|
|
178
|
+
}
|
|
179
|
+
localEditor.apply();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private BundleInfo readLastFailedBundle() {
|
|
183
|
+
if (this.prefs == null) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
final String raw = this.prefs.getString(LAST_FAILED_BUNDLE_PREF_KEY, null);
|
|
187
|
+
if (raw == null || raw.trim().isEmpty()) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
return BundleInfo.fromJSON(raw);
|
|
192
|
+
} catch (final JSONException e) {
|
|
193
|
+
logger.error("Failed to parse failed bundle info: " + e.getMessage());
|
|
194
|
+
this.persistLastFailedBundle(null);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public Thread startNewThread(final Runnable function, Number waitTime) {
|
|
200
|
+
Thread bgTask = new Thread(() -> {
|
|
181
201
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
"Deleting obsolete bundle: " + bundle.getId()
|
|
185
|
-
);
|
|
186
|
-
this.implementation.delete(bundle.getId());
|
|
187
|
-
} catch (final Exception e) {
|
|
188
|
-
Log.e(
|
|
189
|
-
CapacitorUpdater.TAG,
|
|
190
|
-
"Failed to delete: " + bundle.getId(),
|
|
191
|
-
e
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} catch (final Exception e) {
|
|
197
|
-
Log.e(
|
|
198
|
-
CapacitorUpdater.TAG,
|
|
199
|
-
"Could not determine the current version",
|
|
200
|
-
e
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
} catch (final Exception e) {
|
|
204
|
-
Log.e(
|
|
205
|
-
CapacitorUpdater.TAG,
|
|
206
|
-
"Error calculating previous native version",
|
|
207
|
-
e
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
this.editor.putString(
|
|
211
|
-
"LatestVersionNative",
|
|
212
|
-
this.currentVersionNative.toString()
|
|
213
|
-
);
|
|
214
|
-
this.editor.commit();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
public void notifyDownload(final String id, final int percent) {
|
|
218
|
-
try {
|
|
219
|
-
final JSObject ret = new JSObject();
|
|
220
|
-
ret.put("percent", percent);
|
|
221
|
-
final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
|
|
222
|
-
ret.put("bundle", bundleInfo.toJSON());
|
|
223
|
-
this.notifyListeners("download", ret);
|
|
224
|
-
if (percent == 100) {
|
|
225
|
-
this.notifyListeners("downloadComplete", bundleInfo.toJSON());
|
|
226
|
-
this.implementation.sendStats(
|
|
227
|
-
"download_complete",
|
|
228
|
-
bundleInfo.getVersionName()
|
|
229
|
-
);
|
|
230
|
-
} else if (percent % 10 == 0) {
|
|
231
|
-
this.implementation.sendStats(
|
|
232
|
-
"download_" + percent,
|
|
233
|
-
bundleInfo.getVersionName()
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
} catch (final Exception e) {
|
|
237
|
-
Log.e(CapacitorUpdater.TAG, "Could not notify listeners", e);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
@PluginMethod
|
|
242
|
-
public void getDeviceId(final PluginCall call) {
|
|
243
|
-
try {
|
|
244
|
-
final JSObject ret = new JSObject();
|
|
245
|
-
ret.put("deviceId", this.implementation.deviceID);
|
|
246
|
-
call.resolve(ret);
|
|
247
|
-
} catch (final Exception e) {
|
|
248
|
-
Log.e(CapacitorUpdater.TAG, "Could not get device id", e);
|
|
249
|
-
call.reject("Could not get device id", e);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
@PluginMethod
|
|
254
|
-
public void setCustomId(final PluginCall call) {
|
|
255
|
-
final String customId = call.getString("customId");
|
|
256
|
-
if (customId == null) {
|
|
257
|
-
Log.e(CapacitorUpdater.TAG, "setCustomId called without customId");
|
|
258
|
-
call.reject("setCustomId called without customId");
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
this.implementation.customId = customId;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
@PluginMethod
|
|
265
|
-
public void getPluginVersion(final PluginCall call) {
|
|
266
|
-
try {
|
|
267
|
-
final JSObject ret = new JSObject();
|
|
268
|
-
ret.put("version", this.PLUGIN_VERSION);
|
|
269
|
-
call.resolve(ret);
|
|
270
|
-
} catch (final Exception e) {
|
|
271
|
-
Log.e(CapacitorUpdater.TAG, "Could not get plugin version", e);
|
|
272
|
-
call.reject("Could not get plugin version", e);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
@PluginMethod
|
|
277
|
-
public void setChannel(final PluginCall call) {
|
|
278
|
-
final String channel = call.getString("channel");
|
|
279
|
-
if (channel == null) {
|
|
280
|
-
Log.e(CapacitorUpdater.TAG, "setChannel called without channel");
|
|
281
|
-
call.reject("setChannel called without channel");
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
try {
|
|
285
|
-
Log.i(CapacitorUpdater.TAG, "setChannel " + channel);
|
|
286
|
-
new Thread(
|
|
287
|
-
new Runnable() {
|
|
288
|
-
@Override
|
|
289
|
-
public void run() {
|
|
290
|
-
CapacitorUpdaterPlugin.this.implementation.setChannel(
|
|
291
|
-
channel,
|
|
292
|
-
res -> {
|
|
293
|
-
if (res.has("error")) {
|
|
294
|
-
call.reject(res.getString("error"));
|
|
295
|
-
} else {
|
|
296
|
-
call.resolve(res);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
)
|
|
303
|
-
.start();
|
|
304
|
-
} catch (final Exception e) {
|
|
305
|
-
Log.e(CapacitorUpdater.TAG, "Failed to setChannel: " + channel, e);
|
|
306
|
-
call.reject("Failed to setChannel: " + channel, e);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
@PluginMethod
|
|
311
|
-
public void getChannel(final PluginCall call) {
|
|
312
|
-
try {
|
|
313
|
-
Log.i(CapacitorUpdater.TAG, "getChannel");
|
|
314
|
-
new Thread(
|
|
315
|
-
new Runnable() {
|
|
316
|
-
@Override
|
|
317
|
-
public void run() {
|
|
318
|
-
CapacitorUpdaterPlugin.this.implementation.getChannel(res -> {
|
|
319
|
-
if (res.has("error")) {
|
|
320
|
-
call.reject(res.getString("error"));
|
|
321
|
-
} else {
|
|
322
|
-
call.resolve(res);
|
|
202
|
+
if (waitTime.longValue() > 0) {
|
|
203
|
+
Thread.sleep(waitTime.longValue());
|
|
323
204
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
.start();
|
|
329
|
-
} catch (final Exception e) {
|
|
330
|
-
Log.e(CapacitorUpdater.TAG, "Failed to getChannel", e);
|
|
331
|
-
call.reject("Failed to getChannel", e);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
@PluginMethod
|
|
336
|
-
public void download(final PluginCall call) {
|
|
337
|
-
final String url = call.getString("url");
|
|
338
|
-
final String version = call.getString("version");
|
|
339
|
-
final String sessionKey = call.getString("sessionKey", "");
|
|
340
|
-
final String checksum = call.getString("checksum", "");
|
|
341
|
-
if (url == null) {
|
|
342
|
-
Log.e(CapacitorUpdater.TAG, "Download called without url");
|
|
343
|
-
call.reject("Download called without url");
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
if (version == null) {
|
|
347
|
-
Log.e(CapacitorUpdater.TAG, "Download called without version");
|
|
348
|
-
call.reject("Download called without version");
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
try {
|
|
352
|
-
Log.i(CapacitorUpdater.TAG, "Downloading " + url);
|
|
353
|
-
new Thread(
|
|
354
|
-
new Runnable() {
|
|
355
|
-
@Override
|
|
356
|
-
public void run() {
|
|
357
|
-
try {
|
|
358
|
-
final BundleInfo downloaded =
|
|
359
|
-
CapacitorUpdaterPlugin.this.implementation.download(
|
|
360
|
-
url,
|
|
361
|
-
version,
|
|
362
|
-
sessionKey,
|
|
363
|
-
checksum
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
call.resolve(downloaded.toJSON());
|
|
367
|
-
} catch (final IOException e) {
|
|
368
|
-
Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
|
|
369
|
-
call.reject("Failed to download from: " + url, e);
|
|
370
|
-
final JSObject ret = new JSObject();
|
|
371
|
-
ret.put("version", version);
|
|
372
|
-
CapacitorUpdaterPlugin.this.notifyListeners(
|
|
373
|
-
"downloadFailed",
|
|
374
|
-
ret
|
|
375
|
-
);
|
|
376
|
-
final BundleInfo current =
|
|
377
|
-
CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
378
|
-
CapacitorUpdaterPlugin.this.implementation.sendStats(
|
|
379
|
-
"download_fail",
|
|
380
|
-
current.getVersionName()
|
|
381
|
-
);
|
|
205
|
+
function.run();
|
|
206
|
+
} catch (Exception e) {
|
|
207
|
+
e.printStackTrace();
|
|
382
208
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
.start();
|
|
387
|
-
} catch (final Exception e) {
|
|
388
|
-
Log.e(CapacitorUpdater.TAG, "Failed to download from: " + url, e);
|
|
389
|
-
call.reject("Failed to download from: " + url, e);
|
|
390
|
-
final JSObject ret = new JSObject();
|
|
391
|
-
ret.put("version", version);
|
|
392
|
-
CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
|
|
393
|
-
final BundleInfo current =
|
|
394
|
-
CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
395
|
-
CapacitorUpdaterPlugin.this.implementation.sendStats(
|
|
396
|
-
"download_fail",
|
|
397
|
-
current.getVersionName()
|
|
398
|
-
);
|
|
209
|
+
});
|
|
210
|
+
bgTask.start();
|
|
211
|
+
return bgTask;
|
|
399
212
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
@PluginMethod
|
|
431
|
-
public void next(final PluginCall call) {
|
|
432
|
-
final String id = call.getString("id");
|
|
433
|
-
if (id == null) {
|
|
434
|
-
Log.e(CapacitorUpdater.TAG, "Next called without id");
|
|
435
|
-
call.reject("Next called without id");
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
try {
|
|
439
|
-
Log.i(CapacitorUpdater.TAG, "Setting next active id " + id);
|
|
440
|
-
if (!this.implementation.setNextBundle(id)) {
|
|
441
|
-
Log.e(
|
|
442
|
-
CapacitorUpdater.TAG,
|
|
443
|
-
"Set next id failed. Bundle " + id + " does not exist."
|
|
444
|
-
);
|
|
445
|
-
call.reject("Set next id failed. Bundle " + id + " does not exist.");
|
|
446
|
-
} else {
|
|
447
|
-
call.resolve(this.implementation.getBundleInfo(id).toJSON());
|
|
448
|
-
}
|
|
449
|
-
} catch (final Exception e) {
|
|
450
|
-
Log.e(CapacitorUpdater.TAG, "Could not set next id " + id, e);
|
|
451
|
-
call.reject("Could not set next id: " + id, e);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
@PluginMethod
|
|
456
|
-
public void set(final PluginCall call) {
|
|
457
|
-
final String id = call.getString("id");
|
|
458
|
-
if (id == null) {
|
|
459
|
-
Log.e(CapacitorUpdater.TAG, "Set called without id");
|
|
460
|
-
call.reject("Set called without id");
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
try {
|
|
464
|
-
Log.i(CapacitorUpdater.TAG, "Setting active bundle " + id);
|
|
465
|
-
if (!this.implementation.set(id)) {
|
|
466
|
-
Log.i(CapacitorUpdater.TAG, "No such bundle " + id);
|
|
467
|
-
call.reject("Update failed, id " + id + " does not exist.");
|
|
468
|
-
} else {
|
|
469
|
-
Log.i(CapacitorUpdater.TAG, "Bundle successfully set to " + id);
|
|
470
|
-
this.reload(call);
|
|
471
|
-
}
|
|
472
|
-
} catch (final Exception e) {
|
|
473
|
-
Log.e(CapacitorUpdater.TAG, "Could not set id " + id, e);
|
|
474
|
-
call.reject("Could not set id " + id, e);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
@PluginMethod
|
|
479
|
-
public void delete(final PluginCall call) {
|
|
480
|
-
final String id = call.getString("id");
|
|
481
|
-
if (id == null) {
|
|
482
|
-
Log.e(CapacitorUpdater.TAG, "missing id");
|
|
483
|
-
call.reject("missing id");
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
Log.i(CapacitorUpdater.TAG, "Deleting id " + id);
|
|
487
|
-
try {
|
|
488
|
-
final Boolean res = this.implementation.delete(id);
|
|
489
|
-
if (res) {
|
|
490
|
-
call.resolve();
|
|
491
|
-
} else {
|
|
492
|
-
Log.e(
|
|
493
|
-
CapacitorUpdater.TAG,
|
|
494
|
-
"Delete failed, id " + id + " does not exist"
|
|
495
|
-
);
|
|
496
|
-
call.reject("Delete failed, id " + id + " does not exist");
|
|
497
|
-
}
|
|
498
|
-
} catch (final Exception e) {
|
|
499
|
-
Log.e(CapacitorUpdater.TAG, "Could not delete id " + id, e);
|
|
500
|
-
call.reject("Could not delete id " + id, e);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
@PluginMethod
|
|
505
|
-
public void list(final PluginCall call) {
|
|
506
|
-
try {
|
|
507
|
-
final List<BundleInfo> res = this.implementation.list();
|
|
508
|
-
final JSObject ret = new JSObject();
|
|
509
|
-
final JSArray values = new JSArray();
|
|
510
|
-
for (final BundleInfo bundle : res) {
|
|
511
|
-
values.put(bundle.toJSON());
|
|
512
|
-
}
|
|
513
|
-
ret.put("bundles", values);
|
|
514
|
-
call.resolve(ret);
|
|
515
|
-
} catch (final Exception e) {
|
|
516
|
-
Log.e(CapacitorUpdater.TAG, "Could not list bundles", e);
|
|
517
|
-
call.reject("Could not list bundles", e);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
@PluginMethod
|
|
522
|
-
public void getLatest(final PluginCall call) {
|
|
523
|
-
try {
|
|
524
|
-
new Thread(
|
|
525
|
-
new Runnable() {
|
|
526
|
-
@Override
|
|
527
|
-
public void run() {
|
|
528
|
-
CapacitorUpdaterPlugin.this.implementation.getLatest(
|
|
529
|
-
CapacitorUpdaterPlugin.this.updateUrl,
|
|
530
|
-
res -> {
|
|
531
|
-
if (res.has("error")) {
|
|
532
|
-
call.reject(res.getString("error"));
|
|
533
|
-
return;
|
|
534
|
-
} else {
|
|
535
|
-
call.resolve(res);
|
|
536
|
-
}
|
|
537
|
-
final JSObject ret = new JSObject();
|
|
538
|
-
Iterator<String> keys = res.keys();
|
|
539
|
-
while (keys.hasNext()) {
|
|
540
|
-
String key = keys.next();
|
|
541
|
-
if (res.has(key)) {
|
|
542
|
-
try {
|
|
543
|
-
ret.put(key, res.get(key));
|
|
544
|
-
} catch (JSONException e) {
|
|
545
|
-
e.printStackTrace();
|
|
546
|
-
}
|
|
213
|
+
|
|
214
|
+
public Thread startNewThread(final Runnable function) {
|
|
215
|
+
return startNewThread(function, 0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@Override
|
|
219
|
+
public void load() {
|
|
220
|
+
super.load();
|
|
221
|
+
|
|
222
|
+
// Initialize logger with osLogging config
|
|
223
|
+
// Default to true for both platforms to enable system logging by default
|
|
224
|
+
boolean osLogging = this.getConfig().getBoolean("osLogging", true);
|
|
225
|
+
Logger.Options loggerOptions = new Logger.Options(osLogging);
|
|
226
|
+
this.logger = new Logger("CapgoUpdater", loggerOptions);
|
|
227
|
+
|
|
228
|
+
this.prefs = this.getContext().getSharedPreferences(WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
|
|
229
|
+
this.editor = this.prefs.edit();
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
this.implementation = new CapgoUpdater(logger) {
|
|
233
|
+
@Override
|
|
234
|
+
public void notifyDownload(final String id, final int percent) {
|
|
235
|
+
if (activity != null) {
|
|
236
|
+
activity.runOnUiThread(() -> {
|
|
237
|
+
CapacitorUpdaterPlugin.this.notifyDownload(id, percent);
|
|
238
|
+
});
|
|
239
|
+
} else {
|
|
240
|
+
logger.warn("notifyDownload: Activity is null, skipping notification");
|
|
547
241
|
}
|
|
548
|
-
}
|
|
549
|
-
call.resolve(ret);
|
|
550
242
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
private boolean _reset(final Boolean toLastSuccessful) {
|
|
563
|
-
final BundleInfo fallback = this.implementation.getFallbackBundle();
|
|
564
|
-
this.implementation.reset();
|
|
565
|
-
|
|
566
|
-
if (toLastSuccessful && !fallback.isBuiltin()) {
|
|
567
|
-
Log.i(CapacitorUpdater.TAG, "Resetting to: " + fallback);
|
|
568
|
-
return this.implementation.set(fallback) && this._reload();
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
Log.i(CapacitorUpdater.TAG, "Resetting to native.");
|
|
572
|
-
return this._reload();
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
@PluginMethod
|
|
576
|
-
public void reset(final PluginCall call) {
|
|
577
|
-
try {
|
|
578
|
-
final Boolean toLastSuccessful = call.getBoolean(
|
|
579
|
-
"toLastSuccessful",
|
|
580
|
-
false
|
|
581
|
-
);
|
|
582
|
-
if (this._reset(toLastSuccessful)) {
|
|
583
|
-
call.resolve();
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
Log.e(CapacitorUpdater.TAG, "Reset failed");
|
|
587
|
-
call.reject("Reset failed");
|
|
588
|
-
} catch (final Exception e) {
|
|
589
|
-
Log.e(CapacitorUpdater.TAG, "Reset failed", e);
|
|
590
|
-
call.reject("Reset failed", e);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
@PluginMethod
|
|
595
|
-
public void current(final PluginCall call) {
|
|
596
|
-
try {
|
|
597
|
-
final JSObject ret = new JSObject();
|
|
598
|
-
final BundleInfo bundle = this.implementation.getCurrentBundle();
|
|
599
|
-
ret.put("bundle", bundle.toJSON());
|
|
600
|
-
ret.put("native", this.currentVersionNative);
|
|
601
|
-
call.resolve(ret);
|
|
602
|
-
} catch (final Exception e) {
|
|
603
|
-
Log.e(CapacitorUpdater.TAG, "Could not get current bundle", e);
|
|
604
|
-
call.reject("Could not get current bundle", e);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
@PluginMethod
|
|
609
|
-
public void notifyAppReady(final PluginCall call) {
|
|
610
|
-
try {
|
|
611
|
-
final BundleInfo bundle = this.implementation.getCurrentBundle();
|
|
612
|
-
this.implementation.setSuccess(bundle, this.autoDeletePrevious);
|
|
613
|
-
Log.i(
|
|
614
|
-
CapacitorUpdater.TAG,
|
|
615
|
-
"Current bundle loaded successfully. ['notifyAppReady()' was called] " +
|
|
616
|
-
bundle
|
|
617
|
-
);
|
|
618
|
-
call.resolve();
|
|
619
|
-
} catch (final Exception e) {
|
|
620
|
-
Log.e(
|
|
621
|
-
CapacitorUpdater.TAG,
|
|
622
|
-
"Failed to notify app ready state. [Error calling 'notifyAppReady()']",
|
|
623
|
-
e
|
|
624
|
-
);
|
|
625
|
-
call.reject("Failed to commit app ready state.", e);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
@PluginMethod
|
|
630
|
-
public void setMultiDelay(final PluginCall call) {
|
|
631
|
-
try {
|
|
632
|
-
final Object delayConditions = call.getData().opt("delayConditions");
|
|
633
|
-
if (delayConditions == null) {
|
|
634
|
-
Log.e(
|
|
635
|
-
CapacitorUpdater.TAG,
|
|
636
|
-
"setMultiDelay called without delayCondition"
|
|
637
|
-
);
|
|
638
|
-
call.reject("setMultiDelay called without delayCondition");
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
if (_setMultiDelay(delayConditions.toString())) {
|
|
642
|
-
call.resolve();
|
|
643
|
-
} else {
|
|
644
|
-
call.reject("Failed to delay update");
|
|
645
|
-
}
|
|
646
|
-
} catch (final Exception e) {
|
|
647
|
-
Log.e(
|
|
648
|
-
CapacitorUpdater.TAG,
|
|
649
|
-
"Failed to delay update, [Error calling 'setMultiDelay()']",
|
|
650
|
-
e
|
|
651
|
-
);
|
|
652
|
-
call.reject("Failed to delay update", e);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
private Boolean _setMultiDelay(String delayConditions) {
|
|
657
|
-
try {
|
|
658
|
-
this.editor.putString(DELAY_CONDITION_PREFERENCES, delayConditions);
|
|
659
|
-
this.editor.commit();
|
|
660
|
-
Log.i(CapacitorUpdater.TAG, "Delay update saved");
|
|
661
|
-
return true;
|
|
662
|
-
} catch (final Exception e) {
|
|
663
|
-
Log.e(
|
|
664
|
-
CapacitorUpdater.TAG,
|
|
665
|
-
"Failed to delay update, [Error calling '_setMultiDelay()']",
|
|
666
|
-
e
|
|
667
|
-
);
|
|
668
|
-
return false;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
@Deprecated
|
|
673
|
-
@PluginMethod
|
|
674
|
-
public void setDelay(final PluginCall call) {
|
|
675
|
-
try {
|
|
676
|
-
String kind = call.getString("kind");
|
|
677
|
-
String value = call.getString("value");
|
|
678
|
-
String delayConditions =
|
|
679
|
-
"[{\"kind\":\"" +
|
|
680
|
-
kind +
|
|
681
|
-
"\", \"value\":\"" +
|
|
682
|
-
(value != null ? value : "") +
|
|
683
|
-
"\"}]";
|
|
684
|
-
if (_setMultiDelay(delayConditions)) {
|
|
685
|
-
call.resolve();
|
|
686
|
-
} else {
|
|
687
|
-
call.reject("Failed to delay update");
|
|
688
|
-
}
|
|
689
|
-
} catch (final Exception e) {
|
|
690
|
-
Log.e(
|
|
691
|
-
CapacitorUpdater.TAG,
|
|
692
|
-
"Failed to delay update, [Error calling 'setDelay()']",
|
|
693
|
-
e
|
|
694
|
-
);
|
|
695
|
-
call.reject("Failed to delay update", e);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
private boolean _cancelDelay(String source) {
|
|
700
|
-
try {
|
|
701
|
-
this.editor.remove(DELAY_CONDITION_PREFERENCES);
|
|
702
|
-
this.editor.commit();
|
|
703
|
-
Log.i(CapacitorUpdater.TAG, "All delays canceled from " + source);
|
|
704
|
-
return true;
|
|
705
|
-
} catch (final Exception e) {
|
|
706
|
-
Log.e(CapacitorUpdater.TAG, "Failed to cancel update delay", e);
|
|
707
|
-
return false;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
@PluginMethod
|
|
712
|
-
public void cancelDelay(final PluginCall call) {
|
|
713
|
-
if (this._cancelDelay("JS")) {
|
|
714
|
-
call.resolve();
|
|
715
|
-
} else {
|
|
716
|
-
call.reject("Failed to cancel delay");
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
private void _checkCancelDelay(Boolean killed) {
|
|
721
|
-
Gson gson = new Gson();
|
|
722
|
-
String delayUpdatePreferences = prefs.getString(
|
|
723
|
-
DELAY_CONDITION_PREFERENCES,
|
|
724
|
-
"[]"
|
|
725
|
-
);
|
|
726
|
-
Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
|
|
727
|
-
ArrayList<DelayCondition> delayConditionList = gson.fromJson(
|
|
728
|
-
delayUpdatePreferences,
|
|
729
|
-
type
|
|
730
|
-
);
|
|
731
|
-
for (DelayCondition condition : delayConditionList) {
|
|
732
|
-
String kind = condition.getKind().toString();
|
|
733
|
-
String value = condition.getValue();
|
|
734
|
-
if (!"".equals(kind)) {
|
|
735
|
-
switch (kind) {
|
|
736
|
-
case "background":
|
|
737
|
-
if (!killed) {
|
|
738
|
-
this._cancelDelay("background check");
|
|
739
|
-
}
|
|
740
|
-
break;
|
|
741
|
-
case "kill":
|
|
742
|
-
if (killed) {
|
|
743
|
-
this._cancelDelay("kill check");
|
|
744
|
-
this.installNext();
|
|
745
|
-
}
|
|
746
|
-
break;
|
|
747
|
-
case "date":
|
|
748
|
-
if (!"".equals(value)) {
|
|
749
|
-
try {
|
|
750
|
-
final SimpleDateFormat sdf = new SimpleDateFormat(
|
|
751
|
-
"yyyy-MM-dd'T'HH:mm:ss.SSS"
|
|
752
|
-
);
|
|
753
|
-
Date date = sdf.parse(value);
|
|
754
|
-
assert date != null;
|
|
755
|
-
if (new Date().compareTo(date) > 0) {
|
|
756
|
-
this._cancelDelay("date expired");
|
|
243
|
+
|
|
244
|
+
@Override
|
|
245
|
+
public void directUpdateFinish(final BundleInfo latest) {
|
|
246
|
+
if (activity != null) {
|
|
247
|
+
activity.runOnUiThread(() -> {
|
|
248
|
+
CapacitorUpdaterPlugin.this.directUpdateFinish(latest);
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
logger.warn("directUpdateFinish: Activity is null, skipping notification");
|
|
252
|
+
}
|
|
757
253
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
final Version versionLimit = new Version(value);
|
|
769
|
-
if (this.currentVersionNative.isAtLeast(versionLimit)) {
|
|
770
|
-
this._cancelDelay("nativeVersion above limit");
|
|
254
|
+
|
|
255
|
+
@Override
|
|
256
|
+
public void notifyListeners(final String id, final Map<String, Object> res) {
|
|
257
|
+
if (activity != null) {
|
|
258
|
+
activity.runOnUiThread(() -> {
|
|
259
|
+
CapacitorUpdaterPlugin.this.notifyListeners(id, InternalUtils.mapToJSObject(res));
|
|
260
|
+
});
|
|
261
|
+
} else {
|
|
262
|
+
logger.warn("notifyListeners: Activity is null, skipping notification for event: " + id);
|
|
263
|
+
}
|
|
771
264
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
return (
|
|
795
|
-
CapacitorUpdaterPlugin.this.autoUpdate &&
|
|
796
|
-
!"".equals(CapacitorUpdaterPlugin.this.updateUrl) &&
|
|
797
|
-
serverUrl != null &&
|
|
798
|
-
!"".equals(serverUrl)
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
@PluginMethod
|
|
803
|
-
public void isAutoUpdateEnabled(final PluginCall call) {
|
|
804
|
-
try {
|
|
805
|
-
final JSObject ret = new JSObject();
|
|
806
|
-
ret.put("enabled", this._isAutoUpdateEnabled());
|
|
807
|
-
call.resolve(ret);
|
|
808
|
-
} catch (final Exception e) {
|
|
809
|
-
Log.e(CapacitorUpdater.TAG, "Could not get autoUpdate status", e);
|
|
810
|
-
call.reject("Could not get autoUpdate status", e);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
private void checkAppReady() {
|
|
815
|
-
try {
|
|
816
|
-
if (this.appReadyCheck != null) {
|
|
817
|
-
this.appReadyCheck.interrupt();
|
|
818
|
-
}
|
|
819
|
-
this.appReadyCheck = new Thread(new DeferredNotifyAppReadyCheck());
|
|
820
|
-
this.appReadyCheck.start();
|
|
821
|
-
} catch (final Exception e) {
|
|
822
|
-
Log.e(
|
|
823
|
-
CapacitorUpdater.TAG,
|
|
824
|
-
"Failed to start " + DeferredNotifyAppReadyCheck.class.getName(),
|
|
825
|
-
e
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
private boolean isValidURL(String urlStr) {
|
|
831
|
-
try {
|
|
832
|
-
URL url = new URL(urlStr);
|
|
833
|
-
return true;
|
|
834
|
-
} catch (MalformedURLException e) {
|
|
835
|
-
return false;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
private void backgroundDownload() {
|
|
840
|
-
new Thread(
|
|
841
|
-
new Runnable() {
|
|
842
|
-
@Override
|
|
843
|
-
public void run() {
|
|
844
|
-
Log.i(
|
|
845
|
-
CapacitorUpdater.TAG,
|
|
846
|
-
"Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl
|
|
847
|
-
);
|
|
848
|
-
CapacitorUpdaterPlugin.this.implementation.getLatest(
|
|
849
|
-
CapacitorUpdaterPlugin.this.updateUrl,
|
|
850
|
-
res -> {
|
|
851
|
-
final BundleInfo current =
|
|
852
|
-
CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
853
|
-
try {
|
|
854
|
-
if (res.has("message")) {
|
|
855
|
-
Log.i(
|
|
856
|
-
CapacitorUpdater.TAG,
|
|
857
|
-
"message " + res.get("message")
|
|
858
|
-
);
|
|
265
|
+
};
|
|
266
|
+
final PackageInfo pInfo = this.getContext().getPackageManager().getPackageInfo(this.getContext().getPackageName(), 0);
|
|
267
|
+
this.implementation.activity = this.getActivity();
|
|
268
|
+
this.implementation.versionBuild = this.getConfig().getString("version", pInfo.versionName);
|
|
269
|
+
this.implementation.CAP_SERVER_PATH = WebView.CAP_SERVER_PATH;
|
|
270
|
+
this.implementation.pluginVersion = this.pluginVersion;
|
|
271
|
+
this.implementation.versionCode = Integer.toString(pInfo.versionCode);
|
|
272
|
+
// Removed unused OkHttpClient creation - using shared client in DownloadService instead
|
|
273
|
+
// Handle directUpdate configuration - support string values and backward compatibility
|
|
274
|
+
String directUpdateConfig = this.getConfig().getString("directUpdate", null);
|
|
275
|
+
if (directUpdateConfig != null) {
|
|
276
|
+
// Handle backward compatibility for boolean true
|
|
277
|
+
if (directUpdateConfig.equals("true")) {
|
|
278
|
+
this.directUpdateMode = "always";
|
|
279
|
+
this.implementation.directUpdate = true;
|
|
280
|
+
} else {
|
|
281
|
+
this.directUpdateMode = directUpdateConfig;
|
|
282
|
+
this.implementation.directUpdate =
|
|
283
|
+
directUpdateConfig.equals("always") ||
|
|
284
|
+
directUpdateConfig.equals("atInstall") ||
|
|
285
|
+
directUpdateConfig.equals("onLaunch");
|
|
286
|
+
// Validate directUpdate value
|
|
859
287
|
if (
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
288
|
+
!directUpdateConfig.equals("false") &&
|
|
289
|
+
!directUpdateConfig.equals("always") &&
|
|
290
|
+
!directUpdateConfig.equals("atInstall") &&
|
|
291
|
+
!directUpdateConfig.equals("onLaunch")
|
|
863
292
|
) {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
majorAvailable
|
|
293
|
+
logger.error(
|
|
294
|
+
"Invalid directUpdate value: \"" +
|
|
295
|
+
directUpdateConfig +
|
|
296
|
+
"\". Supported values are: \"false\", \"true\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\"."
|
|
869
297
|
);
|
|
298
|
+
this.directUpdateMode = "false";
|
|
299
|
+
this.implementation.directUpdate = false;
|
|
870
300
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
Boolean directUpdateBool = this.getConfig().getBoolean("directUpdate", false);
|
|
304
|
+
if (directUpdateBool) {
|
|
305
|
+
this.directUpdateMode = "always"; // backward compatibility: true = always
|
|
306
|
+
this.implementation.directUpdate = true;
|
|
307
|
+
} else {
|
|
308
|
+
this.directUpdateMode = "false";
|
|
309
|
+
this.implementation.directUpdate = false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
this.currentVersionNative = new Version(this.getConfig().getString("version", pInfo.versionName));
|
|
313
|
+
this.currentBuildVersion = Integer.toString(pInfo.versionCode);
|
|
314
|
+
this.delayUpdateUtils = new DelayUpdateUtils(this.prefs, this.editor, this.currentVersionNative, logger);
|
|
315
|
+
} catch (final PackageManager.NameNotFoundException e) {
|
|
316
|
+
logger.error("Error instantiating implementation " + e.getMessage());
|
|
317
|
+
return;
|
|
318
|
+
} catch (final Exception e) {
|
|
319
|
+
logger.error("Error getting current native app version " + e.getMessage());
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
boolean disableJSLogging = this.getConfig().getBoolean("disableJSLogging", false);
|
|
324
|
+
// Set the bridge in the Logger when webView is available
|
|
325
|
+
if (this.bridge != null && this.bridge.getWebView() != null && !disableJSLogging) {
|
|
326
|
+
logger.setBridge(this.bridge);
|
|
327
|
+
logger.info("WebView set successfully for logging");
|
|
328
|
+
} else {
|
|
329
|
+
logger.info("WebView not ready yet, will be set later");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Set logger for shared classes
|
|
333
|
+
CryptoCipher.setLogger(logger);
|
|
334
|
+
DownloadService.setLogger(logger);
|
|
335
|
+
DownloadWorkerManager.setLogger(logger);
|
|
336
|
+
|
|
337
|
+
final CapConfig config = CapConfig.loadDefault(this.getActivity());
|
|
338
|
+
this.implementation.appId = InternalUtils.getPackageName(getContext().getPackageManager(), getContext().getPackageName());
|
|
339
|
+
this.implementation.appId = config.getString("appId", this.implementation.appId);
|
|
340
|
+
this.implementation.appId = this.getConfig().getString("appId", this.implementation.appId);
|
|
341
|
+
if (this.implementation.appId == null || this.implementation.appId.isEmpty()) {
|
|
342
|
+
// crash the app on purpose it should not happen
|
|
343
|
+
throw new RuntimeException(
|
|
344
|
+
"appId is missing in capacitor.config.json or plugin config, and cannot be retrieved from the native app, please add it globally or in the plugin config"
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
logger.info("appId: " + implementation.appId);
|
|
348
|
+
|
|
349
|
+
this.persistCustomId = this.getConfig().getBoolean("persistCustomId", false);
|
|
350
|
+
this.persistModifyUrl = this.getConfig().getBoolean("persistModifyUrl", false);
|
|
351
|
+
this.allowSetDefaultChannel = this.getConfig().getBoolean("allowSetDefaultChannel", true);
|
|
352
|
+
this.implementation.setPublicKey(this.getConfig().getString("publicKey", ""));
|
|
353
|
+
// Log public key prefix if encryption is enabled
|
|
354
|
+
String keyId = this.implementation.getKeyId();
|
|
355
|
+
if (keyId != null && !keyId.isEmpty()) {
|
|
356
|
+
logger.info("Public key prefix: " + keyId);
|
|
357
|
+
}
|
|
358
|
+
this.implementation.statsUrl = this.getConfig().getString("statsUrl", statsUrlDefault);
|
|
359
|
+
this.implementation.channelUrl = this.getConfig().getString("channelUrl", channelUrlDefault);
|
|
360
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
361
|
+
if (this.prefs.contains(STATS_URL_PREF_KEY)) {
|
|
362
|
+
final String storedStatsUrl = this.prefs.getString(STATS_URL_PREF_KEY, this.implementation.statsUrl);
|
|
363
|
+
if (storedStatsUrl != null) {
|
|
364
|
+
this.implementation.statsUrl = storedStatsUrl;
|
|
365
|
+
logger.info("Loaded persisted statsUrl");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (this.prefs.contains(CHANNEL_URL_PREF_KEY)) {
|
|
369
|
+
final String storedChannelUrl = this.prefs.getString(CHANNEL_URL_PREF_KEY, this.implementation.channelUrl);
|
|
370
|
+
if (storedChannelUrl != null) {
|
|
371
|
+
this.implementation.channelUrl = storedChannelUrl;
|
|
372
|
+
logger.info("Loaded persisted channelUrl");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Load defaultChannel: first try from persistent storage (set via setChannel), then fall back to config
|
|
378
|
+
if (this.prefs.contains(DEFAULT_CHANNEL_PREF_KEY)) {
|
|
379
|
+
final String storedDefaultChannel = this.prefs.getString(DEFAULT_CHANNEL_PREF_KEY, "");
|
|
380
|
+
if (storedDefaultChannel != null && !storedDefaultChannel.isEmpty()) {
|
|
381
|
+
this.implementation.defaultChannel = storedDefaultChannel;
|
|
382
|
+
logger.info("Loaded persisted defaultChannel from setChannel()");
|
|
383
|
+
} else {
|
|
384
|
+
this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
this.implementation.defaultChannel = this.getConfig().getString("defaultChannel", "");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
int userValue = this.getConfig().getInt("periodCheckDelay", 0);
|
|
391
|
+
|
|
392
|
+
if (userValue >= 0 && userValue <= 600) {
|
|
393
|
+
this.periodCheckDelay = 600 * 1000;
|
|
394
|
+
} else if (userValue > 600) {
|
|
395
|
+
this.periodCheckDelay = userValue * 1000;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
this.implementation.documentsDir = this.getContext().getFilesDir();
|
|
399
|
+
this.implementation.prefs = this.prefs;
|
|
400
|
+
this.implementation.editor = this.editor;
|
|
401
|
+
this.implementation.versionOs = Build.VERSION.RELEASE;
|
|
402
|
+
// Use DeviceIdHelper to get or create device ID that persists across reinstalls
|
|
403
|
+
this.implementation.deviceID = DeviceIdHelper.getOrCreateDeviceId(this.getContext(), this.prefs);
|
|
404
|
+
|
|
405
|
+
// Update User-Agent for shared OkHttpClient with OS version
|
|
406
|
+
DownloadService.updateUserAgent(this.implementation.appId, this.pluginVersion, this.implementation.versionOs);
|
|
407
|
+
|
|
408
|
+
if (Boolean.TRUE.equals(this.persistCustomId)) {
|
|
409
|
+
final String storedCustomId = this.prefs.getString(CUSTOM_ID_PREF_KEY, "");
|
|
410
|
+
if (storedCustomId != null && !storedCustomId.isEmpty()) {
|
|
411
|
+
this.implementation.customId = storedCustomId;
|
|
412
|
+
logger.info("Loaded persisted customId");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
logger.info("init for device " + this.implementation.deviceID);
|
|
416
|
+
logger.info("version native " + this.currentVersionNative.getOriginalString());
|
|
417
|
+
this.autoDeleteFailed = this.getConfig().getBoolean("autoDeleteFailed", true);
|
|
418
|
+
this.autoDeletePrevious = this.getConfig().getBoolean("autoDeletePrevious", true);
|
|
419
|
+
this.updateUrl = this.getConfig().getString("updateUrl", updateUrlDefault);
|
|
420
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
421
|
+
if (this.prefs.contains(UPDATE_URL_PREF_KEY)) {
|
|
422
|
+
final String storedUpdateUrl = this.prefs.getString(UPDATE_URL_PREF_KEY, this.updateUrl);
|
|
423
|
+
if (storedUpdateUrl != null) {
|
|
424
|
+
this.updateUrl = storedUpdateUrl;
|
|
425
|
+
logger.info("Loaded persisted updateUrl");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
this.autoUpdate = this.getConfig().getBoolean("autoUpdate", true);
|
|
430
|
+
this.appReadyTimeout = Math.max(1000, this.getConfig().getInt("appReadyTimeout", 10000)); // Minimum 1 second
|
|
431
|
+
this.keepUrlPathAfterReload = this.getConfig().getBoolean("keepUrlPathAfterReload", false);
|
|
432
|
+
this.syncKeepUrlPathFlag(this.keepUrlPathAfterReload);
|
|
433
|
+
this.allowManualBundleError = this.getConfig().getBoolean("allowManualBundleError", false);
|
|
434
|
+
this.autoSplashscreen = this.getConfig().getBoolean("autoSplashscreen", false);
|
|
435
|
+
this.autoSplashscreenLoader = this.getConfig().getBoolean("autoSplashscreenLoader", false);
|
|
436
|
+
int splashscreenTimeoutValue = this.getConfig().getInt("autoSplashscreenTimeout", 10000);
|
|
437
|
+
this.autoSplashscreenTimeout = Math.max(0, splashscreenTimeoutValue);
|
|
438
|
+
this.implementation.timeout = this.getConfig().getInt("responseTimeout", 20) * 1000;
|
|
439
|
+
this.shakeMenuEnabled = this.getConfig().getBoolean("shakeMenu", false);
|
|
440
|
+
this.shakeChannelSelectorEnabled = this.getConfig().getBoolean("allowShakeChannelSelector", false);
|
|
441
|
+
boolean resetWhenUpdate = this.getConfig().getBoolean("resetWhenUpdate", true);
|
|
442
|
+
|
|
443
|
+
// Check if app was recently installed/updated BEFORE cleanupObsoleteVersions updates LatestVersionNative
|
|
444
|
+
this.wasRecentlyInstalledOrUpdated = this.checkIfRecentlyInstalledOrUpdated();
|
|
445
|
+
|
|
446
|
+
this.implementation.autoReset();
|
|
447
|
+
if (resetWhenUpdate) {
|
|
448
|
+
this.cleanupObsoleteVersions();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check for 'kill' delay condition on app launch
|
|
452
|
+
// This handles cases where the app was killed by the system (onDestroy is not reliable)
|
|
453
|
+
this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
|
|
454
|
+
|
|
455
|
+
this.checkForUpdateAfterDelay();
|
|
456
|
+
|
|
457
|
+
// On Android 14+ (API 34+), topActivity in RecentTaskInfo returns null due to
|
|
458
|
+
// security restrictions (StrandHogg task hijacking mitigations). Use ProcessLifecycleOwner
|
|
459
|
+
// for reliable app-level foreground/background detection on these versions.
|
|
460
|
+
// On older versions, we use the traditional activity lifecycle callbacks in handleOnStart/handleOnStop.
|
|
461
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
462
|
+
this.appLifecycleObserver = new AppLifecycleObserver(
|
|
463
|
+
new AppLifecycleObserver.AppLifecycleListener() {
|
|
464
|
+
@Override
|
|
465
|
+
public void onAppMovedToForeground() {
|
|
466
|
+
CapacitorUpdaterPlugin.this.appMovedToForeground();
|
|
962
467
|
}
|
|
963
468
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
469
|
+
@Override
|
|
470
|
+
public void onAppMovedToBackground() {
|
|
471
|
+
CapacitorUpdaterPlugin.this.appMovedToBackground();
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
logger
|
|
475
|
+
);
|
|
476
|
+
this.appLifecycleObserver.register();
|
|
477
|
+
logger.info("Using ProcessLifecycleOwner for foreground/background detection (Android 14+)");
|
|
478
|
+
} else {
|
|
479
|
+
logger.info("Using activity lifecycle callbacks for foreground/background detection (Android <14)");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
977
482
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
483
|
+
private void semaphoreWait(Number waitTime) {
|
|
484
|
+
try {
|
|
485
|
+
semaphoreReady.awaitAdvanceInterruptibly(semaphoreReady.getPhase(), waitTime.longValue(), TimeUnit.SECONDS);
|
|
486
|
+
logger.info("semaphoreReady count " + semaphoreReady.getPhase());
|
|
487
|
+
} catch (InterruptedException e) {
|
|
488
|
+
logger.info("semaphoreWait InterruptedException");
|
|
489
|
+
Thread.currentThread().interrupt(); // Restore interrupted status
|
|
490
|
+
} catch (TimeoutException e) {
|
|
491
|
+
logger.error("Semaphore timeout: " + e.getMessage());
|
|
492
|
+
// Don't throw runtime exception, just log and continue
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private void semaphoreUp() {
|
|
497
|
+
logger.info("semaphoreUp");
|
|
498
|
+
semaphoreReady.register();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private void semaphoreDown() {
|
|
502
|
+
logger.info("semaphoreDown");
|
|
503
|
+
logger.info("semaphoreDown count " + semaphoreReady.getPhase());
|
|
504
|
+
semaphoreReady.arriveAndDeregister();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private void sendReadyToJs(final BundleInfo current, final String msg) {
|
|
508
|
+
sendReadyToJs(current, msg, false);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private void sendReadyToJs(final BundleInfo current, final String msg, final boolean isDirectUpdate) {
|
|
512
|
+
logger.info("sendReadyToJs: " + msg);
|
|
513
|
+
final JSObject ret = new JSObject();
|
|
514
|
+
ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
515
|
+
ret.put("status", msg);
|
|
516
|
+
|
|
517
|
+
// No need to wait for semaphore anymore since _reload() has already waited
|
|
518
|
+
this.notifyListeners("appReady", ret, true);
|
|
519
|
+
|
|
520
|
+
// Auto hide splashscreen if enabled
|
|
521
|
+
// We show it on background when conditions are met, so we should hide it on foreground regardless of update outcome
|
|
522
|
+
if (this.autoSplashscreen) {
|
|
523
|
+
this.hideSplashscreen();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private void hideSplashscreen() {
|
|
528
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
529
|
+
hideSplashscreenInternal();
|
|
530
|
+
} else {
|
|
531
|
+
this.mainHandler.post(this::hideSplashscreenInternal);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private void hideSplashscreenInternal() {
|
|
536
|
+
cancelSplashscreenTimeout();
|
|
537
|
+
removeSplashscreenLoader();
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
if (getBridge() == null) {
|
|
541
|
+
logger.warn("Bridge not ready for hiding splashscreen with autoSplashscreen");
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Try to call the SplashScreen plugin directly through the bridge
|
|
546
|
+
PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
|
|
547
|
+
if (splashScreenPlugin != null) {
|
|
548
|
+
try {
|
|
549
|
+
// Create a plugin call for the hide method using reflection to access private msgHandler
|
|
550
|
+
JSObject options = new JSObject();
|
|
551
|
+
java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
|
|
552
|
+
msgHandlerField.setAccessible(true);
|
|
553
|
+
Object msgHandler = msgHandlerField.get(getBridge());
|
|
554
|
+
|
|
555
|
+
PluginCall call = new PluginCall(
|
|
556
|
+
(com.getcapacitor.MessageHandler) msgHandler,
|
|
557
|
+
"SplashScreen",
|
|
558
|
+
"FAKE_CALLBACK_ID_HIDE",
|
|
559
|
+
"hide",
|
|
560
|
+
options
|
|
1041
561
|
);
|
|
562
|
+
|
|
563
|
+
// Call the hide method directly
|
|
564
|
+
splashScreenPlugin.invoke("hide", call);
|
|
565
|
+
logger.info("Splashscreen hidden automatically via direct plugin call");
|
|
566
|
+
} catch (Exception e) {
|
|
567
|
+
logger.error("Failed to call SplashScreen hide method: " + e.getMessage());
|
|
1042
568
|
}
|
|
1043
|
-
|
|
569
|
+
} else {
|
|
570
|
+
logger.warn("autoSplashscreen: SplashScreen plugin not found. Install @capacitor/splash-screen plugin.");
|
|
571
|
+
}
|
|
572
|
+
} catch (Exception e) {
|
|
573
|
+
logger.error(
|
|
574
|
+
"Error hiding splashscreen with autoSplashscreen: " +
|
|
575
|
+
e.getMessage() +
|
|
576
|
+
". Make sure @capacitor/splash-screen plugin is installed and configured."
|
|
1044
577
|
);
|
|
1045
578
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
private void installNext() {
|
|
1052
|
-
try {
|
|
1053
|
-
Gson gson = new Gson();
|
|
1054
|
-
String delayUpdatePreferences = prefs.getString(
|
|
1055
|
-
DELAY_CONDITION_PREFERENCES,
|
|
1056
|
-
"[]"
|
|
1057
|
-
);
|
|
1058
|
-
Type type = new TypeToken<ArrayList<DelayCondition>>() {}.getType();
|
|
1059
|
-
ArrayList<DelayCondition> delayConditionList = gson.fromJson(
|
|
1060
|
-
delayUpdatePreferences,
|
|
1061
|
-
type
|
|
1062
|
-
);
|
|
1063
|
-
if (delayConditionList != null && delayConditionList.size() != 0) {
|
|
1064
|
-
Log.i(CapacitorUpdater.TAG, "Update delayed to next backgrounding");
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
1068
|
-
final BundleInfo next = this.implementation.getNextBundle();
|
|
1069
|
-
|
|
1070
|
-
if (
|
|
1071
|
-
next != null &&
|
|
1072
|
-
!next.isErrorStatus() &&
|
|
1073
|
-
!next.getId().equals(current.getId())
|
|
1074
|
-
) {
|
|
1075
|
-
// There is a next bundle waiting for activation
|
|
1076
|
-
Log.d(CapacitorUpdater.TAG, "Next bundle is: " + next.getVersionName());
|
|
1077
|
-
if (this.implementation.set(next) && this._reload()) {
|
|
1078
|
-
Log.i(
|
|
1079
|
-
CapacitorUpdater.TAG,
|
|
1080
|
-
"Updated to bundle: " + next.getVersionName()
|
|
1081
|
-
);
|
|
1082
|
-
this.implementation.setNextBundle(null);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private void showSplashscreen() {
|
|
582
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
583
|
+
showSplashscreenNow();
|
|
1083
584
|
} else {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
private void checkRevert() {
|
|
1096
|
-
// Automatically roll back to fallback version if notifyAppReady has not been called yet
|
|
1097
|
-
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
1098
|
-
|
|
1099
|
-
if (current.isBuiltin()) {
|
|
1100
|
-
Log.i(CapacitorUpdater.TAG, "Built-in bundle is active. Nothing to do.");
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
Log.d(CapacitorUpdater.TAG, "Current bundle is: " + current);
|
|
1104
|
-
|
|
1105
|
-
if (BundleStatus.SUCCESS != current.getStatus()) {
|
|
1106
|
-
Log.e(
|
|
1107
|
-
CapacitorUpdater.TAG,
|
|
1108
|
-
"notifyAppReady was not called, roll back current bundle: " +
|
|
1109
|
-
current.getId()
|
|
1110
|
-
);
|
|
1111
|
-
Log.i(
|
|
1112
|
-
CapacitorUpdater.TAG,
|
|
1113
|
-
"Did you forget to call 'notifyAppReady()' in your Capacitor App code?"
|
|
1114
|
-
);
|
|
1115
|
-
final JSObject ret = new JSObject();
|
|
1116
|
-
ret.put("bundle", current.toJSON());
|
|
1117
|
-
this.notifyListeners("updateFailed", ret);
|
|
1118
|
-
this.implementation.sendStats("update_fail", current.getVersionName());
|
|
1119
|
-
this.implementation.setError(current);
|
|
1120
|
-
this._reset(true);
|
|
1121
|
-
if (
|
|
1122
|
-
CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()
|
|
1123
|
-
) {
|
|
1124
|
-
Log.i(
|
|
1125
|
-
CapacitorUpdater.TAG,
|
|
1126
|
-
"Deleting failing bundle: " + current.getVersionName()
|
|
1127
|
-
);
|
|
585
|
+
this.mainHandler.post(this::showSplashscreenNow);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private void showSplashscreenNow() {
|
|
590
|
+
cancelSplashscreenTimeout();
|
|
591
|
+
this.autoSplashscreenTimedOut = false;
|
|
592
|
+
|
|
1128
593
|
try {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
594
|
+
if (getBridge() == null) {
|
|
595
|
+
logger.warn("Bridge not ready for showing splashscreen with autoSplashscreen");
|
|
596
|
+
} else {
|
|
597
|
+
PluginHandle splashScreenPlugin = getBridge().getPlugin("SplashScreen");
|
|
598
|
+
if (splashScreenPlugin != null) {
|
|
599
|
+
JSObject options = new JSObject();
|
|
600
|
+
java.lang.reflect.Field msgHandlerField = getBridge().getClass().getDeclaredField("msgHandler");
|
|
601
|
+
msgHandlerField.setAccessible(true);
|
|
602
|
+
Object msgHandler = msgHandlerField.get(getBridge());
|
|
603
|
+
|
|
604
|
+
PluginCall call = new PluginCall(
|
|
605
|
+
(com.getcapacitor.MessageHandler) msgHandler,
|
|
606
|
+
"SplashScreen",
|
|
607
|
+
"FAKE_CALLBACK_ID_SHOW",
|
|
608
|
+
"show",
|
|
609
|
+
options
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
splashScreenPlugin.invoke("show", call);
|
|
613
|
+
logger.info("Splashscreen shown synchronously to prevent flash");
|
|
614
|
+
} else {
|
|
615
|
+
logger.warn("autoSplashscreen: SplashScreen plugin not found");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch (Exception e) {
|
|
619
|
+
logger.error("Failed to show splashscreen synchronously: " + e.getMessage());
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
addSplashscreenLoaderIfNeeded();
|
|
623
|
+
scheduleSplashscreenTimeout();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private void addSplashscreenLoaderIfNeeded() {
|
|
627
|
+
if (!Boolean.TRUE.equals(this.autoSplashscreenLoader)) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
Runnable addLoader = () -> {
|
|
632
|
+
if (this.splashscreenLoaderOverlay != null) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
Activity activity = getActivity();
|
|
637
|
+
if (activity == null) {
|
|
638
|
+
logger.warn("autoSplashscreen: Activity not available for loader overlay");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
ProgressBar progressBar = new ProgressBar(activity);
|
|
643
|
+
progressBar.setIndeterminate(true);
|
|
644
|
+
|
|
645
|
+
FrameLayout overlay = new FrameLayout(activity);
|
|
646
|
+
overlay.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
|
647
|
+
overlay.setClickable(false);
|
|
648
|
+
overlay.setFocusable(false);
|
|
649
|
+
overlay.setBackgroundColor(Color.TRANSPARENT);
|
|
650
|
+
overlay.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
|
651
|
+
|
|
652
|
+
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
|
653
|
+
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
654
|
+
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
1135
655
|
);
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
656
|
+
params.gravity = Gravity.CENTER;
|
|
657
|
+
overlay.addView(progressBar, params);
|
|
658
|
+
|
|
659
|
+
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
|
|
660
|
+
decorView.addView(overlay);
|
|
661
|
+
|
|
662
|
+
this.splashscreenLoaderOverlay = overlay;
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
666
|
+
addLoader.run();
|
|
667
|
+
} else {
|
|
668
|
+
this.mainHandler.post(addLoader);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private void removeSplashscreenLoader() {
|
|
673
|
+
Runnable removeLoader = () -> {
|
|
674
|
+
if (this.splashscreenLoaderOverlay != null) {
|
|
675
|
+
ViewGroup parent = (ViewGroup) this.splashscreenLoaderOverlay.getParent();
|
|
676
|
+
if (parent != null) {
|
|
677
|
+
parent.removeView(this.splashscreenLoaderOverlay);
|
|
678
|
+
}
|
|
679
|
+
this.splashscreenLoaderOverlay = null;
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
684
|
+
removeLoader.run();
|
|
685
|
+
} else {
|
|
686
|
+
this.mainHandler.post(removeLoader);
|
|
1143
687
|
}
|
|
1144
|
-
}
|
|
1145
|
-
} else {
|
|
1146
|
-
Log.i(
|
|
1147
|
-
CapacitorUpdater.TAG,
|
|
1148
|
-
"notifyAppReady was called. This is fine: " + current.getId()
|
|
1149
|
-
);
|
|
1150
688
|
}
|
|
1151
|
-
}
|
|
1152
689
|
|
|
1153
|
-
|
|
690
|
+
private void scheduleSplashscreenTimeout() {
|
|
691
|
+
if (this.autoSplashscreenTimeout == null || this.autoSplashscreenTimeout <= 0) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
cancelSplashscreenTimeout();
|
|
1154
696
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
697
|
+
this.splashscreenTimeoutRunnable = () -> {
|
|
698
|
+
logger.info("autoSplashscreen timeout reached, hiding splashscreen");
|
|
699
|
+
this.autoSplashscreenTimedOut = true;
|
|
700
|
+
this.implementation.directUpdate = false;
|
|
701
|
+
hideSplashscreen();
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
this.mainHandler.postDelayed(this.splashscreenTimeoutRunnable, this.autoSplashscreenTimeout);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private void cancelSplashscreenTimeout() {
|
|
708
|
+
if (this.splashscreenTimeoutRunnable != null) {
|
|
709
|
+
this.mainHandler.removeCallbacks(this.splashscreenTimeoutRunnable);
|
|
710
|
+
this.splashscreenTimeoutRunnable = null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private boolean checkIfRecentlyInstalledOrUpdated() {
|
|
715
|
+
String currentVersion = this.currentBuildVersion;
|
|
716
|
+
String lastKnownVersion = this.prefs.getString("LatestNativeBuildVersion", "");
|
|
717
|
+
|
|
718
|
+
if (lastKnownVersion.isEmpty()) {
|
|
719
|
+
// First time running, consider it as recently installed
|
|
720
|
+
return true;
|
|
721
|
+
} else if (!lastKnownVersion.equals(currentVersion)) {
|
|
722
|
+
// Version changed, consider it as recently updated
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private boolean shouldUseDirectUpdate() {
|
|
730
|
+
if (Boolean.TRUE.equals(this.autoSplashscreenTimedOut)) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
switch (this.directUpdateMode) {
|
|
734
|
+
case "false":
|
|
735
|
+
return false;
|
|
736
|
+
case "always":
|
|
737
|
+
return true;
|
|
738
|
+
case "atInstall":
|
|
739
|
+
if (this.wasRecentlyInstalledOrUpdated) {
|
|
740
|
+
// Reset the flag after first use to prevent subsequent foreground events from using direct update
|
|
741
|
+
this.wasRecentlyInstalledOrUpdated = false;
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
case "onLaunch":
|
|
746
|
+
if (!this.onLaunchDirectUpdateUsed) {
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
return false;
|
|
750
|
+
default:
|
|
751
|
+
logger.error(
|
|
752
|
+
"Invalid directUpdateMode: \"" +
|
|
753
|
+
this.directUpdateMode +
|
|
754
|
+
"\". Supported values are: \"false\", \"always\", \"atInstall\", \"onLaunch\". Defaulting to \"false\" behavior."
|
|
755
|
+
);
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private boolean isDirectUpdateCurrentlyAllowed(final boolean plannedDirectUpdate) {
|
|
761
|
+
return plannedDirectUpdate && !Boolean.TRUE.equals(this.autoSplashscreenTimedOut);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private void directUpdateFinish(final BundleInfo latest) {
|
|
765
|
+
if ("onLaunch".equals(this.directUpdateMode)) {
|
|
766
|
+
this.onLaunchDirectUpdateUsed = true;
|
|
767
|
+
this.implementation.directUpdate = false;
|
|
768
|
+
}
|
|
769
|
+
CapacitorUpdaterPlugin.this.implementation.set(latest);
|
|
770
|
+
CapacitorUpdaterPlugin.this._reload();
|
|
771
|
+
sendReadyToJs(latest, "update installed", true);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
private void cleanupObsoleteVersions() {
|
|
775
|
+
cleanupThread = startNewThread(() -> {
|
|
776
|
+
synchronized (cleanupLock) {
|
|
1215
777
|
try {
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
778
|
+
final String previous = this.prefs.getString("LatestNativeBuildVersion", "");
|
|
779
|
+
if (!"".equals(previous) && !Objects.equals(this.currentBuildVersion, previous)) {
|
|
780
|
+
logger.info("New native build version detected: " + this.currentBuildVersion);
|
|
781
|
+
this.implementation.reset(true);
|
|
782
|
+
final List<BundleInfo> installed = this.implementation.list(false);
|
|
783
|
+
for (final BundleInfo bundle : installed) {
|
|
784
|
+
// Check if thread was interrupted (cancelled)
|
|
785
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
786
|
+
logger.warn("Cleanup was cancelled, stopping");
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
logger.info("Deleting obsolete bundle: " + bundle.getId());
|
|
791
|
+
this.implementation.delete(bundle.getId());
|
|
792
|
+
} catch (final Exception e) {
|
|
793
|
+
logger.error("Failed to delete: " + bundle.getId() + " " + e.getMessage());
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
final List<BundleInfo> storedBundles = this.implementation.list(true);
|
|
797
|
+
final Set<String> allowedIds = new HashSet<>();
|
|
798
|
+
for (final BundleInfo info : storedBundles) {
|
|
799
|
+
if (info != null && info.getId() != null && !info.getId().isEmpty()) {
|
|
800
|
+
allowedIds.add(info.getId());
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
this.implementation.cleanupDownloadDirectories(allowedIds, Thread.currentThread());
|
|
804
|
+
this.implementation.cleanupOrphanedTempFolders(Thread.currentThread());
|
|
805
|
+
|
|
806
|
+
// Check again before the expensive delta cache cleanup
|
|
807
|
+
if (Thread.currentThread().isInterrupted()) {
|
|
808
|
+
logger.warn("Cleanup was cancelled before delta cache cleanup");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
this.implementation.cleanupDeltaCache(Thread.currentThread());
|
|
812
|
+
}
|
|
813
|
+
this.editor.putString("LatestNativeBuildVersion", this.currentBuildVersion);
|
|
814
|
+
this.editor.apply();
|
|
815
|
+
} catch (Exception e) {
|
|
816
|
+
logger.error("Error during cleanupObsoleteVersions: " + e.getMessage());
|
|
817
|
+
} finally {
|
|
818
|
+
cleanupComplete = true;
|
|
819
|
+
logger.info("Cleanup complete");
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// Start a timeout watchdog thread to cancel cleanup if it takes too long
|
|
825
|
+
final long timeout = this.appReadyTimeout / 2;
|
|
826
|
+
startNewThread(() -> {
|
|
827
|
+
try {
|
|
828
|
+
Thread.sleep(timeout);
|
|
829
|
+
if (cleanupThread != null && cleanupThread.isAlive() && !cleanupComplete) {
|
|
830
|
+
logger.warn("Cleanup timeout exceeded (" + timeout + "ms), interrupting cleanup thread");
|
|
831
|
+
cleanupThread.interrupt();
|
|
832
|
+
}
|
|
833
|
+
} catch (InterruptedException e) {
|
|
834
|
+
// Watchdog thread was interrupted, that's fine
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private void waitForCleanupIfNeeded() {
|
|
840
|
+
if (cleanupComplete) {
|
|
841
|
+
return; // Already done, no need to wait
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
logger.info("Waiting for cleanup to complete before starting download...");
|
|
845
|
+
|
|
846
|
+
// Wait for cleanup to complete - blocks until lock is released
|
|
847
|
+
synchronized (cleanupLock) {
|
|
848
|
+
logger.info("Cleanup finished, proceeding with download");
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
public void notifyDownload(final String id, final int percent) {
|
|
853
|
+
try {
|
|
854
|
+
final JSObject ret = new JSObject();
|
|
855
|
+
ret.put("percent", percent);
|
|
856
|
+
final BundleInfo bundleInfo = this.implementation.getBundleInfo(id);
|
|
857
|
+
ret.put("bundle", InternalUtils.mapToJSObject(bundleInfo.toJSONMap()));
|
|
858
|
+
this.notifyListeners("download", ret);
|
|
859
|
+
|
|
860
|
+
if (percent == 100) {
|
|
861
|
+
final JSObject retDownloadComplete = new JSObject(ret, new String[] { "bundle" });
|
|
862
|
+
this.notifyListeners("downloadComplete", retDownloadComplete);
|
|
863
|
+
this.implementation.sendStats("download_complete", bundleInfo.getVersionName());
|
|
864
|
+
lastNotifiedStatPercent = 100;
|
|
865
|
+
} else {
|
|
866
|
+
int currentStatPercent = (percent / 10) * 10; // Round down to nearest 10
|
|
867
|
+
if (currentStatPercent > lastNotifiedStatPercent) {
|
|
868
|
+
this.implementation.sendStats("download_" + currentStatPercent, bundleInfo.getVersionName());
|
|
869
|
+
lastNotifiedStatPercent = currentStatPercent;
|
|
1225
870
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
@
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
871
|
+
}
|
|
872
|
+
} catch (final Exception e) {
|
|
873
|
+
logger.error("Could not notify listeners " + e.getMessage());
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
@PluginMethod
|
|
878
|
+
public void setUpdateUrl(final PluginCall call) {
|
|
879
|
+
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
880
|
+
logger.error("setUpdateUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
881
|
+
call.reject("setUpdateUrl not allowed");
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
final String url = call.getString("url");
|
|
885
|
+
if (url == null) {
|
|
886
|
+
logger.error("setUpdateUrl called without url");
|
|
887
|
+
call.reject("setUpdateUrl called without url");
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
this.updateUrl = url;
|
|
891
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
892
|
+
this.editor.putString(UPDATE_URL_PREF_KEY, url);
|
|
893
|
+
this.editor.apply();
|
|
894
|
+
}
|
|
895
|
+
call.resolve();
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
@PluginMethod
|
|
899
|
+
public void setStatsUrl(final PluginCall call) {
|
|
900
|
+
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
901
|
+
logger.error("setStatsUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
902
|
+
call.reject("setStatsUrl not allowed");
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
final String url = call.getString("url");
|
|
906
|
+
if (url == null) {
|
|
907
|
+
logger.error("setStatsUrl called without url");
|
|
908
|
+
call.reject("setStatsUrl called without url");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
this.implementation.statsUrl = url;
|
|
912
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
913
|
+
this.editor.putString(STATS_URL_PREF_KEY, url);
|
|
914
|
+
this.editor.apply();
|
|
915
|
+
}
|
|
916
|
+
call.resolve();
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
@PluginMethod
|
|
920
|
+
public void setChannelUrl(final PluginCall call) {
|
|
921
|
+
if (!this.getConfig().getBoolean("allowModifyUrl", false)) {
|
|
922
|
+
logger.error("setChannelUrl not allowed set allowModifyUrl in your config to true to allow it");
|
|
923
|
+
call.reject("setChannelUrl not allowed");
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
final String url = call.getString("url");
|
|
927
|
+
if (url == null) {
|
|
928
|
+
logger.error("setChannelUrl called without url");
|
|
929
|
+
call.reject("setChannelUrl called without url");
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
this.implementation.channelUrl = url;
|
|
933
|
+
if (Boolean.TRUE.equals(this.persistModifyUrl)) {
|
|
934
|
+
this.editor.putString(CHANNEL_URL_PREF_KEY, url);
|
|
935
|
+
this.editor.apply();
|
|
936
|
+
}
|
|
937
|
+
call.resolve();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
@PluginMethod
|
|
941
|
+
public void getBuiltinVersion(final PluginCall call) {
|
|
942
|
+
try {
|
|
943
|
+
final JSObject ret = new JSObject();
|
|
944
|
+
ret.put("version", this.implementation.versionBuild);
|
|
945
|
+
call.resolve(ret);
|
|
946
|
+
} catch (final Exception e) {
|
|
947
|
+
logger.error("Could not get version " + e.getMessage());
|
|
948
|
+
call.reject("Could not get version", e);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
@PluginMethod
|
|
953
|
+
public void getDeviceId(final PluginCall call) {
|
|
954
|
+
try {
|
|
955
|
+
final JSObject ret = new JSObject();
|
|
956
|
+
ret.put("deviceId", this.implementation.deviceID);
|
|
957
|
+
call.resolve(ret);
|
|
958
|
+
} catch (final Exception e) {
|
|
959
|
+
logger.error("Could not get device id " + e.getMessage());
|
|
960
|
+
call.reject("Could not get device id", e);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
@PluginMethod
|
|
965
|
+
public void setCustomId(final PluginCall call) {
|
|
966
|
+
final String customId = call.getString("customId");
|
|
967
|
+
if (customId == null) {
|
|
968
|
+
logger.error("setCustomId called without customId");
|
|
969
|
+
call.reject("setCustomId called without customId");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
this.implementation.customId = customId;
|
|
973
|
+
if (Boolean.TRUE.equals(this.persistCustomId)) {
|
|
974
|
+
if (customId.isEmpty()) {
|
|
975
|
+
this.editor.remove(CUSTOM_ID_PREF_KEY);
|
|
976
|
+
} else {
|
|
977
|
+
this.editor.putString(CUSTOM_ID_PREF_KEY, customId);
|
|
978
|
+
}
|
|
979
|
+
this.editor.apply();
|
|
980
|
+
}
|
|
981
|
+
call.resolve();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
@PluginMethod
|
|
985
|
+
public void getPluginVersion(final PluginCall call) {
|
|
986
|
+
try {
|
|
987
|
+
final JSObject ret = new JSObject();
|
|
988
|
+
ret.put("version", this.pluginVersion);
|
|
989
|
+
call.resolve(ret);
|
|
990
|
+
} catch (final Exception e) {
|
|
991
|
+
logger.error("Could not get plugin version " + e.getMessage());
|
|
992
|
+
call.reject("Could not get plugin version", e);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
@PluginMethod
|
|
997
|
+
public void unsetChannel(final PluginCall call) {
|
|
998
|
+
final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
|
|
999
|
+
|
|
1000
|
+
try {
|
|
1001
|
+
logger.info("unsetChannel triggerAutoUpdate: " + triggerAutoUpdate);
|
|
1002
|
+
startNewThread(() -> {
|
|
1003
|
+
String configDefaultChannel = CapacitorUpdaterPlugin.this.getConfig().getString("defaultChannel", "");
|
|
1004
|
+
CapacitorUpdaterPlugin.this.implementation.unsetChannel(
|
|
1005
|
+
CapacitorUpdaterPlugin.this.editor,
|
|
1006
|
+
DEFAULT_CHANNEL_PREF_KEY,
|
|
1007
|
+
configDefaultChannel,
|
|
1008
|
+
(res) -> {
|
|
1009
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1010
|
+
if (jsRes.has("error")) {
|
|
1011
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
1012
|
+
String errorCode = jsRes.getString("error");
|
|
1013
|
+
|
|
1014
|
+
JSObject errorObj = new JSObject();
|
|
1015
|
+
errorObj.put("message", errorMessage);
|
|
1016
|
+
errorObj.put("error", errorCode);
|
|
1017
|
+
|
|
1018
|
+
call.reject(errorMessage, "UNSETCHANNEL_FAILED", null, errorObj);
|
|
1019
|
+
} else {
|
|
1020
|
+
if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
|
|
1021
|
+
logger.info("Calling autoupdater after channel change!");
|
|
1022
|
+
// Check if download is already in progress (with timeout protection)
|
|
1023
|
+
if (!this.isDownloadStuckOrTimedOut()) {
|
|
1024
|
+
backgroundDownload();
|
|
1025
|
+
} else {
|
|
1026
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
call.resolve(jsRes);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
);
|
|
1033
|
+
});
|
|
1034
|
+
} catch (final Exception e) {
|
|
1035
|
+
logger.error("Failed to unsetChannel: " + e.getMessage());
|
|
1036
|
+
call.reject("Failed to unsetChannel: ", e);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
@PluginMethod
|
|
1041
|
+
public void setChannel(final PluginCall call) {
|
|
1042
|
+
final String channel = call.getString("channel");
|
|
1043
|
+
final Boolean triggerAutoUpdate = call.getBoolean("triggerAutoUpdate", false);
|
|
1044
|
+
|
|
1045
|
+
if (channel == null) {
|
|
1046
|
+
logger.error("setChannel called without channel");
|
|
1047
|
+
JSObject errorObj = new JSObject();
|
|
1048
|
+
errorObj.put("message", "setChannel called without channel");
|
|
1049
|
+
errorObj.put("error", "missing_parameter");
|
|
1050
|
+
call.reject("setChannel called without channel", "SETCHANNEL_INVALID_PARAMS", null, errorObj);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
logger.info("setChannel " + channel + " triggerAutoUpdate: " + triggerAutoUpdate);
|
|
1055
|
+
startNewThread(() ->
|
|
1056
|
+
CapacitorUpdaterPlugin.this.implementation.setChannel(
|
|
1057
|
+
channel,
|
|
1058
|
+
CapacitorUpdaterPlugin.this.editor,
|
|
1059
|
+
DEFAULT_CHANNEL_PREF_KEY,
|
|
1060
|
+
CapacitorUpdaterPlugin.this.allowSetDefaultChannel,
|
|
1061
|
+
(res) -> {
|
|
1062
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1063
|
+
if (jsRes.has("error")) {
|
|
1064
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
1065
|
+
String errorCode = jsRes.getString("error");
|
|
1066
|
+
|
|
1067
|
+
// Fire channelPrivate event if channel doesn't allow self-assignment
|
|
1068
|
+
if (
|
|
1069
|
+
errorCode.contains("cannot_update_via_private_channel") ||
|
|
1070
|
+
errorCode.contains("channel_self_set_not_allowed")
|
|
1071
|
+
) {
|
|
1072
|
+
JSObject eventData = new JSObject();
|
|
1073
|
+
eventData.put("channel", channel);
|
|
1074
|
+
eventData.put("message", errorMessage);
|
|
1075
|
+
notifyListeners("channelPrivate", eventData);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
JSObject errorObj = new JSObject();
|
|
1079
|
+
errorObj.put("message", errorMessage);
|
|
1080
|
+
errorObj.put("error", errorCode);
|
|
1081
|
+
|
|
1082
|
+
call.reject(errorMessage, "SETCHANNEL_FAILED", null, errorObj);
|
|
1083
|
+
} else {
|
|
1084
|
+
if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && Boolean.TRUE.equals(triggerAutoUpdate)) {
|
|
1085
|
+
logger.info("Calling autoupdater after channel change!");
|
|
1086
|
+
// Check if download is already in progress (with timeout protection)
|
|
1087
|
+
if (!this.isDownloadStuckOrTimedOut()) {
|
|
1088
|
+
backgroundDownload();
|
|
1089
|
+
} else {
|
|
1090
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
call.resolve(jsRes);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
)
|
|
1097
|
+
);
|
|
1098
|
+
} catch (final Exception e) {
|
|
1099
|
+
logger.error("Failed to setChannel: " + channel + " " + e.getMessage());
|
|
1100
|
+
call.reject("Failed to setChannel: " + channel, e);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
@PluginMethod
|
|
1105
|
+
public void getChannel(final PluginCall call) {
|
|
1106
|
+
try {
|
|
1107
|
+
logger.info("getChannel");
|
|
1108
|
+
startNewThread(() ->
|
|
1109
|
+
CapacitorUpdaterPlugin.this.implementation.getChannel((res) -> {
|
|
1110
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1111
|
+
if (jsRes.has("error")) {
|
|
1112
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
1113
|
+
String errorCode = jsRes.getString("error");
|
|
1114
|
+
|
|
1115
|
+
JSObject errorObj = new JSObject();
|
|
1116
|
+
errorObj.put("message", errorMessage);
|
|
1117
|
+
errorObj.put("error", errorCode);
|
|
1118
|
+
|
|
1119
|
+
call.reject(errorMessage, "GETCHANNEL_FAILED", null, errorObj);
|
|
1120
|
+
} else {
|
|
1121
|
+
call.resolve(jsRes);
|
|
1122
|
+
}
|
|
1123
|
+
})
|
|
1124
|
+
);
|
|
1125
|
+
} catch (final Exception e) {
|
|
1126
|
+
logger.error("Failed to getChannel " + e.getMessage());
|
|
1127
|
+
call.reject("Failed to getChannel", e);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
@PluginMethod
|
|
1132
|
+
public void listChannels(final PluginCall call) {
|
|
1133
|
+
try {
|
|
1134
|
+
logger.info("listChannels");
|
|
1135
|
+
startNewThread(() ->
|
|
1136
|
+
CapacitorUpdaterPlugin.this.implementation.listChannels((res) -> {
|
|
1137
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1138
|
+
if (jsRes.has("error")) {
|
|
1139
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : jsRes.getString("error");
|
|
1140
|
+
String errorCode = jsRes.getString("error");
|
|
1141
|
+
|
|
1142
|
+
JSObject errorObj = new JSObject();
|
|
1143
|
+
errorObj.put("message", errorMessage);
|
|
1144
|
+
errorObj.put("error", errorCode);
|
|
1145
|
+
|
|
1146
|
+
call.reject(errorMessage, "LISTCHANNELS_FAILED", null, errorObj);
|
|
1147
|
+
} else {
|
|
1148
|
+
call.resolve(jsRes);
|
|
1149
|
+
}
|
|
1150
|
+
})
|
|
1151
|
+
);
|
|
1152
|
+
} catch (final Exception e) {
|
|
1153
|
+
logger.error("Failed to listChannels: " + e.getMessage());
|
|
1154
|
+
call.reject("Failed to listChannels", e);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
@PluginMethod
|
|
1159
|
+
public void download(final PluginCall call) {
|
|
1160
|
+
final String url = call.getString("url");
|
|
1161
|
+
final String version = call.getString("version");
|
|
1162
|
+
final String sessionKey = call.getString("sessionKey", "");
|
|
1163
|
+
final String checksum = call.getString("checksum", "");
|
|
1164
|
+
final JSONArray manifest = call.getData().optJSONArray("manifest");
|
|
1165
|
+
if (url == null) {
|
|
1166
|
+
logger.error("Download called without url");
|
|
1167
|
+
call.reject("Download called without url");
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
if (version == null) {
|
|
1171
|
+
logger.error("Download called without version");
|
|
1172
|
+
call.reject("Download called without version");
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
logger.info("Downloading " + url);
|
|
1177
|
+
startNewThread(() -> {
|
|
1178
|
+
try {
|
|
1179
|
+
final BundleInfo downloaded;
|
|
1180
|
+
if (manifest != null) {
|
|
1181
|
+
// For manifest downloads, we need to handle this asynchronously
|
|
1182
|
+
// to avoid automatically scheduling/applying the downloaded bundle.
|
|
1183
|
+
// Manual download must not schedule/apply the bundle automatically.
|
|
1184
|
+
CapacitorUpdaterPlugin.this.implementation.downloadBackground(url, version, sessionKey, checksum, manifest, false);
|
|
1185
|
+
// Return immediately with a pending status - the actual result will come via listeners
|
|
1186
|
+
final String id = CapacitorUpdaterPlugin.this.implementation.randomString();
|
|
1187
|
+
downloaded = new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis()), "");
|
|
1188
|
+
call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
|
|
1189
|
+
return;
|
|
1190
|
+
} else {
|
|
1191
|
+
downloaded = CapacitorUpdaterPlugin.this.implementation.download(url, version, sessionKey, checksum);
|
|
1192
|
+
}
|
|
1193
|
+
if (downloaded.isErrorStatus()) {
|
|
1194
|
+
throw new RuntimeException("Download failed: " + downloaded.getStatus());
|
|
1195
|
+
} else {
|
|
1196
|
+
call.resolve(InternalUtils.mapToJSObject(downloaded.toJSONMap()));
|
|
1197
|
+
}
|
|
1198
|
+
} catch (final Exception e) {
|
|
1199
|
+
logger.error("Failed to download from: " + url + " " + e.getMessage());
|
|
1200
|
+
call.reject("Failed to download from: " + url, e);
|
|
1201
|
+
final JSObject ret = new JSObject();
|
|
1202
|
+
ret.put("version", version);
|
|
1203
|
+
CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
|
|
1204
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
1205
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
} catch (final Exception e) {
|
|
1209
|
+
logger.error("Failed to download from: " + url + " " + e.getMessage());
|
|
1210
|
+
call.reject("Failed to download from: " + url, e);
|
|
1211
|
+
final JSObject ret = new JSObject();
|
|
1212
|
+
ret.put("version", version);
|
|
1213
|
+
CapacitorUpdaterPlugin.this.notifyListeners("downloadFailed", ret);
|
|
1214
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
1215
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats("download_fail", current.getVersionName());
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private void syncKeepUrlPathFlag(final boolean enabled) {
|
|
1220
|
+
if (this.bridge == null || this.bridge.getWebView() == null) {
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
final String script = enabled
|
|
1224
|
+
? "(function(){try{localStorage.setItem('" +
|
|
1225
|
+
KEEP_URL_FLAG_KEY +
|
|
1226
|
+
"','1');}catch(e){}window.__capgoKeepUrlPathAfterReload=true;var evt;try{evt=new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',{detail:{enabled:true}});}catch(err){evt=document.createEvent('CustomEvent');evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',false,false,{enabled:true});}window.dispatchEvent(evt);})();"
|
|
1227
|
+
: "(function(){try{localStorage.removeItem('" +
|
|
1228
|
+
KEEP_URL_FLAG_KEY +
|
|
1229
|
+
"');}catch(e){}delete window.__capgoKeepUrlPathAfterReload;var evt;try{evt=new CustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',{detail:{enabled:false}});}catch(err){evt=document.createEvent('CustomEvent');evt.initCustomEvent('CapacitorUpdaterKeepUrlPathAfterReload',false,false,{enabled:false});}window.dispatchEvent(evt);})();";
|
|
1230
|
+
this.bridge.getWebView().post(() -> this.bridge.getWebView().evaluateJavascript(script, null));
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
protected boolean _reload() {
|
|
1234
|
+
final String path = this.implementation.getCurrentBundlePath();
|
|
1235
|
+
if (this.keepUrlPathAfterReload) {
|
|
1236
|
+
this.syncKeepUrlPathFlag(true);
|
|
1237
|
+
}
|
|
1238
|
+
this.semaphoreUp();
|
|
1239
|
+
logger.info("Reloading: " + path);
|
|
1240
|
+
|
|
1241
|
+
AtomicReference<URL> url = new AtomicReference<>();
|
|
1242
|
+
if (this.keepUrlPathAfterReload) {
|
|
1243
|
+
try {
|
|
1244
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
1245
|
+
Semaphore mainThreadSemaphore = new Semaphore(0);
|
|
1246
|
+
this.bridge.executeOnMainThread(() -> {
|
|
1247
|
+
try {
|
|
1248
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
1249
|
+
String currentUrl = this.bridge.getWebView().getUrl();
|
|
1250
|
+
if (currentUrl != null) {
|
|
1251
|
+
url.set(new URL(currentUrl));
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
} catch (Exception e) {
|
|
1255
|
+
logger.error("Error executing on main thread " + e.getMessage());
|
|
1256
|
+
}
|
|
1257
|
+
mainThreadSemaphore.release();
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// Add timeout to prevent indefinite blocking
|
|
1261
|
+
if (!mainThreadSemaphore.tryAcquire(10, TimeUnit.SECONDS)) {
|
|
1262
|
+
logger.error("Timeout waiting for main thread operation");
|
|
1263
|
+
}
|
|
1264
|
+
} else {
|
|
1265
|
+
try {
|
|
1266
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
1267
|
+
String currentUrl = this.bridge.getWebView().getUrl();
|
|
1268
|
+
if (currentUrl != null) {
|
|
1269
|
+
url.set(new URL(currentUrl));
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
} catch (Exception e) {
|
|
1273
|
+
logger.error("Error executing on main thread " + e.getMessage());
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
} catch (InterruptedException e) {
|
|
1277
|
+
logger.error("Error waiting for main thread or getting the current URL from webview " + e.getMessage());
|
|
1278
|
+
Thread.currentThread().interrupt(); // Restore interrupted status
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (url.get() != null) {
|
|
1283
|
+
if (this.implementation.isUsingBuiltin()) {
|
|
1284
|
+
this.bridge.getLocalServer().hostAssets(path);
|
|
1285
|
+
} else {
|
|
1286
|
+
this.bridge.getLocalServer().hostFiles(path);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
try {
|
|
1290
|
+
URL finalUrl = null;
|
|
1291
|
+
finalUrl = new URL(this.bridge.getAppUrl());
|
|
1292
|
+
finalUrl = new URL(finalUrl.getProtocol(), finalUrl.getHost(), finalUrl.getPort(), url.get().getPath());
|
|
1293
|
+
URL finalUrl1 = finalUrl;
|
|
1294
|
+
this.bridge.getWebView().post(() -> {
|
|
1295
|
+
this.bridge.getWebView().loadUrl(finalUrl1.toString());
|
|
1296
|
+
if (!this.keepUrlPathAfterReload) {
|
|
1297
|
+
this.bridge.getWebView().clearHistory();
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
} catch (MalformedURLException e) {
|
|
1301
|
+
logger.error("Cannot get finalUrl from capacitor bridge " + e.getMessage());
|
|
1302
|
+
|
|
1303
|
+
if (this.implementation.isUsingBuiltin()) {
|
|
1304
|
+
this.bridge.setServerAssetPath(path);
|
|
1305
|
+
} else {
|
|
1306
|
+
this.bridge.setServerBasePath(path);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
} else {
|
|
1310
|
+
if (this.implementation.isUsingBuiltin()) {
|
|
1311
|
+
this.bridge.setServerAssetPath(path);
|
|
1312
|
+
} else {
|
|
1313
|
+
this.bridge.setServerBasePath(path);
|
|
1314
|
+
}
|
|
1315
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
1316
|
+
this.bridge.getWebView().post(() -> {
|
|
1317
|
+
if (this.bridge.getWebView() != null) {
|
|
1318
|
+
this.bridge.getWebView().loadUrl(this.bridge.getAppUrl());
|
|
1319
|
+
if (!this.keepUrlPathAfterReload) {
|
|
1320
|
+
this.bridge.getWebView().clearHistory();
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
this.checkAppReady();
|
|
1328
|
+
this.notifyListeners("appReloaded", new JSObject());
|
|
1329
|
+
|
|
1330
|
+
// Wait for the reload to complete (until notifyAppReady is called)
|
|
1331
|
+
try {
|
|
1332
|
+
this.semaphoreWait(this.appReadyTimeout);
|
|
1333
|
+
} catch (Exception e) {
|
|
1334
|
+
logger.error("Error waiting for app ready: " + e.getMessage());
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
@PluginMethod
|
|
1342
|
+
public void reload(final PluginCall call) {
|
|
1343
|
+
try {
|
|
1344
|
+
if (this._reload()) {
|
|
1345
|
+
call.resolve();
|
|
1346
|
+
} else {
|
|
1347
|
+
logger.error("Reload failed");
|
|
1348
|
+
call.reject("Reload failed");
|
|
1349
|
+
}
|
|
1350
|
+
} catch (final Exception e) {
|
|
1351
|
+
logger.error("Could not reload " + e.getMessage());
|
|
1352
|
+
call.reject("Could not reload", e);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
@PluginMethod
|
|
1357
|
+
public void next(final PluginCall call) {
|
|
1358
|
+
final String id = call.getString("id");
|
|
1359
|
+
if (id == null) {
|
|
1360
|
+
logger.error("Next called without id");
|
|
1361
|
+
call.reject("Next called without id");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
try {
|
|
1365
|
+
logger.info("Setting next active id " + id);
|
|
1366
|
+
if (!this.implementation.setNextBundle(id)) {
|
|
1367
|
+
logger.error("Set next id failed. Bundle " + id + " does not exist.");
|
|
1368
|
+
call.reject("Set next id failed. Bundle " + id + " does not exist.");
|
|
1369
|
+
} else {
|
|
1370
|
+
call.resolve(InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
|
|
1371
|
+
}
|
|
1372
|
+
} catch (final Exception e) {
|
|
1373
|
+
logger.error("Could not set next id " + id + " " + e.getMessage());
|
|
1374
|
+
call.reject("Could not set next id: " + id, e);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
@PluginMethod
|
|
1379
|
+
public void set(final PluginCall call) {
|
|
1380
|
+
final String id = call.getString("id");
|
|
1381
|
+
if (id == null) {
|
|
1382
|
+
logger.error("Set called without id");
|
|
1383
|
+
call.reject("Set called without id");
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
try {
|
|
1387
|
+
logger.info("Setting active bundle " + id);
|
|
1388
|
+
if (!this.implementation.set(id)) {
|
|
1389
|
+
logger.info("No such bundle " + id);
|
|
1390
|
+
call.reject("Update failed, id " + id + " does not exist.");
|
|
1391
|
+
} else {
|
|
1392
|
+
logger.info("Bundle successfully set to " + id);
|
|
1393
|
+
this.reload(call);
|
|
1394
|
+
}
|
|
1395
|
+
} catch (final Exception e) {
|
|
1396
|
+
logger.error("Could not set id " + id + " " + e.getMessage());
|
|
1397
|
+
call.reject("Could not set id " + id, e);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
@PluginMethod
|
|
1402
|
+
public void delete(final PluginCall call) {
|
|
1403
|
+
final String id = call.getString("id");
|
|
1404
|
+
if (id == null) {
|
|
1405
|
+
logger.error("missing id");
|
|
1406
|
+
call.reject("missing id");
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
logger.info("Deleting id " + id);
|
|
1410
|
+
try {
|
|
1411
|
+
final Boolean res = this.implementation.delete(id);
|
|
1412
|
+
if (res) {
|
|
1413
|
+
call.resolve();
|
|
1414
|
+
} else {
|
|
1415
|
+
logger.error("Delete failed, id " + id + " does not exist");
|
|
1416
|
+
call.reject("Delete failed, id " + id + " does not exist or it cannot be deleted (perhaps it is the 'next' bundle)");
|
|
1417
|
+
}
|
|
1418
|
+
} catch (final Exception e) {
|
|
1419
|
+
logger.error("Could not delete id " + id + " " + e.getMessage());
|
|
1420
|
+
call.reject("Could not delete id " + id, e);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
@PluginMethod
|
|
1425
|
+
public void setBundleError(final PluginCall call) {
|
|
1426
|
+
if (!Boolean.TRUE.equals(this.allowManualBundleError)) {
|
|
1427
|
+
logger.error("setBundleError called without allowManualBundleError");
|
|
1428
|
+
call.reject("setBundleError not allowed. Set allowManualBundleError to true in your config to enable it.");
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
final String id = call.getString("id");
|
|
1432
|
+
if (id == null) {
|
|
1433
|
+
logger.error("setBundleError called without id");
|
|
1434
|
+
call.reject("setBundleError called without id");
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
try {
|
|
1438
|
+
final BundleInfo bundle = this.implementation.getBundleInfo(id);
|
|
1439
|
+
if (bundle == null || bundle.isUnknown()) {
|
|
1440
|
+
logger.error("setBundleError called with unknown bundle " + id);
|
|
1441
|
+
call.reject("Bundle " + id + " does not exist");
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
if (bundle.isBuiltin()) {
|
|
1445
|
+
logger.error("setBundleError called on builtin bundle");
|
|
1446
|
+
call.reject("Cannot set builtin bundle to error state");
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (Boolean.TRUE.equals(this.autoUpdate)) {
|
|
1450
|
+
logger.warn("setBundleError used while autoUpdate is enabled; this method is intended for manual mode");
|
|
1451
|
+
}
|
|
1452
|
+
this.implementation.setError(bundle);
|
|
1453
|
+
final JSObject ret = new JSObject();
|
|
1454
|
+
ret.put("bundle", InternalUtils.mapToJSObject(this.implementation.getBundleInfo(id).toJSONMap()));
|
|
1455
|
+
call.resolve(ret);
|
|
1456
|
+
} catch (final Exception e) {
|
|
1457
|
+
logger.error("Could not set bundle error for id " + id + " " + e.getMessage());
|
|
1458
|
+
call.reject("Could not set bundle error for id " + id, e);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
@PluginMethod
|
|
1463
|
+
public void list(final PluginCall call) {
|
|
1464
|
+
try {
|
|
1465
|
+
final List<BundleInfo> res = this.implementation.list(call.getBoolean("raw", false));
|
|
1466
|
+
final JSObject ret = new JSObject();
|
|
1467
|
+
final JSArray values = new JSArray();
|
|
1468
|
+
for (final BundleInfo bundle : res) {
|
|
1469
|
+
values.put(InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
1470
|
+
}
|
|
1471
|
+
ret.put("bundles", values);
|
|
1472
|
+
call.resolve(ret);
|
|
1473
|
+
} catch (final Exception e) {
|
|
1474
|
+
logger.error("Could not list bundles " + e.getMessage());
|
|
1475
|
+
call.reject("Could not list bundles", e);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
@PluginMethod
|
|
1480
|
+
public void getLatest(final PluginCall call) {
|
|
1481
|
+
final String channel = call.getString("channel");
|
|
1482
|
+
startNewThread(() ->
|
|
1483
|
+
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, channel, (res) -> {
|
|
1484
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1485
|
+
if (jsRes.has("error")) {
|
|
1486
|
+
String error = jsRes.getString("error");
|
|
1487
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
|
|
1488
|
+
logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
|
|
1489
|
+
call.reject(jsRes.getString("error"));
|
|
1490
|
+
return;
|
|
1491
|
+
} else if (jsRes.has("message")) {
|
|
1492
|
+
call.reject(jsRes.getString("message"));
|
|
1493
|
+
return;
|
|
1494
|
+
} else {
|
|
1495
|
+
call.resolve(jsRes);
|
|
1496
|
+
}
|
|
1497
|
+
})
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
private boolean _reset(final Boolean toLastSuccessful) {
|
|
1502
|
+
final BundleInfo fallback = this.implementation.getFallbackBundle();
|
|
1503
|
+
this.implementation.reset();
|
|
1504
|
+
|
|
1505
|
+
if (toLastSuccessful && !fallback.isBuiltin()) {
|
|
1506
|
+
logger.info("Resetting to: " + fallback);
|
|
1507
|
+
return this.implementation.set(fallback) && this._reload();
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
logger.info("Resetting to native.");
|
|
1511
|
+
return this._reload();
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
@PluginMethod
|
|
1515
|
+
public void reset(final PluginCall call) {
|
|
1516
|
+
try {
|
|
1517
|
+
final Boolean toLastSuccessful = call.getBoolean("toLastSuccessful", false);
|
|
1518
|
+
if (this._reset(toLastSuccessful)) {
|
|
1519
|
+
call.resolve();
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
logger.error("Reset failed");
|
|
1523
|
+
call.reject("Reset failed");
|
|
1524
|
+
} catch (final Exception e) {
|
|
1525
|
+
logger.error("Reset failed " + e.getMessage());
|
|
1526
|
+
call.reject("Reset failed", e);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
@PluginMethod
|
|
1531
|
+
public void current(final PluginCall call) {
|
|
1532
|
+
ensureBridgeSet();
|
|
1533
|
+
try {
|
|
1534
|
+
final JSObject ret = new JSObject();
|
|
1535
|
+
final BundleInfo bundle = this.implementation.getCurrentBundle();
|
|
1536
|
+
ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
1537
|
+
ret.put("native", this.currentVersionNative);
|
|
1538
|
+
call.resolve(ret);
|
|
1539
|
+
} catch (final Exception e) {
|
|
1540
|
+
logger.error("Could not get current bundle " + e.getMessage());
|
|
1541
|
+
call.reject("Could not get current bundle", e);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
@PluginMethod
|
|
1546
|
+
public void getNextBundle(final PluginCall call) {
|
|
1547
|
+
try {
|
|
1548
|
+
final BundleInfo bundle = this.implementation.getNextBundle();
|
|
1549
|
+
if (bundle == null) {
|
|
1550
|
+
call.resolve(null);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
call.resolve(InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
1555
|
+
} catch (final Exception e) {
|
|
1556
|
+
logger.error("Could not get next bundle " + e.getMessage());
|
|
1557
|
+
call.reject("Could not get next bundle", e);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
@PluginMethod
|
|
1562
|
+
public void getFailedUpdate(final PluginCall call) {
|
|
1563
|
+
try {
|
|
1564
|
+
final BundleInfo bundle = this.readLastFailedBundle();
|
|
1565
|
+
if (bundle == null || bundle.isUnknown()) {
|
|
1566
|
+
call.resolve(null);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
this.persistLastFailedBundle(null);
|
|
1571
|
+
|
|
1572
|
+
final JSObject ret = new JSObject();
|
|
1573
|
+
ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
1574
|
+
call.resolve(ret);
|
|
1575
|
+
} catch (final Exception e) {
|
|
1576
|
+
logger.error("Could not get failed update " + e.getMessage());
|
|
1577
|
+
call.reject("Could not get failed update", e);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
public void checkForUpdateAfterDelay() {
|
|
1582
|
+
if (this.periodCheckDelay == 0 || !this._isAutoUpdateEnabled()) {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
final Timer timer = new Timer();
|
|
1586
|
+
timer.schedule(
|
|
1587
|
+
new TimerTask() {
|
|
1588
|
+
@Override
|
|
1589
|
+
public void run() {
|
|
1590
|
+
try {
|
|
1591
|
+
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
1592
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1593
|
+
if (jsRes.has("error")) {
|
|
1594
|
+
String error = jsRes.getString("error");
|
|
1595
|
+
String errorMessage = jsRes.has("message")
|
|
1596
|
+
? jsRes.getString("message")
|
|
1597
|
+
: "server did not provide a message";
|
|
1598
|
+
logger.error("getLatest failed with error: " + error + ", message: " + errorMessage);
|
|
1599
|
+
} else if (jsRes.has("version")) {
|
|
1600
|
+
String newVersion = jsRes.getString("version");
|
|
1601
|
+
String currentVersion = String.valueOf(CapacitorUpdaterPlugin.this.implementation.getCurrentBundle());
|
|
1602
|
+
if (!Objects.equals(newVersion, currentVersion)) {
|
|
1603
|
+
logger.info("New version found: " + newVersion);
|
|
1604
|
+
// Check if download is already in progress (with timeout protection)
|
|
1605
|
+
if (!CapacitorUpdaterPlugin.this.isDownloadStuckOrTimedOut()) {
|
|
1606
|
+
CapacitorUpdaterPlugin.this.backgroundDownload();
|
|
1607
|
+
} else {
|
|
1608
|
+
logger.info("Download already in progress, skipping duplicate download request");
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
} catch (final Exception e) {
|
|
1614
|
+
logger.error("Failed to check for update " + e.getMessage());
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
this.periodCheckDelay,
|
|
1619
|
+
this.periodCheckDelay
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
@PluginMethod
|
|
1624
|
+
public void notifyAppReady(final PluginCall call) {
|
|
1625
|
+
ensureBridgeSet();
|
|
1626
|
+
try {
|
|
1627
|
+
final BundleInfo bundle = this.implementation.getCurrentBundle();
|
|
1628
|
+
this.implementation.setSuccess(bundle, this.autoDeletePrevious);
|
|
1629
|
+
logger.info("Current bundle loaded successfully. ['notifyAppReady()' was called] " + bundle);
|
|
1630
|
+
logger.info("semaphoreReady countDown");
|
|
1631
|
+
this.semaphoreDown();
|
|
1632
|
+
logger.info("semaphoreReady countDown done");
|
|
1633
|
+
final JSObject ret = new JSObject();
|
|
1634
|
+
ret.put("bundle", InternalUtils.mapToJSObject(bundle.toJSONMap()));
|
|
1635
|
+
call.resolve(ret);
|
|
1636
|
+
} catch (final Exception e) {
|
|
1637
|
+
logger.error("Failed to notify app ready state. [Error calling 'notifyAppReady()'] " + e.getMessage());
|
|
1638
|
+
call.reject("Failed to commit app ready state.", e);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
@PluginMethod
|
|
1643
|
+
public void setMultiDelay(final PluginCall call) {
|
|
1644
|
+
try {
|
|
1645
|
+
final JSONArray delayConditions = call.getData().optJSONArray("delayConditions");
|
|
1646
|
+
if (delayConditions == null) {
|
|
1647
|
+
logger.error("setMultiDelay called without delayCondition");
|
|
1648
|
+
call.reject("setMultiDelay called without delayCondition");
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
for (int i = 0; i < delayConditions.length(); i++) {
|
|
1652
|
+
final JSONObject object = delayConditions.optJSONObject(i);
|
|
1653
|
+
if (object != null && object.optString("kind").equals("background") && object.optString("value").isEmpty()) {
|
|
1654
|
+
object.put("value", "0");
|
|
1655
|
+
delayConditions.put(i, object);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
if (this.delayUpdateUtils.setMultiDelay(delayConditions.toString())) {
|
|
1660
|
+
call.resolve();
|
|
1661
|
+
} else {
|
|
1662
|
+
call.reject("Failed to delay update");
|
|
1663
|
+
}
|
|
1664
|
+
} catch (final Exception e) {
|
|
1665
|
+
logger.error("Failed to delay update, [Error calling 'setMultiDelay()'] " + e.getMessage());
|
|
1666
|
+
call.reject("Failed to delay update", e);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
@PluginMethod
|
|
1671
|
+
public void cancelDelay(final PluginCall call) {
|
|
1672
|
+
if (this.delayUpdateUtils.cancelDelay("JS")) {
|
|
1673
|
+
call.resolve();
|
|
1674
|
+
} else {
|
|
1675
|
+
call.reject("Failed to cancel delay");
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
private Boolean _isAutoUpdateEnabled() {
|
|
1680
|
+
final CapConfig config = CapConfig.loadDefault(this.getActivity());
|
|
1681
|
+
String serverUrl = config.getServerUrl();
|
|
1682
|
+
if (serverUrl != null && !serverUrl.isEmpty()) {
|
|
1683
|
+
// log warning autoupdate disabled when serverUrl is set
|
|
1684
|
+
logger.warn("AutoUpdate is automatic disabled when serverUrl is set.");
|
|
1685
|
+
}
|
|
1686
|
+
return (
|
|
1687
|
+
CapacitorUpdaterPlugin.this.autoUpdate &&
|
|
1688
|
+
!"".equals(CapacitorUpdaterPlugin.this.updateUrl) &&
|
|
1689
|
+
(serverUrl == null || serverUrl.isEmpty())
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
@PluginMethod
|
|
1694
|
+
public void isAutoUpdateEnabled(final PluginCall call) {
|
|
1695
|
+
try {
|
|
1696
|
+
final JSObject ret = new JSObject();
|
|
1697
|
+
ret.put("enabled", this._isAutoUpdateEnabled());
|
|
1698
|
+
call.resolve(ret);
|
|
1699
|
+
} catch (final Exception e) {
|
|
1700
|
+
logger.error("Could not get autoUpdate status " + e.getMessage());
|
|
1701
|
+
call.reject("Could not get autoUpdate status", e);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
@PluginMethod
|
|
1706
|
+
public void isAutoUpdateAvailable(final PluginCall call) {
|
|
1707
|
+
try {
|
|
1708
|
+
final CapConfig config = CapConfig.loadDefault(this.getActivity());
|
|
1709
|
+
String serverUrl = config.getServerUrl();
|
|
1710
|
+
final JSObject ret = new JSObject();
|
|
1711
|
+
ret.put("available", serverUrl == null || serverUrl.isEmpty());
|
|
1712
|
+
call.resolve(ret);
|
|
1713
|
+
} catch (final Exception e) {
|
|
1714
|
+
logger.error("Could not get autoUpdate availability " + e.getMessage());
|
|
1715
|
+
call.reject("Could not get autoUpdate availability", e);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
private void checkAppReady() {
|
|
1720
|
+
try {
|
|
1721
|
+
if (this.appReadyCheck != null) {
|
|
1722
|
+
this.appReadyCheck.interrupt();
|
|
1723
|
+
}
|
|
1724
|
+
this.appReadyCheck = startNewThread(new DeferredNotifyAppReadyCheck());
|
|
1725
|
+
} catch (final Exception e) {
|
|
1726
|
+
logger.error("Failed to start " + DeferredNotifyAppReadyCheck.class.getName() + " " + e.getMessage());
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
private boolean isValidURL(String urlStr) {
|
|
1731
|
+
try {
|
|
1732
|
+
new URL(urlStr);
|
|
1733
|
+
return true;
|
|
1734
|
+
} catch (MalformedURLException e) {
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
private void ensureBridgeSet() {
|
|
1740
|
+
if (this.bridge != null && this.bridge.getWebView() != null) {
|
|
1741
|
+
logger.setBridge(this.bridge);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
private void endBackGroundTaskWithNotif(String msg, String latestVersionName, BundleInfo current, Boolean error) {
|
|
1746
|
+
endBackGroundTaskWithNotif(msg, latestVersionName, current, error, false, "download_fail", "downloadFailed", true);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
private void endBackGroundTaskWithNotif(
|
|
1750
|
+
String msg,
|
|
1751
|
+
String latestVersionName,
|
|
1752
|
+
BundleInfo current,
|
|
1753
|
+
Boolean error,
|
|
1754
|
+
Boolean isDirectUpdate
|
|
1755
|
+
) {
|
|
1756
|
+
endBackGroundTaskWithNotif(msg, latestVersionName, current, error, isDirectUpdate, "download_fail", "downloadFailed", true);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
private void endBackGroundTaskWithNotif(
|
|
1760
|
+
String msg,
|
|
1761
|
+
String latestVersionName,
|
|
1762
|
+
BundleInfo current,
|
|
1763
|
+
Boolean error,
|
|
1764
|
+
Boolean isDirectUpdate,
|
|
1765
|
+
String failureAction,
|
|
1766
|
+
String failureEvent
|
|
1767
|
+
) {
|
|
1768
|
+
endBackGroundTaskWithNotif(msg, latestVersionName, current, error, isDirectUpdate, failureAction, failureEvent, true);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
private void endBackGroundTaskWithNotif(
|
|
1772
|
+
String msg,
|
|
1773
|
+
String latestVersionName,
|
|
1774
|
+
BundleInfo current,
|
|
1775
|
+
Boolean error,
|
|
1776
|
+
Boolean isDirectUpdate,
|
|
1777
|
+
String failureAction,
|
|
1778
|
+
String failureEvent,
|
|
1779
|
+
boolean shouldSendStats
|
|
1780
|
+
) {
|
|
1781
|
+
if (error) {
|
|
1782
|
+
logger.info(
|
|
1783
|
+
"endBackGroundTaskWithNotif error: " +
|
|
1784
|
+
error +
|
|
1785
|
+
" current: " +
|
|
1786
|
+
current.getVersionName() +
|
|
1787
|
+
"latestVersionName: " +
|
|
1788
|
+
latestVersionName
|
|
1789
|
+
);
|
|
1790
|
+
if (shouldSendStats) {
|
|
1791
|
+
this.implementation.sendStats(failureAction, current.getVersionName());
|
|
1792
|
+
}
|
|
1793
|
+
final JSObject ret = new JSObject();
|
|
1794
|
+
ret.put("version", latestVersionName);
|
|
1795
|
+
this.notifyListeners(failureEvent, ret);
|
|
1796
|
+
}
|
|
1797
|
+
final JSObject ret = new JSObject();
|
|
1798
|
+
ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
1799
|
+
this.notifyListeners("noNeedUpdate", ret);
|
|
1800
|
+
this.sendReadyToJs(current, msg, isDirectUpdate);
|
|
1801
|
+
this.backgroundDownloadTask = null;
|
|
1802
|
+
this.downloadStartTimeMs = 0;
|
|
1803
|
+
logger.info("endBackGroundTaskWithNotif " + msg);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
private boolean isDownloadStuckOrTimedOut() {
|
|
1807
|
+
if (this.backgroundDownloadTask == null || !this.backgroundDownloadTask.isAlive()) {
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// Check if download has timed out
|
|
1812
|
+
if (this.downloadStartTimeMs > 0) {
|
|
1813
|
+
long elapsed = System.currentTimeMillis() - this.downloadStartTimeMs;
|
|
1814
|
+
if (elapsed > DOWNLOAD_TIMEOUT_MS) {
|
|
1815
|
+
logger.warn(
|
|
1816
|
+
"Download has been in progress for " +
|
|
1817
|
+
elapsed +
|
|
1818
|
+
" ms, exceeding timeout of " +
|
|
1819
|
+
DOWNLOAD_TIMEOUT_MS +
|
|
1820
|
+
" ms. Clearing stuck state."
|
|
1821
|
+
);
|
|
1822
|
+
this.backgroundDownloadTask = null;
|
|
1823
|
+
this.downloadStartTimeMs = 0;
|
|
1824
|
+
return false; // Now it's not stuck anymore, caller can proceed
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
return true;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
private Thread backgroundDownload() {
|
|
1832
|
+
final boolean plannedDirectUpdate = this.shouldUseDirectUpdate();
|
|
1833
|
+
final boolean initialDirectUpdateAllowed = this.isDirectUpdateCurrentlyAllowed(plannedDirectUpdate);
|
|
1834
|
+
this.implementation.directUpdate = initialDirectUpdateAllowed;
|
|
1835
|
+
final String messageUpdate = initialDirectUpdateAllowed
|
|
1836
|
+
? "Update will occur now."
|
|
1837
|
+
: "Update will occur next time app moves to background.";
|
|
1838
|
+
Thread newTask = startNewThread(() -> {
|
|
1839
|
+
// Wait for cleanup to complete before starting download
|
|
1840
|
+
waitForCleanupIfNeeded();
|
|
1841
|
+
logger.info("Check for update via: " + CapacitorUpdaterPlugin.this.updateUrl);
|
|
1842
|
+
try {
|
|
1843
|
+
CapacitorUpdaterPlugin.this.implementation.getLatest(CapacitorUpdaterPlugin.this.updateUrl, null, (res) -> {
|
|
1844
|
+
JSObject jsRes = InternalUtils.mapToJSObject(res);
|
|
1845
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
1846
|
+
|
|
1847
|
+
// Handle network errors and other failures first
|
|
1848
|
+
if (jsRes.has("error")) {
|
|
1849
|
+
String error = jsRes.getString("error");
|
|
1850
|
+
String errorMessage = jsRes.has("message") ? jsRes.getString("message") : "server did not provide a message";
|
|
1851
|
+
int statusCode = jsRes.has("statusCode") ? jsRes.optInt("statusCode", 0) : 0;
|
|
1852
|
+
boolean responseIsOk = statusCode >= 200 && statusCode < 300;
|
|
1853
|
+
|
|
1854
|
+
logger.error(
|
|
1855
|
+
"getLatest failed with error: " + error + ", message: " + errorMessage + ", statusCode: " + statusCode
|
|
1856
|
+
);
|
|
1857
|
+
String latestVersion = jsRes.has("version") ? jsRes.getString("version") : current.getVersionName();
|
|
1858
|
+
|
|
1859
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1860
|
+
errorMessage,
|
|
1861
|
+
latestVersion,
|
|
1862
|
+
current,
|
|
1863
|
+
true,
|
|
1864
|
+
plannedDirectUpdate,
|
|
1865
|
+
"download_fail",
|
|
1866
|
+
"downloadFailed",
|
|
1867
|
+
!responseIsOk
|
|
1868
|
+
);
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
try {
|
|
1873
|
+
final String latestVersionName = jsRes.getString("version");
|
|
1874
|
+
|
|
1875
|
+
if ("builtin".equals(latestVersionName)) {
|
|
1876
|
+
logger.info("Latest version is builtin");
|
|
1877
|
+
final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
|
|
1878
|
+
plannedDirectUpdate
|
|
1879
|
+
);
|
|
1880
|
+
if (directUpdateAllowedNow) {
|
|
1881
|
+
logger.info("Direct update to builtin version");
|
|
1882
|
+
this._reset(false);
|
|
1883
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1884
|
+
"Updated to builtin version",
|
|
1885
|
+
latestVersionName,
|
|
1886
|
+
CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
|
|
1887
|
+
false,
|
|
1888
|
+
true
|
|
1889
|
+
);
|
|
1890
|
+
} else {
|
|
1891
|
+
if (plannedDirectUpdate && !directUpdateAllowedNow) {
|
|
1892
|
+
logger.info(
|
|
1893
|
+
"Direct update skipped because splashscreen timeout occurred. Update will be applied later."
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
logger.info("Setting next bundle to builtin");
|
|
1897
|
+
CapacitorUpdaterPlugin.this.implementation.setNextBundle(BundleInfo.ID_BUILTIN);
|
|
1898
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1899
|
+
"Next update will be to builtin version",
|
|
1900
|
+
latestVersionName,
|
|
1901
|
+
current,
|
|
1902
|
+
false
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
if (!jsRes.has("url") || !CapacitorUpdaterPlugin.this.isValidURL(jsRes.getString("url"))) {
|
|
1909
|
+
logger.error("Error no url or wrong format");
|
|
1910
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1911
|
+
"Error no url or wrong format",
|
|
1912
|
+
current.getVersionName(),
|
|
1913
|
+
current,
|
|
1914
|
+
true,
|
|
1915
|
+
plannedDirectUpdate
|
|
1916
|
+
);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
if (
|
|
1921
|
+
latestVersionName != null && !latestVersionName.isEmpty() && !current.getVersionName().equals(latestVersionName)
|
|
1922
|
+
) {
|
|
1923
|
+
final BundleInfo latest = CapacitorUpdaterPlugin.this.implementation.getBundleInfoByName(latestVersionName);
|
|
1924
|
+
if (latest != null) {
|
|
1925
|
+
final JSObject ret = new JSObject();
|
|
1926
|
+
ret.put("bundle", InternalUtils.mapToJSObject(latest.toJSONMap()));
|
|
1927
|
+
if (latest.isErrorStatus()) {
|
|
1928
|
+
logger.error("Latest bundle already exists, and is in error state. Aborting update.");
|
|
1929
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1930
|
+
"Latest bundle already exists, and is in error state. Aborting update.",
|
|
1931
|
+
latestVersionName,
|
|
1932
|
+
current,
|
|
1933
|
+
true,
|
|
1934
|
+
plannedDirectUpdate
|
|
1935
|
+
);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
if (latest.isDownloaded()) {
|
|
1939
|
+
logger.info("Latest bundle already exists and download is NOT required. " + messageUpdate);
|
|
1940
|
+
final boolean directUpdateAllowedNow = CapacitorUpdaterPlugin.this.isDirectUpdateCurrentlyAllowed(
|
|
1941
|
+
plannedDirectUpdate
|
|
1942
|
+
);
|
|
1943
|
+
if (directUpdateAllowedNow) {
|
|
1944
|
+
String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
|
|
1945
|
+
ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(
|
|
1946
|
+
delayUpdatePreferences
|
|
1947
|
+
);
|
|
1948
|
+
if (!delayConditionList.isEmpty()) {
|
|
1949
|
+
logger.info("Update delayed until delay conditions met");
|
|
1950
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1951
|
+
"Update delayed until delay conditions met",
|
|
1952
|
+
latestVersionName,
|
|
1953
|
+
latest,
|
|
1954
|
+
false,
|
|
1955
|
+
plannedDirectUpdate
|
|
1956
|
+
);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
CapacitorUpdaterPlugin.this.implementation.set(latest);
|
|
1960
|
+
CapacitorUpdaterPlugin.this._reload();
|
|
1961
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1962
|
+
"Update installed",
|
|
1963
|
+
latestVersionName,
|
|
1964
|
+
latest,
|
|
1965
|
+
false,
|
|
1966
|
+
true
|
|
1967
|
+
);
|
|
1968
|
+
} else {
|
|
1969
|
+
if (plannedDirectUpdate && !directUpdateAllowedNow) {
|
|
1970
|
+
logger.info(
|
|
1971
|
+
"Direct update skipped because splashscreen timeout occurred. Update will install on next background."
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
CapacitorUpdaterPlugin.this.notifyListeners("updateAvailable", ret);
|
|
1975
|
+
CapacitorUpdaterPlugin.this.implementation.setNextBundle(latest.getId());
|
|
1976
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
1977
|
+
"update downloaded, will install next background",
|
|
1978
|
+
latestVersionName,
|
|
1979
|
+
latest,
|
|
1980
|
+
false
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (latest.isDeleted()) {
|
|
1986
|
+
logger.info("Latest bundle already exists and will be deleted, download will overwrite it.");
|
|
1987
|
+
try {
|
|
1988
|
+
final Boolean deleted = CapacitorUpdaterPlugin.this.implementation.delete(latest.getId(), true);
|
|
1989
|
+
if (deleted) {
|
|
1990
|
+
logger.info("Failed bundle deleted: " + latest.getVersionName());
|
|
1991
|
+
}
|
|
1992
|
+
} catch (final IOException e) {
|
|
1993
|
+
logger.error("Failed to delete failed bundle: " + latest.getVersionName() + " " + e.getMessage());
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
startNewThread(() -> {
|
|
1998
|
+
try {
|
|
1999
|
+
logger.info(
|
|
2000
|
+
"New bundle: " +
|
|
2001
|
+
latestVersionName +
|
|
2002
|
+
" found. Current is: " +
|
|
2003
|
+
current.getVersionName() +
|
|
2004
|
+
". " +
|
|
2005
|
+
messageUpdate
|
|
2006
|
+
);
|
|
2007
|
+
|
|
2008
|
+
final String url = jsRes.getString("url");
|
|
2009
|
+
final String sessionKey = jsRes.has("sessionKey") ? jsRes.getString("sessionKey") : "";
|
|
2010
|
+
final String checksum = jsRes.has("checksum") ? jsRes.getString("checksum") : "";
|
|
2011
|
+
|
|
2012
|
+
if (jsRes.has("manifest")) {
|
|
2013
|
+
// Handle manifest-based download
|
|
2014
|
+
JSONArray manifest = jsRes.getJSONArray("manifest");
|
|
2015
|
+
CapacitorUpdaterPlugin.this.implementation.downloadBackground(
|
|
2016
|
+
url,
|
|
2017
|
+
latestVersionName,
|
|
2018
|
+
sessionKey,
|
|
2019
|
+
checksum,
|
|
2020
|
+
manifest
|
|
2021
|
+
);
|
|
2022
|
+
} else {
|
|
2023
|
+
// Handle single file download (existing code)
|
|
2024
|
+
CapacitorUpdaterPlugin.this.implementation.downloadBackground(
|
|
2025
|
+
url,
|
|
2026
|
+
latestVersionName,
|
|
2027
|
+
sessionKey,
|
|
2028
|
+
checksum,
|
|
2029
|
+
null
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
} catch (final Exception e) {
|
|
2033
|
+
logger.error("error downloading file " + e.getMessage());
|
|
2034
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
2035
|
+
"Error downloading file",
|
|
2036
|
+
latestVersionName,
|
|
2037
|
+
CapacitorUpdaterPlugin.this.implementation.getCurrentBundle(),
|
|
2038
|
+
true,
|
|
2039
|
+
plannedDirectUpdate
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
} else {
|
|
2044
|
+
logger.info("No need to update, " + current.getId() + " is the latest bundle.");
|
|
2045
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif("No need to update", latestVersionName, current, false);
|
|
2046
|
+
}
|
|
2047
|
+
} catch (final Exception e) {
|
|
2048
|
+
logger.error("error in update check " + e.getMessage());
|
|
2049
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
2050
|
+
"Error in update check",
|
|
2051
|
+
current.getVersionName(),
|
|
2052
|
+
current,
|
|
2053
|
+
true,
|
|
2054
|
+
plannedDirectUpdate
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
} catch (final Exception e) {
|
|
2059
|
+
logger.error("getLatest call failed: " + e.getMessage());
|
|
2060
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
2061
|
+
CapacitorUpdaterPlugin.this.endBackGroundTaskWithNotif(
|
|
2062
|
+
"Network connection failed",
|
|
2063
|
+
current.getVersionName(),
|
|
2064
|
+
current,
|
|
2065
|
+
true,
|
|
2066
|
+
plannedDirectUpdate
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
this.backgroundDownloadTask = newTask;
|
|
2071
|
+
this.downloadStartTimeMs = System.currentTimeMillis();
|
|
2072
|
+
return newTask;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
private void installNext() {
|
|
2076
|
+
try {
|
|
2077
|
+
String delayUpdatePreferences = prefs.getString(DelayUpdateUtils.DELAY_CONDITION_PREFERENCES, "[]");
|
|
2078
|
+
ArrayList<DelayCondition> delayConditionList = delayUpdateUtils.parseDelayConditions(delayUpdatePreferences);
|
|
2079
|
+
if (!delayConditionList.isEmpty()) {
|
|
2080
|
+
logger.info("Update delayed until delay conditions met");
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
2084
|
+
final BundleInfo next = this.implementation.getNextBundle();
|
|
2085
|
+
|
|
2086
|
+
if (next != null && !next.isErrorStatus() && !next.getId().equals(current.getId())) {
|
|
2087
|
+
// There is a next bundle waiting for activation
|
|
2088
|
+
logger.debug("Next bundle is: " + next.getVersionName());
|
|
2089
|
+
if (this.implementation.set(next) && this._reload()) {
|
|
2090
|
+
logger.info("Updated to bundle: " + next.getVersionName());
|
|
2091
|
+
this.implementation.setNextBundle(null);
|
|
2092
|
+
} else {
|
|
2093
|
+
logger.error("Update to bundle: " + next.getVersionName() + " Failed!");
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
} catch (final Exception e) {
|
|
2097
|
+
logger.error("Error during onActivityStopped " + e.getMessage());
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
private void checkRevert() {
|
|
2102
|
+
// Automatically roll back to fallback version if notifyAppReady has not been called yet
|
|
2103
|
+
final BundleInfo current = this.implementation.getCurrentBundle();
|
|
2104
|
+
|
|
2105
|
+
if (current.isBuiltin()) {
|
|
2106
|
+
logger.info("Built-in bundle is active. We skip the check for notifyAppReady.");
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
logger.debug("Current bundle is: " + current);
|
|
2110
|
+
|
|
2111
|
+
if (BundleStatus.SUCCESS != current.getStatus()) {
|
|
2112
|
+
logger.error("notifyAppReady was not called, roll back current bundle: " + current.getId());
|
|
2113
|
+
logger.info("Did you forget to call 'notifyAppReady()' in your Capacitor App code?");
|
|
2114
|
+
final JSObject ret = new JSObject();
|
|
2115
|
+
ret.put("bundle", InternalUtils.mapToJSObject(current.toJSONMap()));
|
|
2116
|
+
this.persistLastFailedBundle(current);
|
|
2117
|
+
this.notifyListeners("updateFailed", ret);
|
|
2118
|
+
this.implementation.sendStats("update_fail", current.getVersionName());
|
|
2119
|
+
this.implementation.setError(current);
|
|
2120
|
+
this._reset(true);
|
|
2121
|
+
if (CapacitorUpdaterPlugin.this.autoDeleteFailed && !current.isBuiltin()) {
|
|
2122
|
+
logger.info("Deleting failing bundle: " + current.getVersionName());
|
|
2123
|
+
try {
|
|
2124
|
+
final Boolean res = this.implementation.delete(current.getId(), false);
|
|
2125
|
+
if (res) {
|
|
2126
|
+
logger.info("Failed bundle deleted: " + current.getVersionName());
|
|
2127
|
+
}
|
|
2128
|
+
} catch (final IOException e) {
|
|
2129
|
+
logger.error("Failed to delete failed bundle: " + current.getVersionName() + " " + e.getMessage());
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
} else {
|
|
2133
|
+
logger.info("notifyAppReady was called. This is fine: " + current.getId());
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
private class DeferredNotifyAppReadyCheck implements Runnable {
|
|
2138
|
+
|
|
2139
|
+
@Override
|
|
2140
|
+
public void run() {
|
|
2141
|
+
try {
|
|
2142
|
+
logger.info("Wait for " + CapacitorUpdaterPlugin.this.appReadyTimeout + "ms, then check for notifyAppReady");
|
|
2143
|
+
Thread.sleep(CapacitorUpdaterPlugin.this.appReadyTimeout);
|
|
2144
|
+
CapacitorUpdaterPlugin.this.checkRevert();
|
|
2145
|
+
CapacitorUpdaterPlugin.this.appReadyCheck = null;
|
|
2146
|
+
} catch (final InterruptedException e) {
|
|
2147
|
+
logger.info(DeferredNotifyAppReadyCheck.class.getName() + " was interrupted.");
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
public void appMovedToForeground() {
|
|
2153
|
+
// Ensure activity reference is up-to-date before proceeding
|
|
2154
|
+
// This is critical for callbacks that may be invoked during background operations
|
|
2155
|
+
try {
|
|
2156
|
+
Activity currentActivity = this.getActivity();
|
|
2157
|
+
if (currentActivity != null) {
|
|
2158
|
+
CapacitorUpdaterPlugin.this.implementation.activity = currentActivity;
|
|
2159
|
+
} else {
|
|
2160
|
+
logger.warn("appMovedToForeground: Activity is null, operations may be limited");
|
|
2161
|
+
}
|
|
2162
|
+
} catch (Exception e) {
|
|
2163
|
+
logger.error("appMovedToForeground: Failed to update activity reference: " + e.getMessage());
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
2167
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_foreground", current.getVersionName());
|
|
2168
|
+
this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.FOREGROUND);
|
|
2169
|
+
this.delayUpdateUtils.unsetBackgroundTimestamp();
|
|
2170
|
+
|
|
2171
|
+
if (CapacitorUpdaterPlugin.this._isAutoUpdateEnabled() && !this.isDownloadStuckOrTimedOut()) {
|
|
2172
|
+
this.backgroundDownload();
|
|
2173
|
+
} else {
|
|
2174
|
+
final CapConfig config = CapConfig.loadDefault(this.getActivity());
|
|
2175
|
+
String serverUrl = config.getServerUrl();
|
|
2176
|
+
if (serverUrl != null && !serverUrl.isEmpty()) {
|
|
2177
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats("blocked_by_server_url", current.getVersionName());
|
|
2178
|
+
}
|
|
2179
|
+
logger.info("Auto update is disabled");
|
|
2180
|
+
this.sendReadyToJs(current, "disabled");
|
|
2181
|
+
}
|
|
2182
|
+
this.checkAppReady();
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
public void appMovedToBackground() {
|
|
2186
|
+
// Reset timeout flag at start of each background cycle
|
|
2187
|
+
this.autoSplashscreenTimedOut = false;
|
|
2188
|
+
|
|
2189
|
+
// Ensure activity reference is up-to-date before proceeding
|
|
2190
|
+
try {
|
|
2191
|
+
Activity currentActivity = this.getActivity();
|
|
2192
|
+
if (currentActivity != null) {
|
|
2193
|
+
CapacitorUpdaterPlugin.this.implementation.activity = currentActivity;
|
|
2194
|
+
} else {
|
|
2195
|
+
logger.warn("appMovedToBackground: Activity is null, operations may be limited");
|
|
2196
|
+
}
|
|
2197
|
+
} catch (Exception e) {
|
|
2198
|
+
logger.error("appMovedToBackground: Failed to update activity reference: " + e.getMessage());
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
final BundleInfo current = CapacitorUpdaterPlugin.this.implementation.getCurrentBundle();
|
|
2202
|
+
|
|
2203
|
+
// Show splashscreen FIRST, before any other background work to ensure launcher shows it
|
|
2204
|
+
if (this.autoSplashscreen) {
|
|
2205
|
+
boolean canShowSplashscreen = true;
|
|
2206
|
+
|
|
2207
|
+
if (!this._isAutoUpdateEnabled()) {
|
|
2208
|
+
logger.warn(
|
|
2209
|
+
"autoSplashscreen is enabled but autoUpdate is disabled. Splashscreen will not be shown. Enable autoUpdate or disable autoSplashscreen."
|
|
2210
|
+
);
|
|
2211
|
+
canShowSplashscreen = false;
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
if (!this.shouldUseDirectUpdate()) {
|
|
2215
|
+
if ("false".equals(this.directUpdateMode)) {
|
|
2216
|
+
logger.warn(
|
|
2217
|
+
"autoSplashscreen is enabled but directUpdate is not configured for immediate updates. Set directUpdate to 'always' or disable autoSplashscreen."
|
|
2218
|
+
);
|
|
2219
|
+
} else if ("atInstall".equals(this.directUpdateMode) || "onLaunch".equals(this.directUpdateMode)) {
|
|
2220
|
+
logger.info(
|
|
2221
|
+
"autoSplashscreen is enabled but directUpdate is set to \"" +
|
|
2222
|
+
this.directUpdateMode +
|
|
2223
|
+
"\". This is normal. Skipping autoSplashscreen logic."
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
canShowSplashscreen = false;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
if (canShowSplashscreen) {
|
|
2230
|
+
logger.info("Showing splashscreen for launcher/task switcher");
|
|
2231
|
+
this.showSplashscreen();
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
// Do other background work after splashscreen is shown
|
|
2236
|
+
CapacitorUpdaterPlugin.this.implementation.sendStats("app_moved_to_background", current.getVersionName());
|
|
2237
|
+
logger.info("Checking for pending update");
|
|
2238
|
+
|
|
2239
|
+
try {
|
|
2240
|
+
// We need to set "backgrounded time"
|
|
2241
|
+
this.delayUpdateUtils.setBackgroundTimestamp(System.currentTimeMillis());
|
|
2242
|
+
this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.BACKGROUND);
|
|
2243
|
+
this.installNext();
|
|
2244
|
+
} catch (final Exception e) {
|
|
2245
|
+
logger.error("Error during onActivityStopped " + e.getMessage());
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
/**
|
|
2250
|
+
* Check if the current activity is the main activity.
|
|
2251
|
+
* Used for activity-based foreground/background detection on Android < 14.
|
|
2252
|
+
* On Android 14+, topActivity returns null due to security restrictions, so we use
|
|
2253
|
+
* ProcessLifecycleOwner instead.
|
|
2254
|
+
*/
|
|
2255
|
+
private boolean isMainActivity() {
|
|
2256
|
+
try {
|
|
2257
|
+
Context mContext = this.getContext();
|
|
2258
|
+
android.app.ActivityManager activityManager = (android.app.ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
|
|
2259
|
+
java.util.List<android.app.ActivityManager.AppTask> runningTasks = activityManager.getAppTasks();
|
|
2260
|
+
if (runningTasks.isEmpty()) {
|
|
2261
|
+
return false;
|
|
2262
|
+
}
|
|
2263
|
+
android.app.ActivityManager.RecentTaskInfo runningTask = runningTasks.get(0).getTaskInfo();
|
|
2264
|
+
String className = java.util.Objects.requireNonNull(runningTask.baseIntent.getComponent()).getClassName();
|
|
2265
|
+
if (runningTask.topActivity == null) {
|
|
2266
|
+
return false;
|
|
2267
|
+
}
|
|
2268
|
+
String runningActivity = runningTask.topActivity.getClassName();
|
|
2269
|
+
return className.equals(runningActivity);
|
|
2270
|
+
} catch (NullPointerException e) {
|
|
2271
|
+
return false;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
@Override
|
|
2276
|
+
public void handleOnStart() {
|
|
2277
|
+
try {
|
|
2278
|
+
logger.info("handleOnStart: onActivityStarted " + getActivity().getClass().getName());
|
|
2279
|
+
|
|
2280
|
+
// On Android < 14, use activity lifecycle for foreground detection
|
|
2281
|
+
// On Android 14+, ProcessLifecycleOwner handles this via AppLifecycleObserver
|
|
2282
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
2283
|
+
if (isPreviousMainActivity) {
|
|
2284
|
+
logger.info("handleOnStart: appMovedToForeground (Android <14 path)");
|
|
2285
|
+
this.appMovedToForeground();
|
|
2286
|
+
}
|
|
2287
|
+
isPreviousMainActivity = true;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// Initialize shake menu if enabled and activity is BridgeActivity
|
|
2291
|
+
if (shakeMenuEnabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
|
|
2292
|
+
try {
|
|
2293
|
+
shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
|
|
2294
|
+
logger.info("Shake menu initialized");
|
|
2295
|
+
} catch (Exception e) {
|
|
2296
|
+
logger.error("Failed to initialize shake menu: " + e.getMessage());
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
} catch (Exception e) {
|
|
2300
|
+
logger.error("Failed to run handleOnStart: " + e.getMessage());
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
@Override
|
|
2305
|
+
public void handleOnStop() {
|
|
2306
|
+
try {
|
|
2307
|
+
logger.info("handleOnStop: onActivityStopped");
|
|
2308
|
+
|
|
2309
|
+
// On Android < 14, use activity lifecycle for background detection
|
|
2310
|
+
// On Android 14+, ProcessLifecycleOwner handles this via AppLifecycleObserver
|
|
2311
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
2312
|
+
isPreviousMainActivity = isMainActivity();
|
|
2313
|
+
if (isPreviousMainActivity) {
|
|
2314
|
+
logger.info("handleOnStop: appMovedToBackground (Android <14 path)");
|
|
2315
|
+
this.appMovedToBackground();
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
} catch (Exception e) {
|
|
2319
|
+
logger.error("Failed to run handleOnStop: " + e.getMessage());
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
@Override
|
|
2324
|
+
public void handleOnResume() {
|
|
2325
|
+
try {
|
|
2326
|
+
if (backgroundTask != null && taskRunning) {
|
|
2327
|
+
backgroundTask.interrupt();
|
|
2328
|
+
}
|
|
2329
|
+
this.implementation.activity = getActivity();
|
|
2330
|
+
} catch (Exception e) {
|
|
2331
|
+
logger.error("Failed to run handleOnResume: " + e.getMessage());
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
@Override
|
|
2336
|
+
public void handleOnPause() {
|
|
2337
|
+
try {
|
|
2338
|
+
this.implementation.activity = getActivity();
|
|
2339
|
+
} catch (Exception e) {
|
|
2340
|
+
logger.error("Failed to run handleOnPause: " + e.getMessage());
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
@PluginMethod
|
|
2345
|
+
public void setShakeMenu(final PluginCall call) {
|
|
2346
|
+
final Boolean enabled = call.getBoolean("enabled");
|
|
2347
|
+
if (enabled == null) {
|
|
2348
|
+
logger.error("setShakeMenu called without enabled parameter");
|
|
2349
|
+
call.reject("setShakeMenu called without enabled parameter");
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
this.shakeMenuEnabled = enabled;
|
|
2354
|
+
logger.info("Shake menu " + (enabled ? "enabled" : "disabled"));
|
|
2355
|
+
|
|
2356
|
+
// Manage shake menu instance based on enabled state
|
|
2357
|
+
if (enabled && getActivity() instanceof com.getcapacitor.BridgeActivity && shakeMenu == null) {
|
|
2358
|
+
try {
|
|
2359
|
+
shakeMenu = new ShakeMenu(this, (com.getcapacitor.BridgeActivity) getActivity(), logger);
|
|
2360
|
+
logger.info("Shake menu initialized");
|
|
2361
|
+
} catch (Exception e) {
|
|
2362
|
+
logger.error("Failed to initialize shake menu: " + e.getMessage());
|
|
2363
|
+
}
|
|
2364
|
+
} else if (!enabled && shakeMenu != null) {
|
|
2365
|
+
try {
|
|
2366
|
+
shakeMenu.stop();
|
|
2367
|
+
shakeMenu = null;
|
|
2368
|
+
logger.info("Shake menu stopped");
|
|
2369
|
+
} catch (Exception e) {
|
|
2370
|
+
logger.error("Failed to stop shake menu: " + e.getMessage());
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
call.resolve();
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
@PluginMethod
|
|
2378
|
+
public void isShakeMenuEnabled(final PluginCall call) {
|
|
2379
|
+
try {
|
|
2380
|
+
final JSObject ret = new JSObject();
|
|
2381
|
+
ret.put("enabled", this.shakeMenuEnabled);
|
|
2382
|
+
call.resolve(ret);
|
|
2383
|
+
} catch (final Exception e) {
|
|
2384
|
+
logger.error("Could not get shake menu status " + e.getMessage());
|
|
2385
|
+
call.reject("Could not get shake menu status", e);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
@PluginMethod
|
|
2390
|
+
public void setShakeChannelSelector(final PluginCall call) {
|
|
2391
|
+
final Boolean enabled = call.getBoolean("enabled");
|
|
2392
|
+
if (enabled == null) {
|
|
2393
|
+
logger.error("setShakeChannelSelector called without enabled parameter");
|
|
2394
|
+
call.reject("setShakeChannelSelector called without enabled parameter");
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
this.shakeChannelSelectorEnabled = enabled;
|
|
2399
|
+
logger.info("Shake channel selector " + (enabled ? "enabled" : "disabled"));
|
|
2400
|
+
call.resolve();
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
@PluginMethod
|
|
2404
|
+
public void isShakeChannelSelectorEnabled(final PluginCall call) {
|
|
2405
|
+
try {
|
|
2406
|
+
final JSObject ret = new JSObject();
|
|
2407
|
+
ret.put("enabled", this.shakeChannelSelectorEnabled);
|
|
2408
|
+
call.resolve(ret);
|
|
2409
|
+
} catch (final Exception e) {
|
|
2410
|
+
logger.error("Could not get shake channel selector status " + e.getMessage());
|
|
2411
|
+
call.reject("Could not get shake channel selector status", e);
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
@PluginMethod
|
|
2416
|
+
public void getAppId(final PluginCall call) {
|
|
2417
|
+
try {
|
|
2418
|
+
final JSObject ret = new JSObject();
|
|
2419
|
+
ret.put("appId", this.implementation.appId);
|
|
2420
|
+
call.resolve(ret);
|
|
2421
|
+
} catch (final Exception e) {
|
|
2422
|
+
logger.error("Could not get appId " + e.getMessage());
|
|
2423
|
+
call.reject("Could not get appId", e);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
@PluginMethod
|
|
2428
|
+
public void setAppId(final PluginCall call) {
|
|
2429
|
+
if (!this.getConfig().getBoolean("allowModifyAppId", false)) {
|
|
2430
|
+
logger.error("setAppId not allowed set allowModifyAppId in your config to true to allow it");
|
|
2431
|
+
call.reject("setAppId not allowed");
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
final String appId = call.getString("appId");
|
|
2435
|
+
if (appId == null) {
|
|
2436
|
+
logger.error("setAppId called without appId");
|
|
2437
|
+
call.reject("setAppId called without appId");
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
this.implementation.appId = appId;
|
|
2441
|
+
call.resolve();
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// ============================================================================
|
|
2445
|
+
// Play Store In-App Update Methods
|
|
2446
|
+
// ============================================================================
|
|
2447
|
+
|
|
2448
|
+
// AppUpdateAvailability enum values matching TypeScript definitions
|
|
2449
|
+
private static final int UPDATE_AVAILABILITY_UNKNOWN = 0;
|
|
2450
|
+
private static final int UPDATE_AVAILABILITY_NOT_AVAILABLE = 1;
|
|
2451
|
+
private static final int UPDATE_AVAILABILITY_AVAILABLE = 2;
|
|
2452
|
+
private static final int UPDATE_AVAILABILITY_IN_PROGRESS = 3;
|
|
2453
|
+
|
|
2454
|
+
// AppUpdateResultCode enum values matching TypeScript definitions
|
|
2455
|
+
private static final int RESULT_OK = 0;
|
|
2456
|
+
private static final int RESULT_CANCELED = 1;
|
|
2457
|
+
private static final int RESULT_FAILED = 2;
|
|
2458
|
+
private static final int RESULT_NOT_AVAILABLE = 3;
|
|
2459
|
+
private static final int RESULT_NOT_ALLOWED = 4;
|
|
2460
|
+
private static final int RESULT_INFO_MISSING = 5;
|
|
2461
|
+
|
|
2462
|
+
private AppUpdateManager getAppUpdateManager() {
|
|
2463
|
+
if (appUpdateManager == null) {
|
|
2464
|
+
appUpdateManager = AppUpdateManagerFactory.create(getContext());
|
|
2465
|
+
}
|
|
2466
|
+
return appUpdateManager;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
private int mapUpdateAvailability(int playStoreAvailability) {
|
|
2470
|
+
switch (playStoreAvailability) {
|
|
2471
|
+
case UpdateAvailability.UPDATE_AVAILABLE:
|
|
2472
|
+
return UPDATE_AVAILABILITY_AVAILABLE;
|
|
2473
|
+
case UpdateAvailability.UPDATE_NOT_AVAILABLE:
|
|
2474
|
+
return UPDATE_AVAILABILITY_NOT_AVAILABLE;
|
|
2475
|
+
case UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS:
|
|
2476
|
+
return UPDATE_AVAILABILITY_IN_PROGRESS;
|
|
2477
|
+
default:
|
|
2478
|
+
return UPDATE_AVAILABILITY_UNKNOWN;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
@PluginMethod
|
|
2483
|
+
public void getAppUpdateInfo(final PluginCall call) {
|
|
2484
|
+
logger.info("Getting Play Store update info");
|
|
2485
|
+
|
|
2486
|
+
try {
|
|
2487
|
+
AppUpdateManager manager = getAppUpdateManager();
|
|
2488
|
+
Task<AppUpdateInfo> appUpdateInfoTask = manager.getAppUpdateInfo();
|
|
2489
|
+
|
|
2490
|
+
appUpdateInfoTask
|
|
2491
|
+
.addOnSuccessListener((appUpdateInfo) -> {
|
|
2492
|
+
cachedAppUpdateInfo = appUpdateInfo;
|
|
2493
|
+
|
|
2494
|
+
JSObject result = new JSObject();
|
|
2495
|
+
try {
|
|
2496
|
+
PackageInfo pInfo = getContext().getPackageManager().getPackageInfo(getContext().getPackageName(), 0);
|
|
2497
|
+
result.put("currentVersionName", pInfo.versionName);
|
|
2498
|
+
result.put("currentVersionCode", String.valueOf(pInfo.versionCode));
|
|
2499
|
+
} catch (PackageManager.NameNotFoundException e) {
|
|
2500
|
+
result.put("currentVersionName", "0.0.0");
|
|
2501
|
+
result.put("currentVersionCode", "0");
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
result.put("updateAvailability", mapUpdateAvailability(appUpdateInfo.updateAvailability()));
|
|
2505
|
+
|
|
2506
|
+
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
|
|
2507
|
+
result.put("availableVersionCode", String.valueOf(appUpdateInfo.availableVersionCode()));
|
|
2508
|
+
// Play Store doesn't provide version name, only version code
|
|
2509
|
+
result.put("availableVersionName", String.valueOf(appUpdateInfo.availableVersionCode()));
|
|
2510
|
+
result.put("updatePriority", appUpdateInfo.updatePriority());
|
|
2511
|
+
result.put("immediateUpdateAllowed", appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE));
|
|
2512
|
+
result.put("flexibleUpdateAllowed", appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE));
|
|
2513
|
+
|
|
2514
|
+
Integer stalenessDays = appUpdateInfo.clientVersionStalenessDays();
|
|
2515
|
+
if (stalenessDays != null) {
|
|
2516
|
+
result.put("clientVersionStalenessDays", stalenessDays);
|
|
2517
|
+
}
|
|
2518
|
+
} else {
|
|
2519
|
+
result.put("immediateUpdateAllowed", false);
|
|
2520
|
+
result.put("flexibleUpdateAllowed", false);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
result.put("installStatus", appUpdateInfo.installStatus());
|
|
2524
|
+
|
|
2525
|
+
call.resolve(result);
|
|
2526
|
+
})
|
|
2527
|
+
.addOnFailureListener((e) -> {
|
|
2528
|
+
logger.error("Failed to get app update info: " + e.getMessage());
|
|
2529
|
+
call.reject("Failed to get app update info: " + e.getMessage());
|
|
2530
|
+
});
|
|
2531
|
+
} catch (Exception e) {
|
|
2532
|
+
logger.error("Error getting app update info: " + e.getMessage());
|
|
2533
|
+
call.reject("Error getting app update info: " + e.getMessage());
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
@PluginMethod
|
|
2538
|
+
public void openAppStore(final PluginCall call) {
|
|
2539
|
+
String packageName = call.getString("packageName");
|
|
2540
|
+
if (packageName == null || packageName.isEmpty()) {
|
|
2541
|
+
packageName = getContext().getPackageName();
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
try {
|
|
2545
|
+
// Try to open Play Store app first
|
|
2546
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + packageName));
|
|
2547
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
2548
|
+
getContext().startActivity(intent);
|
|
2549
|
+
call.resolve();
|
|
2550
|
+
} catch (android.content.ActivityNotFoundException e) {
|
|
2551
|
+
// Fall back to browser
|
|
2552
|
+
try {
|
|
2553
|
+
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + packageName));
|
|
2554
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
2555
|
+
getContext().startActivity(intent);
|
|
2556
|
+
call.resolve();
|
|
2557
|
+
} catch (Exception ex) {
|
|
2558
|
+
logger.error("Failed to open Play Store: " + ex.getMessage());
|
|
2559
|
+
call.reject("Failed to open Play Store: " + ex.getMessage());
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
@PluginMethod
|
|
2565
|
+
public void performImmediateUpdate(final PluginCall call) {
|
|
2566
|
+
if (cachedAppUpdateInfo == null) {
|
|
2567
|
+
logger.error("No update info available. Call getAppUpdateInfo first.");
|
|
2568
|
+
JSObject result = new JSObject();
|
|
2569
|
+
result.put("code", RESULT_INFO_MISSING);
|
|
2570
|
+
call.resolve(result);
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
if (cachedAppUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
|
|
2575
|
+
logger.info("No update available");
|
|
2576
|
+
JSObject result = new JSObject();
|
|
2577
|
+
result.put("code", RESULT_NOT_AVAILABLE);
|
|
2578
|
+
call.resolve(result);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
if (!cachedAppUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
|
|
2583
|
+
logger.info("Immediate update not allowed");
|
|
2584
|
+
JSObject result = new JSObject();
|
|
2585
|
+
result.put("code", RESULT_NOT_ALLOWED);
|
|
2586
|
+
call.resolve(result);
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
try {
|
|
2591
|
+
Activity activity = getActivity();
|
|
2592
|
+
if (activity == null) {
|
|
2593
|
+
call.reject("Activity not available");
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// Save the call for later resolution
|
|
2598
|
+
bridge.saveCall(call);
|
|
2599
|
+
|
|
2600
|
+
AppUpdateManager manager = getAppUpdateManager();
|
|
2601
|
+
manager.startUpdateFlowForResult(
|
|
2602
|
+
cachedAppUpdateInfo,
|
|
2603
|
+
activity,
|
|
2604
|
+
AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(),
|
|
2605
|
+
APP_UPDATE_REQUEST_CODE
|
|
2606
|
+
);
|
|
2607
|
+
} catch (Exception e) {
|
|
2608
|
+
logger.error("Failed to start immediate update: " + e.getMessage());
|
|
2609
|
+
JSObject result = new JSObject();
|
|
2610
|
+
result.put("code", RESULT_FAILED);
|
|
2611
|
+
call.resolve(result);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
@PluginMethod
|
|
2616
|
+
public void startFlexibleUpdate(final PluginCall call) {
|
|
2617
|
+
if (cachedAppUpdateInfo == null) {
|
|
2618
|
+
logger.error("No update info available. Call getAppUpdateInfo first.");
|
|
2619
|
+
JSObject result = new JSObject();
|
|
2620
|
+
result.put("code", RESULT_INFO_MISSING);
|
|
2621
|
+
call.resolve(result);
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
if (cachedAppUpdateInfo.updateAvailability() != UpdateAvailability.UPDATE_AVAILABLE) {
|
|
2626
|
+
logger.info("No update available");
|
|
2627
|
+
JSObject result = new JSObject();
|
|
2628
|
+
result.put("code", RESULT_NOT_AVAILABLE);
|
|
2629
|
+
call.resolve(result);
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
if (!cachedAppUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
|
|
2634
|
+
logger.info("Flexible update not allowed");
|
|
2635
|
+
JSObject result = new JSObject();
|
|
2636
|
+
result.put("code", RESULT_NOT_ALLOWED);
|
|
2637
|
+
call.resolve(result);
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
try {
|
|
2642
|
+
Activity activity = getActivity();
|
|
2643
|
+
if (activity == null) {
|
|
2644
|
+
call.reject("Activity not available");
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// Register listener for flexible update state changes
|
|
2649
|
+
AppUpdateManager manager = getAppUpdateManager();
|
|
2650
|
+
|
|
2651
|
+
// Remove any existing listener
|
|
2652
|
+
if (installStateUpdatedListener != null) {
|
|
2653
|
+
manager.unregisterListener(installStateUpdatedListener);
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
installStateUpdatedListener = (state) -> {
|
|
2657
|
+
JSObject eventData = new JSObject();
|
|
2658
|
+
eventData.put("installStatus", state.installStatus());
|
|
2659
|
+
|
|
2660
|
+
if (state.installStatus() == InstallStatus.DOWNLOADING) {
|
|
2661
|
+
eventData.put("bytesDownloaded", state.bytesDownloaded());
|
|
2662
|
+
eventData.put("totalBytesToDownload", state.totalBytesToDownload());
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
notifyListeners("onFlexibleUpdateStateChange", eventData);
|
|
2666
|
+
};
|
|
2667
|
+
|
|
2668
|
+
manager.registerListener(installStateUpdatedListener);
|
|
2669
|
+
|
|
2670
|
+
// Save the call for later resolution
|
|
2671
|
+
bridge.saveCall(call);
|
|
2672
|
+
|
|
2673
|
+
manager.startUpdateFlowForResult(
|
|
2674
|
+
cachedAppUpdateInfo,
|
|
2675
|
+
activity,
|
|
2676
|
+
AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build(),
|
|
2677
|
+
APP_UPDATE_REQUEST_CODE
|
|
2678
|
+
);
|
|
2679
|
+
} catch (Exception e) {
|
|
2680
|
+
logger.error("Failed to start flexible update: " + e.getMessage());
|
|
2681
|
+
JSObject result = new JSObject();
|
|
2682
|
+
result.put("code", RESULT_FAILED);
|
|
2683
|
+
call.resolve(result);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
@PluginMethod
|
|
2688
|
+
public void completeFlexibleUpdate(final PluginCall call) {
|
|
2689
|
+
try {
|
|
2690
|
+
AppUpdateManager manager = getAppUpdateManager();
|
|
2691
|
+
manager
|
|
2692
|
+
.completeUpdate()
|
|
2693
|
+
.addOnSuccessListener((aVoid) -> {
|
|
2694
|
+
// The app will restart, so this may not be called
|
|
2695
|
+
call.resolve();
|
|
2696
|
+
})
|
|
2697
|
+
.addOnFailureListener((e) -> {
|
|
2698
|
+
logger.error("Failed to complete flexible update: " + e.getMessage());
|
|
2699
|
+
call.reject("Failed to complete flexible update: " + e.getMessage());
|
|
2700
|
+
});
|
|
2701
|
+
} catch (Exception e) {
|
|
2702
|
+
logger.error("Error completing flexible update: " + e.getMessage());
|
|
2703
|
+
call.reject("Error completing flexible update: " + e.getMessage());
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
@Override
|
|
2708
|
+
protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {
|
|
2709
|
+
super.handleOnActivityResult(requestCode, resultCode, data);
|
|
2710
|
+
|
|
2711
|
+
if (requestCode == APP_UPDATE_REQUEST_CODE) {
|
|
2712
|
+
PluginCall savedCall = bridge.getSavedCall("com.getcapacitor.PluginCall");
|
|
2713
|
+
if (savedCall == null) {
|
|
2714
|
+
// Try to get any saved call (for backward compatibility)
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
JSObject result = new JSObject();
|
|
2719
|
+
if (resultCode == Activity.RESULT_OK) {
|
|
2720
|
+
result.put("code", RESULT_OK);
|
|
2721
|
+
} else if (resultCode == Activity.RESULT_CANCELED) {
|
|
2722
|
+
result.put("code", RESULT_CANCELED);
|
|
2723
|
+
} else {
|
|
2724
|
+
result.put("code", RESULT_FAILED);
|
|
2725
|
+
}
|
|
2726
|
+
savedCall.resolve(result);
|
|
2727
|
+
bridge.releaseCall(savedCall);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
@Override
|
|
2732
|
+
protected void handleOnDestroy() {
|
|
2733
|
+
// Clean up the install state listener
|
|
2734
|
+
if (installStateUpdatedListener != null && appUpdateManager != null) {
|
|
2735
|
+
try {
|
|
2736
|
+
appUpdateManager.unregisterListener(installStateUpdatedListener);
|
|
2737
|
+
installStateUpdatedListener = null;
|
|
2738
|
+
} catch (Exception e) {
|
|
2739
|
+
logger.error("Failed to unregister install state listener: " + e.getMessage());
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
handleOnDestroyInternal();
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
private void handleOnDestroyInternal() {
|
|
2747
|
+
// Original handleOnDestroy code
|
|
2748
|
+
try {
|
|
2749
|
+
logger.info("onActivityDestroyed " + getActivity().getClass().getName());
|
|
2750
|
+
this.implementation.activity = getActivity();
|
|
2751
|
+
|
|
2752
|
+
// Check for 'kill' delay condition on activity destroy
|
|
2753
|
+
// Note: onDestroy is not reliably called - also check on next app launch
|
|
2754
|
+
this.delayUpdateUtils.checkCancelDelay(DelayUpdateUtils.CancelDelaySource.KILLED);
|
|
2755
|
+
this.delayUpdateUtils.setBackgroundTimestamp(0);
|
|
2756
|
+
|
|
2757
|
+
// Clean up shake menu
|
|
2758
|
+
if (shakeMenu != null) {
|
|
2759
|
+
try {
|
|
2760
|
+
shakeMenu.stop();
|
|
2761
|
+
shakeMenu = null;
|
|
2762
|
+
logger.info("Shake menu cleaned up");
|
|
2763
|
+
} catch (Exception e) {
|
|
2764
|
+
logger.error("Failed to clean up shake menu: " + e.getMessage());
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
// Clean up app lifecycle observer
|
|
2769
|
+
if (appLifecycleObserver != null) {
|
|
2770
|
+
try {
|
|
2771
|
+
appLifecycleObserver.unregister();
|
|
2772
|
+
appLifecycleObserver = null;
|
|
2773
|
+
logger.info("AppLifecycleObserver cleaned up");
|
|
2774
|
+
} catch (Exception e) {
|
|
2775
|
+
logger.error("Failed to clean up AppLifecycleObserver: " + e.getMessage());
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
} catch (Exception e) {
|
|
2779
|
+
logger.error("Failed to run handleOnDestroy: " + e.getMessage());
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
1304
2782
|
}
|