@capgo/capacitor-updater 5.0.0-alpha.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CapgoCapacitorUpdater.podspec +1 -1
  2. package/LICENCE +373 -661
  3. package/README.md +339 -75
  4. package/android/build.gradle +13 -12
  5. package/android/src/main/AndroidManifest.xml +4 -2
  6. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +205 -121
  7. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +32 -24
  8. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +1041 -441
  9. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1217 -536
  10. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +153 -0
  11. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +62 -0
  12. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +14 -0
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +126 -0
  14. package/dist/docs.json +727 -171
  15. package/dist/esm/definitions.d.ts +234 -45
  16. package/dist/esm/definitions.js +5 -0
  17. package/dist/esm/definitions.js.map +1 -1
  18. package/dist/esm/index.d.ts +2 -2
  19. package/dist/esm/index.js +9 -4
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/web.d.ts +12 -6
  22. package/dist/esm/web.js +64 -20
  23. package/dist/esm/web.js.map +1 -1
  24. package/dist/plugin.cjs.js +70 -23
  25. package/dist/plugin.cjs.js.map +1 -1
  26. package/dist/plugin.js +70 -23
  27. package/dist/plugin.js.map +1 -1
  28. package/ios/Plugin/BundleInfo.swift +38 -19
  29. package/ios/Plugin/BundleStatus.swift +11 -4
  30. package/ios/Plugin/CapacitorUpdater.swift +520 -192
  31. package/ios/Plugin/CapacitorUpdaterPlugin.m +8 -1
  32. package/ios/Plugin/CapacitorUpdaterPlugin.swift +447 -190
  33. package/ios/Plugin/CryptoCipher.swift +240 -0
  34. package/ios/Plugin/DelayCondition.swift +74 -0
  35. package/ios/Plugin/DelayUntilNext.swift +30 -0
  36. package/ios/Plugin/UserDefaultsExtension.swift +48 -0
  37. package/package.json +26 -20
  38. package/ios/Plugin/ObjectPreferences.swift +0 -97
@@ -1,18 +1,32 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
1
7
  package ee.forgr.capacitor_updater;
2
8
 
9
+ import android.app.Activity;
10
+ import android.content.BroadcastReceiver;
11
+ import android.content.Context;
12
+ import android.content.Intent;
13
+ import android.content.IntentFilter;
3
14
  import android.content.SharedPreferences;
15
+ import android.os.Build;
16
+ import android.os.Bundle;
17
+ import android.util.Base64;
4
18
  import android.util.Log;
5
-
19
+ import com.android.volley.BuildConfig;
20
+ import com.android.volley.DefaultRetryPolicy;
21
+ import com.android.volley.NetworkResponse;
6
22
  import com.android.volley.Request;
7
23
  import com.android.volley.RequestQueue;
8
24
  import com.android.volley.Response;
9
25
  import com.android.volley.VolleyError;
26
+ import com.android.volley.toolbox.HttpHeaderParser;
10
27
  import com.android.volley.toolbox.JsonObjectRequest;
28
+ import com.getcapacitor.JSObject;
11
29
  import com.getcapacitor.plugin.WebView;
12
-
13
- import org.json.JSONException;
14
- import org.json.JSONObject;
15
-
16
30
  import java.io.BufferedInputStream;
17
31
  import java.io.DataInputStream;
18
32
  import java.io.File;
@@ -22,509 +36,1095 @@ import java.io.FileOutputStream;
22
36
  import java.io.FilenameFilter;
23
37
  import java.io.IOException;
24
38
  import java.io.InputStream;
39
+ import java.io.UnsupportedEncodingException;
25
40
  import java.net.URL;
26
41
  import java.net.URLConnection;
42
+ import java.security.GeneralSecurityException;
43
+ import java.security.PrivateKey;
27
44
  import java.security.SecureRandom;
28
45
  import java.util.ArrayList;
29
46
  import java.util.Date;
47
+ import java.util.Iterator;
30
48
  import java.util.List;
49
+ import java.util.zip.CRC32;
31
50
  import java.util.zip.ZipEntry;
32
51
  import java.util.zip.ZipInputStream;
52
+ import javax.crypto.SecretKey;
53
+ import org.json.JSONException;
54
+ import org.json.JSONObject;
33
55
 
34
56
  interface Callback {
35
- void callback(JSONObject jsonObject);
57
+ void callback(JSObject jsoObject);
36
58
  }
37
59
 
38
60
  public class CapacitorUpdater {
39
- private static final String AB = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
40
- private static final SecureRandom rnd = new SecureRandom();
41
61
 
42
- private static final String INFO_SUFFIX = "_info";
43
-
44
- private static final String FALLBACK_VERSION = "pastVersion";
45
- private static final String NEXT_VERSION = "nextVersion";
46
- private static final String bundleDirectory = "versions";
47
-
48
- public static final String TAG = "Capacitor-updater";
49
- public static final String pluginVersion = "5.0.0-alpha.0";
50
-
51
- public SharedPreferences.Editor editor;
52
- public SharedPreferences prefs;
62
+ private static final String AB =
63
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
64
+ private static final SecureRandom rnd = new SecureRandom();
65
+
66
+ private static final String INFO_SUFFIX = "_info";
67
+
68
+ private static final String FALLBACK_VERSION = "pastVersion";
69
+ private static final String NEXT_VERSION = "nextVersion";
70
+ private static final String bundleDirectory = "versions";
71
+
72
+ public static final String TAG = "Capacitor-updater";
73
+ public static final int timeout = 20000;
74
+
75
+ public SharedPreferences.Editor editor;
76
+ public SharedPreferences prefs;
77
+
78
+ public RequestQueue requestQueue;
79
+
80
+ public File documentsDir;
81
+ public Activity activity;
82
+ public String PLUGIN_VERSION = "";
83
+ public String versionBuild = "";
84
+ public String versionCode = "";
85
+ public String versionOs = "";
86
+
87
+ public String customId = "";
88
+ public String statsUrl = "";
89
+ public String channelUrl = "";
90
+ public String appId = "";
91
+ public String privateKey = "";
92
+ public String deviceID = "";
93
+
94
+ private final FilenameFilter filter = new FilenameFilter() {
95
+ @Override
96
+ public boolean accept(final File f, final String name) {
97
+ // ignore directories generated by mac os x
98
+ return (
99
+ !name.startsWith("__MACOSX") &&
100
+ !name.startsWith(".") &&
101
+ !name.startsWith(".DS_Store")
102
+ );
103
+ }
104
+ };
105
+
106
+ private boolean isProd() {
107
+ return !BuildConfig.DEBUG;
108
+ }
109
+
110
+ private boolean isEmulator() {
111
+ return (
112
+ (
113
+ Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
114
+ ) ||
115
+ Build.FINGERPRINT.startsWith("generic") ||
116
+ Build.FINGERPRINT.startsWith("unknown") ||
117
+ Build.HARDWARE.contains("goldfish") ||
118
+ Build.HARDWARE.contains("ranchu") ||
119
+ Build.MODEL.contains("google_sdk") ||
120
+ Build.MODEL.contains("Emulator") ||
121
+ Build.MODEL.contains("Android SDK built for x86") ||
122
+ Build.MANUFACTURER.contains("Genymotion") ||
123
+ Build.PRODUCT.contains("sdk_google") ||
124
+ Build.PRODUCT.contains("google_sdk") ||
125
+ Build.PRODUCT.contains("sdk") ||
126
+ Build.PRODUCT.contains("sdk_x86") ||
127
+ Build.PRODUCT.contains("sdk_gphone64_arm64") ||
128
+ Build.PRODUCT.contains("vbox86p") ||
129
+ Build.PRODUCT.contains("emulator") ||
130
+ Build.PRODUCT.contains("simulator")
131
+ );
132
+ }
133
+
134
+ private int calcTotalPercent(
135
+ final int percent,
136
+ final int min,
137
+ final int max
138
+ ) {
139
+ return (percent * (max - min)) / 100 + min;
140
+ }
141
+
142
+ void notifyDownload(final String id, final int percent) {
143
+ return;
144
+ }
145
+
146
+ void notifyListeners(final String id, final JSObject res) {
147
+ return;
148
+ }
149
+
150
+ private String randomString(final int len) {
151
+ final StringBuilder sb = new StringBuilder(len);
152
+ for (int i = 0; i < len; i++) sb.append(
153
+ AB.charAt(rnd.nextInt(AB.length()))
154
+ );
155
+ return sb.toString();
156
+ }
157
+
158
+ private File unzip(final String id, final File zipFile, final String dest)
159
+ throws IOException {
160
+ final File targetDirectory = new File(this.documentsDir, dest);
161
+ final ZipInputStream zis = new ZipInputStream(
162
+ new BufferedInputStream(new FileInputStream(zipFile))
163
+ );
164
+ try {
165
+ int count;
166
+ final int bufferSize = 8192;
167
+ final byte[] buffer = new byte[bufferSize];
168
+ final long lengthTotal = zipFile.length();
169
+ long lengthRead = bufferSize;
170
+ int percent = 0;
171
+ this.notifyDownload(id, 75);
172
+
173
+ ZipEntry entry;
174
+ while ((entry = zis.getNextEntry()) != null) {
175
+ if (entry.getName().contains("\\")) {
176
+ Log.e(
177
+ TAG,
178
+ "unzip: Windows path is not supported, please use unix path as require by zip RFC: " +
179
+ entry.getName()
180
+ );
181
+ }
182
+ final File file = new File(targetDirectory, entry.getName());
183
+ final String canonicalPath = file.getCanonicalPath();
184
+ final String canonicalDir = targetDirectory.getCanonicalPath();
185
+ final File dir = entry.isDirectory() ? file : file.getParentFile();
186
+
187
+ if (!canonicalPath.startsWith(canonicalDir)) {
188
+ throw new FileNotFoundException(
189
+ "SecurityException, Failed to ensure directory is the start path : " +
190
+ canonicalDir +
191
+ " of " +
192
+ canonicalPath
193
+ );
194
+ }
53
195
 
54
- public RequestQueue requestQueue;
196
+ if (!dir.isDirectory() && !dir.mkdirs()) {
197
+ throw new FileNotFoundException(
198
+ "Failed to ensure directory: " + dir.getAbsolutePath()
199
+ );
200
+ }
55
201
 
56
- public File documentsDir;
57
- public String versionBuild = "";
58
- public String versionCode = "";
59
- public String versionOs = "";
202
+ if (entry.isDirectory()) {
203
+ continue;
204
+ }
60
205
 
61
- public String statsUrl = "";
62
- public String appId = "";
63
- public String deviceID = "";
206
+ try (final FileOutputStream outputStream = new FileOutputStream(file)) {
207
+ while ((count = zis.read(buffer)) != -1) outputStream.write(
208
+ buffer,
209
+ 0,
210
+ count
211
+ );
212
+ }
64
213
 
65
- private final FilenameFilter filter = new FilenameFilter() {
66
- @Override
67
- public boolean accept(final File f, final String name) {
68
- // ignore directories generated by mac os x
69
- return !name.startsWith("__MACOSX") && !name.startsWith(".") && !name.startsWith(".DS_Store");
214
+ final int newPercent = (int) ((lengthRead / (float) lengthTotal) * 100);
215
+ if (lengthTotal > 1 && newPercent != percent) {
216
+ percent = newPercent;
217
+ this.notifyDownload(id, this.calcTotalPercent(percent, 75, 90));
70
218
  }
71
- };
72
219
 
73
- private int calcTotalPercent(final int percent, final int min, final int max) {
74
- return (percent * (max - min)) / 100 + min;
220
+ lengthRead += entry.getCompressedSize();
221
+ }
222
+ return targetDirectory;
223
+ } finally {
224
+ try {
225
+ zis.close();
226
+ } catch (final IOException e) {
227
+ Log.e(TAG, "Failed to close zip input stream", e);
228
+ }
75
229
  }
76
-
77
- void notifyDownload(final String id, final int percent) {
78
- return;
230
+ }
231
+
232
+ private void flattenAssets(final File sourceFile, final String dest)
233
+ throws IOException {
234
+ if (!sourceFile.exists()) {
235
+ throw new FileNotFoundException(
236
+ "Source file not found: " + sourceFile.getPath()
237
+ );
79
238
  }
80
-
81
- private String randomString(final int len){
82
- final StringBuilder sb = new StringBuilder(len);
83
- for(int i = 0; i < len; i++)
84
- sb.append(AB.charAt(rnd.nextInt(AB.length())));
85
- return sb.toString();
239
+ final File destinationFile = new File(this.documentsDir, dest);
240
+ destinationFile.getParentFile().mkdirs();
241
+ final String[] entries = sourceFile.list(this.filter);
242
+ if (entries == null || entries.length == 0) {
243
+ throw new IOException(
244
+ "Source file was not a directory or was empty: " + sourceFile.getPath()
245
+ );
86
246
  }
87
-
88
- private File unzip(final String id, final File zipFile, final String dest) throws IOException {
89
- final File targetDirectory = new File(this.documentsDir, dest);
90
- final ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream(zipFile)));
91
- try {
92
- int count;
93
- final int bufferSize = 8192;
94
- final byte[] buffer = new byte[bufferSize];
95
- final long lengthTotal = zipFile.length();
96
- long lengthRead = bufferSize;
97
- int percent = 0;
98
- this.notifyDownload(id, 75);
99
-
100
- ZipEntry entry;
101
- while ((entry = zis.getNextEntry()) != null) {
102
- final File file = new File(targetDirectory, entry.getName());
103
- final String canonicalPath = file.getCanonicalPath();
104
- final String canonicalDir = (new File(String.valueOf(targetDirectory))).getCanonicalPath();
105
- final File dir = entry.isDirectory() ? file : file.getParentFile();
106
-
107
- if (!canonicalPath.startsWith(canonicalDir)) {
108
- throw new FileNotFoundException("SecurityException, Failed to ensure directory is the start path : " +
109
- canonicalDir + " of " + canonicalPath);
110
- }
111
-
112
- if (!dir.isDirectory() && !dir.mkdirs()) {
113
- throw new FileNotFoundException("Failed to ensure directory: " +
114
- dir.getAbsolutePath());
115
- }
116
-
117
- if (entry.isDirectory()) {
118
- continue;
119
- }
120
-
121
- try(final FileOutputStream outputStream = new FileOutputStream(file)) {
122
- while ((count = zis.read(buffer)) != -1)
123
- outputStream.write(buffer, 0, count);
124
- }
125
-
126
- final int newPercent = (int)((lengthRead * 100) / lengthTotal);
127
- if (lengthTotal > 1 && newPercent != percent) {
128
- percent = newPercent;
129
- this.notifyDownload(id, this.calcTotalPercent(percent, 75, 90));
130
- }
131
-
132
- lengthRead += entry.getCompressedSize();
133
- }
134
- return targetDirectory;
135
- } finally {
136
- try {
137
- zis.close();
138
- } catch (final IOException e) {
139
- Log.e(TAG, "Failed to close zip input stream", e);
140
- }
141
- }
247
+ if (entries.length == 1 && !"index.html".equals(entries[0])) {
248
+ final File child = new File(sourceFile, entries[0]);
249
+ child.renameTo(destinationFile);
250
+ } else {
251
+ sourceFile.renameTo(destinationFile);
142
252
  }
143
-
144
- private void flattenAssets(final File sourceFile, final String dest) throws IOException {
145
- if (!sourceFile.exists()) {
146
- throw new FileNotFoundException("Source file not found: " + sourceFile.getPath());
147
- }
148
- final File destinationFile = new File(this.documentsDir, dest);
149
- destinationFile.getParentFile().mkdirs();
150
- final String[] entries = sourceFile.list(this.filter);
151
- if (entries == null || entries.length == 0) {
152
- throw new IOException("Source file was not a directory or was empty: " + sourceFile.getPath());
153
- }
154
- if (entries.length == 1 && !"index.html".equals(entries[0])) {
155
- final File child = new File(sourceFile, entries[0]);
156
- child.renameTo(destinationFile);
253
+ sourceFile.delete();
254
+ }
255
+
256
+ public void onResume() {
257
+ this.activity.registerReceiver(
258
+ receiver,
259
+ new IntentFilter(DownloadService.NOTIFICATION)
260
+ );
261
+ }
262
+
263
+ public void onPause() {
264
+ this.activity.unregisterReceiver(receiver);
265
+ }
266
+
267
+ private BroadcastReceiver receiver = new BroadcastReceiver() {
268
+ @Override
269
+ public void onReceive(Context context, Intent intent) {
270
+ String action = intent.getAction();
271
+ Bundle bundle = intent.getExtras();
272
+ if (bundle != null) {
273
+ if (action == DownloadService.PERCENTDOWNLOAD) {
274
+ String id = bundle.getString(DownloadService.ID);
275
+ int percent = bundle.getInt(DownloadService.PERCENT);
276
+ CapacitorUpdater.this.notifyDownload(id, percent);
277
+ } else if (action == DownloadService.NOTIFICATION) {
278
+ String id = bundle.getString(DownloadService.ID);
279
+ String dest = bundle.getString(DownloadService.FILEDEST);
280
+ String version = bundle.getString(DownloadService.VERSION);
281
+ String sessionKey = bundle.getString(DownloadService.SESSIONKEY);
282
+ String checksum = bundle.getString(DownloadService.CHECKSUM);
283
+ Log.i(
284
+ CapacitorUpdater.TAG,
285
+ "res " +
286
+ id +
287
+ " " +
288
+ dest +
289
+ " " +
290
+ version +
291
+ " " +
292
+ sessionKey +
293
+ " " +
294
+ checksum
295
+ );
296
+ if (dest == null) {
297
+ final JSObject ret = new JSObject();
298
+ ret.put(
299
+ "version",
300
+ CapacitorUpdater.this.getCurrentBundle().getVersionName()
301
+ );
302
+ CapacitorUpdater.this.notifyListeners("downloadFailed", ret);
303
+ CapacitorUpdater.this.sendStats(
304
+ "download_fail",
305
+ CapacitorUpdater.this.getCurrentBundle().getVersionName()
306
+ );
307
+ return;
308
+ }
309
+ CapacitorUpdater.this.finishDownload(
310
+ id,
311
+ dest,
312
+ version,
313
+ sessionKey,
314
+ checksum,
315
+ true
316
+ );
157
317
  } else {
158
- sourceFile.renameTo(destinationFile);
159
- }
160
- sourceFile.delete();
161
- }
162
-
163
- private File downloadFile(final String id, final String url, final String dest) throws IOException {
164
-
165
- final URL u = new URL(url);
166
- final URLConnection connection = u.openConnection();
167
- final InputStream is = u.openStream();
168
- final DataInputStream dis = new DataInputStream(is);
169
-
170
- final File target = new File(this.documentsDir, dest);
171
- target.getParentFile().mkdirs();
172
- target.createNewFile();
173
- final FileOutputStream fos = new FileOutputStream(target);
174
-
175
- final long totalLength = connection.getContentLength();
176
- final int bufferSize = 1024;
177
- final byte[] buffer = new byte[bufferSize];
178
- int length;
179
-
180
- int bytesRead = bufferSize;
181
- int percent = 0;
182
- this.notifyDownload(id, 10);
183
- while ((length = dis.read(buffer))>0) {
184
- fos.write(buffer, 0, length);
185
- final int newPercent = (int)((bytesRead * 100) / totalLength);
186
- if (totalLength > 1 && newPercent != percent) {
187
- percent = newPercent;
188
- this.notifyDownload(id, this.calcTotalPercent(percent, 10, 70));
189
- }
190
- bytesRead += length;
318
+ Log.i(TAG, "Unknown action " + action);
191
319
  }
192
- return target;
320
+ }
193
321
  }
194
-
195
- private void deleteDirectory(final File file) throws IOException {
196
- if (file.isDirectory()) {
197
- final File[] entries = file.listFiles();
198
- if (entries != null) {
199
- for (final File entry : entries) {
200
- this.deleteDirectory(entry);
201
- }
202
- }
203
- }
204
- if (!file.delete()) {
205
- throw new IOException("Failed to delete: " + file);
322
+ };
323
+
324
+ public void finishDownload(
325
+ String id,
326
+ String dest,
327
+ String version,
328
+ String sessionKey,
329
+ String checksumRes,
330
+ Boolean setNext
331
+ ) {
332
+ try {
333
+ final File downloaded = new File(this.documentsDir, dest);
334
+ this.decryptFile(downloaded, sessionKey, version);
335
+ final String checksum;
336
+ checksum = this.getChecksum(downloaded);
337
+ this.notifyDownload(id, 71);
338
+ final File unzipped = this.unzip(id, downloaded, this.randomString(10));
339
+ downloaded.delete();
340
+ this.notifyDownload(id, 91);
341
+ final String idName = bundleDirectory + "/" + id;
342
+ this.flattenAssets(unzipped, idName);
343
+ this.notifyDownload(id, 100);
344
+ this.saveBundleInfo(id, null);
345
+ BundleInfo next = new BundleInfo(
346
+ id,
347
+ version,
348
+ BundleStatus.PENDING,
349
+ new Date(System.currentTimeMillis()),
350
+ checksum
351
+ );
352
+ this.saveBundleInfo(id, next);
353
+ if (
354
+ checksumRes != null &&
355
+ !checksumRes.isEmpty() &&
356
+ !checksumRes.equals(checksum)
357
+ ) {
358
+ Log.e(
359
+ CapacitorUpdater.TAG,
360
+ "Error checksum " + next.getChecksum() + " " + checksum
361
+ );
362
+ this.sendStats("checksum_fail", getCurrentBundle().getVersionName());
363
+ final Boolean res = this.delete(id);
364
+ if (res) {
365
+ Log.i(
366
+ CapacitorUpdater.TAG,
367
+ "Failed bundle deleted: " + next.getVersionName()
368
+ );
206
369
  }
370
+ throw new IOException("Checksum failed: " + id);
371
+ }
372
+ final JSObject ret = new JSObject();
373
+ ret.put("bundle", next.toJSON());
374
+ CapacitorUpdater.this.notifyListeners("updateAvailable", ret);
375
+ if (setNext) {
376
+ this.setNextBundle(next.getId());
377
+ }
378
+ } catch (IOException e) {
379
+ e.printStackTrace();
380
+ final JSObject ret = new JSObject();
381
+ ret.put(
382
+ "version",
383
+ CapacitorUpdater.this.getCurrentBundle().getVersionName()
384
+ );
385
+ CapacitorUpdater.this.notifyListeners("downloadFailed", ret);
386
+ CapacitorUpdater.this.sendStats(
387
+ "download_fail",
388
+ CapacitorUpdater.this.getCurrentBundle().getVersionName()
389
+ );
207
390
  }
208
-
209
- private void setCurrentBundle(final File bundle) {
210
- this.editor.putString(WebView.CAP_SERVER_PATH, bundle.getPath());
211
- Log.i(TAG, "Current bundle set to: " + bundle);
212
- this.editor.commit();
213
- }
214
-
215
- public BundleInfo download(final String url, final String version) throws IOException {
216
- final String id = this.randomString(10);
217
- this.saveBundleInfo(id, new BundleInfo(id, version, BundleStatus.DOWNLOADING, new Date(System.currentTimeMillis())));
218
- this.notifyDownload(id, 0);
219
- final String idName = bundleDirectory + "/" + id;
220
- this.notifyDownload(id, 5);
221
- final File downloaded = this.downloadFile(id, url, this.randomString(10));
222
- this.notifyDownload(id, 71);
223
- final File unzipped = this.unzip(id, downloaded, this.randomString(10));
224
- downloaded.delete();
225
- this.notifyDownload(id, 91);
226
- this.flattenAssets(unzipped, idName);
227
- this.notifyDownload(id, 100);
228
- this.saveBundleInfo(id, null);
229
- BundleInfo info = new BundleInfo(id, version, BundleStatus.PENDING, new Date(System.currentTimeMillis()));
230
- this.saveBundleInfo(id, info);
231
- return info;
232
- }
233
-
234
- public List<BundleInfo> list() {
235
- final List<BundleInfo> res = new ArrayList<>();
236
- final File destHot = new File(this.documentsDir, bundleDirectory);
237
- Log.d(TAG, "list File : " + destHot.getPath());
238
- if (destHot.exists()) {
239
- for (final File i : destHot.listFiles()) {
240
- final String id = i.getName();
241
- res.add(this.getBundleInfo(id));
242
- }
243
- } else {
244
- Log.i(TAG, "No versions available to list" + destHot);
245
- }
246
- return res;
391
+ }
392
+
393
+ private void downloadFileBackground(
394
+ final String id,
395
+ final String url,
396
+ final String version,
397
+ final String sessionKey,
398
+ final String checksum,
399
+ final String dest
400
+ ) {
401
+ Intent intent = new Intent(this.activity, DownloadService.class);
402
+ intent.putExtra(DownloadService.URL, url);
403
+ intent.putExtra(DownloadService.FILEDEST, dest);
404
+ intent.putExtra(
405
+ DownloadService.DOCDIR,
406
+ this.documentsDir.getAbsolutePath()
407
+ );
408
+ intent.putExtra(DownloadService.ID, id);
409
+ intent.putExtra(DownloadService.VERSION, version);
410
+ intent.putExtra(DownloadService.SESSIONKEY, sessionKey);
411
+ intent.putExtra(DownloadService.CHECKSUM, checksum);
412
+ this.activity.startService(intent);
413
+ }
414
+
415
+ private File downloadFile(
416
+ final String id,
417
+ final String url,
418
+ final String dest
419
+ ) throws IOException {
420
+ final URL u = new URL(url);
421
+ final URLConnection connection = u.openConnection();
422
+ final InputStream is = u.openStream();
423
+ final DataInputStream dis = new DataInputStream(is);
424
+
425
+ final File target = new File(this.documentsDir, dest);
426
+ target.getParentFile().mkdirs();
427
+ target.createNewFile();
428
+ final FileOutputStream fos = new FileOutputStream(target);
429
+
430
+ final long totalLength = connection.getContentLength();
431
+ final int bufferSize = 1024;
432
+ final byte[] buffer = new byte[bufferSize];
433
+ int length;
434
+
435
+ int bytesRead = bufferSize;
436
+ int percent = 0;
437
+ this.notifyDownload(id, 10);
438
+ while ((length = dis.read(buffer)) > 0) {
439
+ fos.write(buffer, 0, length);
440
+ final int newPercent = (int) ((bytesRead / (float) totalLength) * 100);
441
+ if (totalLength > 1 && newPercent != percent) {
442
+ percent = newPercent;
443
+ this.notifyDownload(id, this.calcTotalPercent(percent, 10, 70));
444
+ }
445
+ bytesRead += length;
247
446
  }
248
-
249
- public Boolean delete(final String id) throws IOException {
250
- final BundleInfo deleted = this.getBundleInfo(id);
251
- final File bundle = new File(this.documentsDir, bundleDirectory + "/" + id);
252
- if (bundle.exists()) {
253
- this.deleteDirectory(bundle);
254
- this.removeBundleInfo(id);
255
- return true;
447
+ return target;
448
+ }
449
+
450
+ private void deleteDirectory(final File file) throws IOException {
451
+ if (file.isDirectory()) {
452
+ final File[] entries = file.listFiles();
453
+ if (entries != null) {
454
+ for (final File entry : entries) {
455
+ this.deleteDirectory(entry);
256
456
  }
257
- Log.e(TAG, "Directory not removed: " + bundle.getPath());
258
- this.sendStats("delete", deleted);
259
- return false;
457
+ }
260
458
  }
261
-
262
- private File getBundleDirectory(final String id) {
263
- return new File(this.documentsDir, bundleDirectory + "/" + id);
459
+ if (!file.delete()) {
460
+ throw new IOException("Failed to delete: " + file);
264
461
  }
265
-
266
- private boolean bundleExists(final File bundle) {
267
- if(bundle == null || !bundle.exists()) {
268
- return false;
269
- }
270
-
271
- return new File(bundle.getPath(), "/index.html").exists();
462
+ }
463
+
464
+ private void setCurrentBundle(final File bundle) {
465
+ this.editor.putString(WebView.CAP_SERVER_PATH, bundle.getPath());
466
+ Log.i(TAG, "Current bundle set to: " + bundle);
467
+ this.editor.commit();
468
+ }
469
+
470
+ private String getChecksum(File file) throws IOException {
471
+ byte[] bytes = new byte[(int) file.length()];
472
+ try (FileInputStream fis = new FileInputStream(file)) {
473
+ fis.read(bytes);
272
474
  }
273
-
274
- public Boolean set(final BundleInfo bundle) {
275
- return this.set(bundle.getId());
475
+ CRC32 crc = new CRC32();
476
+ crc.update(bytes);
477
+ String enc = String.format("%08X", crc.getValue());
478
+ return enc.toLowerCase();
479
+ }
480
+
481
+ private void decryptFile(
482
+ final File file,
483
+ final String ivSessionKey,
484
+ final String version
485
+ ) throws IOException {
486
+ // (str != null && !str.isEmpty())
487
+ if (
488
+ this.privateKey == null ||
489
+ this.privateKey.isEmpty() ||
490
+ ivSessionKey == null ||
491
+ ivSessionKey.isEmpty()
492
+ ) {
493
+ return;
276
494
  }
277
-
278
- public Boolean set(final String id) {
279
-
280
- final BundleInfo existing = this.getBundleInfo(id);
281
- final File bundle = this.getBundleDirectory(id);
282
-
283
- Log.i(TAG, "Setting next active bundle: " + existing);
284
- if (this.bundleExists(bundle)) {
285
- this.setCurrentBundle(bundle);
286
- this.setBundleStatus(id, BundleStatus.PENDING);
287
- this.sendStats("set", existing);
288
- return true;
289
- }
290
- this.sendStats("set_fail", existing);
291
- return false;
495
+ try {
496
+ String ivB64 = ivSessionKey.split(":")[0];
497
+ String sessionKeyB64 = ivSessionKey.split(":")[1];
498
+ byte[] iv = Base64.decode(ivB64.getBytes(), Base64.DEFAULT);
499
+ byte[] sessionKey = Base64.decode(
500
+ sessionKeyB64.getBytes(),
501
+ Base64.DEFAULT
502
+ );
503
+ PrivateKey pKey = CryptoCipher.stringToPrivateKey(this.privateKey);
504
+ byte[] decryptedSessionKey = CryptoCipher.decryptRSA(sessionKey, pKey);
505
+ SecretKey sKey = CryptoCipher.byteToSessionKey(decryptedSessionKey);
506
+ byte[] content = new byte[(int) file.length()];
507
+ BufferedInputStream bis = new BufferedInputStream(
508
+ new FileInputStream(file)
509
+ );
510
+ DataInputStream dis = new DataInputStream(bis);
511
+ dis.readFully(content);
512
+ dis.close();
513
+ byte[] decrypted = CryptoCipher.decryptAES(content, sKey, iv);
514
+ // write the decrypted string to the file
515
+ FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
516
+ fos.write(decrypted);
517
+ fos.close();
518
+ } catch (GeneralSecurityException e) {
519
+ Log.i(TAG, "decryptFile fail");
520
+ this.sendStats("decrypt_fail", version);
521
+ e.printStackTrace();
522
+ throw new IOException("GeneralSecurityException");
292
523
  }
293
-
294
- public void commit(final BundleInfo bundle) {
295
- this.setBundleStatus(bundle.getId(), BundleStatus.SUCCESS);
296
- this.setFallbackVersion(bundle);
524
+ }
525
+
526
+ public void downloadBackground(
527
+ final String url,
528
+ final String version,
529
+ final String sessionKey,
530
+ final String checksum
531
+ ) {
532
+ final String id = this.randomString(10);
533
+ this.saveBundleInfo(
534
+ id,
535
+ new BundleInfo(
536
+ id,
537
+ version,
538
+ BundleStatus.DOWNLOADING,
539
+ new Date(System.currentTimeMillis()),
540
+ ""
541
+ )
542
+ );
543
+ this.notifyDownload(id, 0);
544
+ this.notifyDownload(id, 5);
545
+ this.downloadFileBackground(
546
+ id,
547
+ url,
548
+ version,
549
+ sessionKey,
550
+ checksum,
551
+ this.randomString(10)
552
+ );
553
+ }
554
+
555
+ public BundleInfo download(
556
+ final String url,
557
+ final String version,
558
+ final String sessionKey,
559
+ final String checksum
560
+ ) throws IOException {
561
+ final String id = this.randomString(10);
562
+ this.saveBundleInfo(
563
+ id,
564
+ new BundleInfo(
565
+ id,
566
+ version,
567
+ BundleStatus.DOWNLOADING,
568
+ new Date(System.currentTimeMillis()),
569
+ ""
570
+ )
571
+ );
572
+ this.notifyDownload(id, 0);
573
+ final String idName = bundleDirectory + "/" + id;
574
+ this.notifyDownload(id, 5);
575
+ final String dest = this.randomString(10);
576
+ final File downloaded = this.downloadFile(id, url, dest);
577
+ this.finishDownload(id, dest, version, sessionKey, checksum, false);
578
+ BundleInfo info = new BundleInfo(
579
+ id,
580
+ version,
581
+ BundleStatus.PENDING,
582
+ new Date(System.currentTimeMillis()),
583
+ checksum
584
+ );
585
+ this.saveBundleInfo(id, info);
586
+ return info;
587
+ }
588
+
589
+ public List<BundleInfo> list() {
590
+ final List<BundleInfo> res = new ArrayList<>();
591
+ final File destHot = new File(this.documentsDir, bundleDirectory);
592
+ Log.d(TAG, "list File : " + destHot.getPath());
593
+ if (destHot.exists()) {
594
+ for (final File i : destHot.listFiles()) {
595
+ final String id = i.getName();
596
+ res.add(this.getBundleInfo(id));
597
+ }
598
+ } else {
599
+ Log.i(TAG, "No versions available to list" + destHot);
297
600
  }
298
-
299
- public void reset() {
300
- this.reset(false);
601
+ return res;
602
+ }
603
+
604
+ public Boolean delete(final String id, final Boolean removeInfo)
605
+ throws IOException {
606
+ final BundleInfo deleted = this.getBundleInfo(id);
607
+ if (deleted.isBuiltin() || this.getCurrentBundleId().equals(id)) {
608
+ Log.e(TAG, "Cannot delete " + id);
609
+ return false;
301
610
  }
302
-
303
- public void rollback(final BundleInfo bundle) {
304
- this.setBundleStatus(bundle.getId(), BundleStatus.ERROR);
611
+ final File bundle = new File(this.documentsDir, bundleDirectory + "/" + id);
612
+ if (bundle.exists()) {
613
+ this.deleteDirectory(bundle);
614
+ if (removeInfo) {
615
+ this.removeBundleInfo(id);
616
+ } else {
617
+ this.saveBundleInfo(id, deleted.setStatus(BundleStatus.DELETED));
618
+ }
619
+ return true;
305
620
  }
306
-
307
- public void reset(final boolean internal) {
308
- this.setCurrentBundle(new File("public"));
309
- this.setFallbackVersion(null);
310
- this.setNextVersion(null);
311
- if(!internal) {
312
- this.sendStats("reset", this.getCurrentBundle());
313
- }
621
+ Log.e(TAG, "bundle removed: " + deleted.getVersionName());
622
+ this.sendStats("delete", deleted.getVersionName());
623
+ return false;
624
+ }
625
+
626
+ public Boolean delete(final String id) throws IOException {
627
+ return this.delete(id, true);
628
+ }
629
+
630
+ private File getBundleDirectory(final String id) {
631
+ return new File(this.documentsDir, bundleDirectory + "/" + id);
632
+ }
633
+
634
+ private boolean bundleExists(final String id) {
635
+ final File bundle = this.getBundleDirectory(id);
636
+ final BundleInfo bundleInfo = this.getBundleInfo(id);
637
+ if (bundle == null || !bundle.exists() || bundleInfo.isDeleted()) {
638
+ return false;
314
639
  }
315
-
316
- public void getLatest(final String updateUrl, final Callback callback) {
317
- final String deviceID = this.deviceID;
318
- final String appId = this.appId;
319
- final String versionBuild = this.versionBuild;
320
- final String versionCode = this.versionCode;
321
- final String versionOs = this.versionOs;
322
- final String pluginVersion = CapacitorUpdater.pluginVersion;
323
- final String version = this.getCurrentBundle().getId();
324
- try {
325
- JSONObject json = new JSONObject();
326
- json.put("platform", "android");
327
- json.put("device_id", deviceID);
328
- json.put("app_id", appId);
329
- json.put("version_build", versionBuild);
330
- json.put("version_code", versionCode);
331
- json.put("version_os", versionOs);
332
- json.put("version_name", version);
333
- json.put("plugin_version", pluginVersion);
334
-
335
- // Building a request
336
- JsonObjectRequest request = new JsonObjectRequest(
337
- Request.Method.POST,
338
- updateUrl,
339
- json,
340
- new Response.Listener<JSONObject>() {
341
- @Override
342
- public void onResponse(JSONObject response) {
343
- callback.callback(response);
344
- }
345
- },
346
- new Response.ErrorListener(){
347
- @Override
348
- public void onErrorResponse(VolleyError error) {
349
- Log.e(TAG, "Error getting Latest " + error);
350
- }
351
- });
352
- this.requestQueue.add(request);
353
- } catch(JSONException ex){
354
- // Catch if something went wrong with the params
355
- Log.e(TAG, "Error getLatest JSONException " + ex);
356
- }
640
+ return new File(bundle.getPath(), "/index.html").exists();
641
+ }
642
+
643
+ public Boolean set(final BundleInfo bundle) {
644
+ return this.set(bundle.getId());
645
+ }
646
+
647
+ public Boolean set(final String id) {
648
+ final BundleInfo newBundle = this.getBundleInfo(id);
649
+ if (newBundle.isBuiltin()) {
650
+ this.reset();
651
+ return true;
357
652
  }
358
-
359
- public void sendStats(final String action, final BundleInfo bundle) {
360
- String statsUrl = this.statsUrl;
361
- if (statsUrl == null || "".equals(statsUrl) || statsUrl.length() == 0) { return; }
362
- try {
363
- JSONObject json = new JSONObject();
364
- json.put("platform", "android");
365
- json.put("device_id", this.deviceID);
366
- json.put("app_id", this.appId);
367
- json.put("version_build", this.versionBuild);
368
- json.put("version_code", this.versionCode);
369
- json.put("version_os", this.versionOs);
370
- json.put("version_name", bundle.getVersionName());
371
- json.put("plugin_version", pluginVersion);
372
- json.put("action", action);
373
-
374
- // Building a request
375
- JsonObjectRequest request = new JsonObjectRequest(
376
- Request.Method.POST,
377
- statsUrl,
378
- json,
379
- new Response.Listener<JSONObject>() {
380
- @Override
381
- public void onResponse(JSONObject response) {
382
- Log.i(TAG, "Stats send for \"" + action + "\", version " + bundle.getVersionName());
383
- }
384
- },
385
- new Response.ErrorListener(){
386
- @Override
387
- public void onErrorResponse(VolleyError error) {
388
- Log.i(TAG, "Stats send for \"" + action + "\", version " + bundle.getVersionName());
389
- }
390
- });
391
- this.requestQueue.add(request);
392
- } catch(JSONException ex){
393
- // Catch if something went wrong with the params
394
- Log.e(TAG, "Error sendStats JSONException " + ex);
653
+ final File bundle = this.getBundleDirectory(id);
654
+ Log.i(TAG, "Setting next active bundle: " + id);
655
+ if (this.bundleExists(id)) {
656
+ this.setCurrentBundle(bundle);
657
+ this.setBundleStatus(id, BundleStatus.PENDING);
658
+ this.sendStats("set", newBundle.getVersionName());
659
+ return true;
660
+ }
661
+ this.setBundleStatus(id, BundleStatus.ERROR);
662
+ this.sendStats("set_fail", newBundle.getVersionName());
663
+ return false;
664
+ }
665
+
666
+ public void reset() {
667
+ this.reset(false);
668
+ }
669
+
670
+ public void setSuccess(final BundleInfo bundle, Boolean autoDeletePrevious) {
671
+ this.setBundleStatus(bundle.getId(), BundleStatus.SUCCESS);
672
+ final BundleInfo fallback = this.getFallbackBundle();
673
+ Log.d(CapacitorUpdater.TAG, "Fallback bundle is: " + fallback);
674
+ Log.i(
675
+ CapacitorUpdater.TAG,
676
+ "Version successfully loaded: " + bundle.getVersionName()
677
+ );
678
+ if (autoDeletePrevious && !fallback.isBuiltin()) {
679
+ try {
680
+ final Boolean res = this.delete(fallback.getId());
681
+ if (res) {
682
+ Log.i(
683
+ CapacitorUpdater.TAG,
684
+ "Deleted previous bundle: " + fallback.getVersionName()
685
+ );
395
686
  }
687
+ } catch (final IOException e) {
688
+ Log.e(
689
+ CapacitorUpdater.TAG,
690
+ "Failed to delete previous bundle: " + fallback.getVersionName(),
691
+ e
692
+ );
693
+ }
694
+ }
695
+ this.setFallbackBundle(bundle);
696
+ }
697
+
698
+ public void setError(final BundleInfo bundle) {
699
+ this.setBundleStatus(bundle.getId(), BundleStatus.ERROR);
700
+ }
701
+
702
+ public void reset(final boolean internal) {
703
+ Log.d(CapacitorUpdater.TAG, "reset: " + internal);
704
+ this.setCurrentBundle(new File("public"));
705
+ this.setFallbackBundle(null);
706
+ this.setNextBundle(null);
707
+ if (!internal) {
708
+ this.sendStats("reset", this.getCurrentBundle().getVersionName());
709
+ }
710
+ }
711
+
712
+ private JSONObject createInfoObject() throws JSONException {
713
+ JSONObject json = new JSONObject();
714
+ json.put("platform", "android");
715
+ json.put("device_id", this.deviceID);
716
+ json.put("app_id", this.appId);
717
+ json.put("custom_id", this.customId);
718
+ json.put("version_build", this.versionBuild);
719
+ json.put("version_code", this.versionCode);
720
+ json.put("version_os", this.versionOs);
721
+ json.put("version_name", this.getCurrentBundle().getVersionName());
722
+ json.put("plugin_version", this.PLUGIN_VERSION);
723
+ json.put("is_emulator", this.isEmulator());
724
+ json.put("is_prod", this.isProd());
725
+ return json;
726
+ }
727
+
728
+ private JSObject createError(String message, VolleyError error) {
729
+ NetworkResponse response = error.networkResponse;
730
+ final JSObject retError = new JSObject();
731
+ retError.put("error", "response_error");
732
+ if (response != null) {
733
+ try {
734
+ String json = new String(
735
+ response.data,
736
+ HttpHeaderParser.parseCharset(response.headers)
737
+ );
738
+ retError.put("message", message + ": " + json);
739
+ } catch (UnsupportedEncodingException e) {
740
+ retError.put("message", message + ": " + e.toString());
741
+ }
742
+ } else {
743
+ retError.put("message", message + ": " + error.toString());
744
+ }
745
+ Log.e(TAG, message + ": " + retError);
746
+ return retError;
747
+ }
748
+
749
+ public void getLatest(final String updateUrl, final Callback callback) {
750
+ JSONObject json = null;
751
+ try {
752
+ json = this.createInfoObject();
753
+ } catch (JSONException e) {
754
+ Log.e(TAG, "Error getLatest JSONException", e);
755
+ e.printStackTrace();
756
+ final JSObject retError = new JSObject();
757
+ retError.put("message", "Cannot get info: " + e.toString());
758
+ retError.put("error", "json_error");
759
+ callback.callback(retError);
760
+ return;
396
761
  }
397
762
 
398
- public BundleInfo getBundleInfo(String id) {
399
- if(id == null) {
400
- id = BundleInfo.VERSION_UNKNOWN;
401
- }
402
- Log.d(TAG, "Getting info for bundle [" + id + "]");
403
- BundleInfo result;
404
- if(BundleInfo.ID_BUILTIN.equals(id)) {
405
- result = new BundleInfo(id, (String) null, BundleStatus.SUCCESS, "");
406
- } else {
407
- try {
408
- String stored = this.prefs.getString(id + INFO_SUFFIX, "");
409
- result = BundleInfo.fromJSON(stored);
410
- } catch (JSONException e) {
411
- Log.e(TAG, "Failed to parse info for bundle [" + id + "] ", e);
412
- result = new BundleInfo(id, (String) null, BundleStatus.PENDING, "");
763
+ Log.i(CapacitorUpdater.TAG, "Auto-update parameters: " + json);
764
+ // Building a request
765
+ JsonObjectRequest request = new JsonObjectRequest(
766
+ Request.Method.POST,
767
+ updateUrl,
768
+ json,
769
+ new Response.Listener<JSONObject>() {
770
+ @Override
771
+ public void onResponse(JSONObject res) {
772
+ final JSObject ret = new JSObject();
773
+ Iterator<String> keys = res.keys();
774
+ while (keys.hasNext()) {
775
+ String key = keys.next();
776
+ if (res.has(key)) {
777
+ try {
778
+ if ("session_key".equals(key)) {
779
+ ret.put("sessionKey", res.get(key));
780
+ } else {
781
+ ret.put(key, res.get(key));
782
+ }
783
+ } catch (JSONException e) {
784
+ e.printStackTrace();
785
+ final JSObject retError = new JSObject();
786
+ retError.put("message", "Cannot set info: " + e.toString());
787
+ retError.put("error", "response_error");
788
+ callback.callback(retError);
789
+ }
413
790
  }
791
+ }
792
+ callback.callback(ret);
414
793
  }
415
-
416
- Log.d(TAG, "Returning info [" + id + "] " + result);
417
- return result;
794
+ },
795
+ new Response.ErrorListener() {
796
+ @Override
797
+ public void onErrorResponse(VolleyError error) {
798
+ callback.callback(
799
+ CapacitorUpdater.this.createError("Error get latest", error)
800
+ );
801
+ }
802
+ }
803
+ );
804
+ request.setRetryPolicy(
805
+ new DefaultRetryPolicy(
806
+ this.timeout,
807
+ DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
808
+ DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
809
+ )
810
+ );
811
+ this.requestQueue.add(request);
812
+ }
813
+
814
+ public void setChannel(final String channel, final Callback callback) {
815
+ String channelUrl = this.channelUrl;
816
+ if (
817
+ channelUrl == null || "".equals(channelUrl) || channelUrl.length() == 0
818
+ ) {
819
+ Log.e(TAG, "Channel URL is not set");
820
+ final JSObject retError = new JSObject();
821
+ retError.put("message", "channelUrl missing");
822
+ retError.put("error", "missing_config");
823
+ callback.callback(retError);
824
+ return;
418
825
  }
419
-
420
- public BundleInfo getBundleInfoByName(final String versionName) {
421
- final List<BundleInfo> installed = this.list();
422
- for(final BundleInfo i : installed) {
423
- if(i.getVersionName().equals(versionName)) {
424
- return i;
826
+ JSONObject json = null;
827
+ try {
828
+ json = this.createInfoObject();
829
+ json.put("channel", channel);
830
+ } catch (JSONException e) {
831
+ Log.e(TAG, "Error setChannel JSONException", e);
832
+ e.printStackTrace();
833
+ final JSObject retError = new JSObject();
834
+ retError.put("message", "Cannot get info: " + e.toString());
835
+ retError.put("error", "json_error");
836
+ callback.callback(retError);
837
+ return;
838
+ }
839
+ // Building a request
840
+ JsonObjectRequest request = new JsonObjectRequest(
841
+ Request.Method.POST,
842
+ channelUrl,
843
+ json,
844
+ new Response.Listener<JSONObject>() {
845
+ @Override
846
+ public void onResponse(JSONObject res) {
847
+ final JSObject ret = new JSObject();
848
+ Iterator<String> keys = res.keys();
849
+ while (keys.hasNext()) {
850
+ String key = keys.next();
851
+ if (res.has(key)) {
852
+ try {
853
+ ret.put(key, res.get(key));
854
+ } catch (JSONException e) {
855
+ e.printStackTrace();
856
+ final JSObject retError = new JSObject();
857
+ retError.put("message", "Cannot set channel: " + e.toString());
858
+ retError.put("error", "response_error");
859
+ callback.callback(ret);
860
+ }
425
861
  }
862
+ }
863
+ Log.i(TAG, "Channel set to \"" + channel);
864
+ callback.callback(ret);
426
865
  }
427
- return null;
866
+ },
867
+ new Response.ErrorListener() {
868
+ @Override
869
+ public void onErrorResponse(VolleyError error) {
870
+ callback.callback(
871
+ CapacitorUpdater.this.createError("Error set channel", error)
872
+ );
873
+ }
874
+ }
875
+ );
876
+ request.setRetryPolicy(
877
+ new DefaultRetryPolicy(
878
+ this.timeout,
879
+ DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
880
+ DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
881
+ )
882
+ );
883
+ this.requestQueue.add(request);
884
+ }
885
+
886
+ public void getChannel(final Callback callback) {
887
+ String channelUrl = this.channelUrl;
888
+ if (
889
+ channelUrl == null || "".equals(channelUrl) || channelUrl.length() == 0
890
+ ) {
891
+ Log.e(TAG, "Channel URL is not set");
892
+ final JSObject retError = new JSObject();
893
+ retError.put("message", "Channel URL is not set");
894
+ retError.put("error", "missing_config");
895
+ callback.callback(retError);
896
+ return;
428
897
  }
429
-
430
- private void removeBundleInfo(final String id) {
431
- this.saveBundleInfo(id, null);
898
+ JSONObject json = null;
899
+ try {
900
+ json = this.createInfoObject();
901
+ } catch (JSONException e) {
902
+ Log.e(TAG, "Error getChannel JSONException", e);
903
+ e.printStackTrace();
904
+ final JSObject retError = new JSObject();
905
+ retError.put("message", "Cannot get info: " + e.toString());
906
+ retError.put("error", "json_error");
907
+ callback.callback(retError);
908
+ return;
432
909
  }
433
-
434
- private void saveBundleInfo(final String id, final BundleInfo info) {
435
- if(id == null || (info != null && (info.isBuiltin() || info.isUnknown()))) {
436
- Log.d(TAG, "Not saving info for bundle: [" + id + "] " + info);
437
- return;
910
+ // Building a request
911
+ JsonObjectRequest request = new JsonObjectRequest(
912
+ Request.Method.PUT,
913
+ channelUrl,
914
+ json,
915
+ new Response.Listener<JSONObject>() {
916
+ @Override
917
+ public void onResponse(JSONObject res) {
918
+ final JSObject ret = new JSObject();
919
+ Iterator<String> keys = res.keys();
920
+ while (keys.hasNext()) {
921
+ String key = keys.next();
922
+ if (res.has(key)) {
923
+ try {
924
+ ret.put(key, res.get(key));
925
+ } catch (JSONException e) {
926
+ e.printStackTrace();
927
+ }
928
+ }
929
+ }
930
+ Log.i(TAG, "Channel get to \"" + ret);
931
+ callback.callback(ret);
438
932
  }
439
-
440
- if(info == null) {
441
- Log.d(TAG, "Removing info for bundle [" + id + "]");
442
- this.editor.remove(id + INFO_SUFFIX);
443
- } else {
444
- final BundleInfo update = info.setId(id);
445
- Log.d(TAG, "Storing info for bundle [" + id + "] " + update.toString());
446
- this.editor.putString(id + INFO_SUFFIX, update.toString());
933
+ },
934
+ new Response.ErrorListener() {
935
+ @Override
936
+ public void onErrorResponse(VolleyError error) {
937
+ callback.callback(
938
+ CapacitorUpdater.this.createError("Error get channel", error)
939
+ );
447
940
  }
448
- this.editor.commit();
941
+ }
942
+ );
943
+ request.setRetryPolicy(
944
+ new DefaultRetryPolicy(
945
+ this.timeout,
946
+ DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
947
+ DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
948
+ )
949
+ );
950
+ this.requestQueue.add(request);
951
+ }
952
+
953
+ public void sendStats(final String action, final String versionName) {
954
+ String statsUrl = this.statsUrl;
955
+ if (statsUrl == null || "".equals(statsUrl) || statsUrl.length() == 0) {
956
+ return;
449
957
  }
450
-
451
- public void setVersionName(final String id, final String name) {
452
- if(id != null) {
453
- Log.d(TAG, "Setting name for bundle [" + id + "] to " + name);
454
- BundleInfo info = this.getBundleInfo(id);
455
- this.saveBundleInfo(id, info.setVersionName(name));
456
- }
958
+ JSONObject json = null;
959
+ try {
960
+ json = this.createInfoObject();
961
+ json.put("action", action);
962
+ } catch (JSONException e) {
963
+ Log.e(TAG, "Error sendStats JSONException", e);
964
+ e.printStackTrace();
965
+ return;
457
966
  }
458
-
459
- private void setBundleStatus(final String id, final BundleStatus status) {
460
- if(id != null && status != null) {
461
- BundleInfo info = this.getBundleInfo(id);
462
- Log.d(TAG, "Setting status for bundle [" + id + "] to " + status);
463
- this.saveBundleInfo(id, info.setStatus(status));
967
+ // Building a request
968
+ JsonObjectRequest request = new JsonObjectRequest(
969
+ Request.Method.POST,
970
+ statsUrl,
971
+ json,
972
+ new Response.Listener<JSONObject>() {
973
+ @Override
974
+ public void onResponse(JSONObject response) {
975
+ Log.i(
976
+ TAG,
977
+ "Stats send for \"" + action + "\", version " + versionName
978
+ );
464
979
  }
465
- }
466
-
467
- private String getCurrentBundleId() {
468
- if(this.isUsingBuiltin()) {
469
- return BundleInfo.ID_BUILTIN;
470
- } else {
471
- final String path = this.getCurrentBundlePath();
472
- return path.substring(path.lastIndexOf('/') + 1);
980
+ },
981
+ new Response.ErrorListener() {
982
+ @Override
983
+ public void onErrorResponse(VolleyError error) {
984
+ CapacitorUpdater.this.createError("Error send stats", error);
473
985
  }
986
+ }
987
+ );
988
+ request.setRetryPolicy(
989
+ new DefaultRetryPolicy(
990
+ this.timeout,
991
+ DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
992
+ DefaultRetryPolicy.DEFAULT_BACKOFF_MULT
993
+ )
994
+ );
995
+ this.requestQueue.add(request);
996
+ }
997
+
998
+ public BundleInfo getBundleInfo(final String id) {
999
+ String trueId = BundleInfo.VERSION_UNKNOWN;
1000
+ if (id != null) {
1001
+ trueId = id;
474
1002
  }
475
-
476
- public BundleInfo getCurrentBundle() {
477
- return this.getBundleInfo(this.getCurrentBundleId());
1003
+ Log.d(TAG, "Getting info for bundle [" + trueId + "]");
1004
+ BundleInfo result;
1005
+ if (BundleInfo.ID_BUILTIN.equals(trueId)) {
1006
+ result = new BundleInfo(trueId, null, BundleStatus.SUCCESS, "", "");
1007
+ } else if (BundleInfo.VERSION_UNKNOWN.equals(trueId)) {
1008
+ result = new BundleInfo(trueId, null, BundleStatus.ERROR, "", "");
1009
+ } else {
1010
+ try {
1011
+ String stored = this.prefs.getString(trueId + INFO_SUFFIX, "");
1012
+ result = BundleInfo.fromJSON(stored);
1013
+ } catch (JSONException e) {
1014
+ Log.e(TAG, "Failed to parse info for bundle [" + trueId + "] ", e);
1015
+ result = new BundleInfo(trueId, null, BundleStatus.PENDING, "", "");
1016
+ }
478
1017
  }
479
-
480
- public String getCurrentBundlePath() {
481
- String path = this.prefs.getString(WebView.CAP_SERVER_PATH, "public");
482
- if("".equals(path.trim())) {
483
- return "public";
484
- }
485
- return path;
1018
+ // Log.d(TAG, "Returning info [" + trueId + "] " + result);
1019
+ return result;
1020
+ }
1021
+
1022
+ public BundleInfo getBundleInfoByName(final String versionName) {
1023
+ final List<BundleInfo> installed = this.list();
1024
+ for (final BundleInfo i : installed) {
1025
+ if (i.getVersionName().equals(versionName)) {
1026
+ return i;
1027
+ }
486
1028
  }
487
-
488
- public Boolean isUsingBuiltin() {
489
- return this.getCurrentBundlePath().equals("public");
1029
+ return null;
1030
+ }
1031
+
1032
+ private void removeBundleInfo(final String id) {
1033
+ this.saveBundleInfo(id, null);
1034
+ }
1035
+
1036
+ private void saveBundleInfo(final String id, final BundleInfo info) {
1037
+ if (
1038
+ id == null || (info != null && (info.isBuiltin() || info.isUnknown()))
1039
+ ) {
1040
+ Log.d(TAG, "Not saving info for bundle: [" + id + "] " + info);
1041
+ return;
490
1042
  }
491
1043
 
492
- public BundleInfo getFallbackVersion() {
493
- final String id = this.prefs.getString(FALLBACK_VERSION, BundleInfo.ID_BUILTIN);
494
- return this.getBundleInfo(id);
1044
+ if (info == null) {
1045
+ Log.d(TAG, "Removing info for bundle [" + id + "]");
1046
+ this.editor.remove(id + INFO_SUFFIX);
1047
+ } else {
1048
+ final BundleInfo update = info.setId(id);
1049
+ Log.d(TAG, "Storing info for bundle [" + id + "] " + update.toString());
1050
+ this.editor.putString(id + INFO_SUFFIX, update.toString());
495
1051
  }
496
-
497
- private void setFallbackVersion(final BundleInfo fallback) {
498
- this.editor.putString(FALLBACK_VERSION,
499
- fallback == null
500
- ? BundleInfo.ID_BUILTIN
501
- : fallback.getId()
502
- );
1052
+ this.editor.commit();
1053
+ }
1054
+
1055
+ public void setVersionName(final String id, final String name) {
1056
+ if (id != null) {
1057
+ Log.d(TAG, "Setting name for bundle [" + id + "] to " + name);
1058
+ BundleInfo info = this.getBundleInfo(id);
1059
+ this.saveBundleInfo(id, info.setVersionName(name));
503
1060
  }
1061
+ }
504
1062
 
505
- public BundleInfo getNextVersion() {
506
- final String id = this.prefs.getString(NEXT_VERSION, "");
507
- if(id != "") {
508
- return this.getBundleInfo(id);
509
- } else {
510
- return null;
511
- }
1063
+ private void setBundleStatus(final String id, final BundleStatus status) {
1064
+ if (id != null && status != null) {
1065
+ BundleInfo info = this.getBundleInfo(id);
1066
+ Log.d(TAG, "Setting status for bundle [" + id + "] to " + status);
1067
+ this.saveBundleInfo(id, info.setStatus(status));
512
1068
  }
1069
+ }
1070
+
1071
+ private String getCurrentBundleId() {
1072
+ if (this.isUsingBuiltin()) {
1073
+ return BundleInfo.ID_BUILTIN;
1074
+ } else {
1075
+ final String path = this.getCurrentBundlePath();
1076
+ return path.substring(path.lastIndexOf('/') + 1);
1077
+ }
1078
+ }
513
1079
 
514
- public boolean setNextVersion(final String next) {
515
- if (next == null) {
516
- this.editor.remove(NEXT_VERSION);
517
- } else {
518
- final File bundle = this.getBundleDirectory(next);
519
- if (!this.bundleExists(bundle)) {
520
- return false;
521
- }
1080
+ public BundleInfo getCurrentBundle() {
1081
+ return this.getBundleInfo(this.getCurrentBundleId());
1082
+ }
522
1083
 
523
- this.editor.putString(NEXT_VERSION, next);
524
- this.setBundleStatus(next, BundleStatus.PENDING);
525
- }
526
- this.editor.commit();
527
- return true;
1084
+ public String getCurrentBundlePath() {
1085
+ String path = this.prefs.getString(WebView.CAP_SERVER_PATH, "public");
1086
+ if ("".equals(path.trim())) {
1087
+ return "public";
528
1088
  }
529
-
1089
+ return path;
1090
+ }
1091
+
1092
+ public Boolean isUsingBuiltin() {
1093
+ return this.getCurrentBundlePath().equals("public");
1094
+ }
1095
+
1096
+ public BundleInfo getFallbackBundle() {
1097
+ final String id =
1098
+ this.prefs.getString(FALLBACK_VERSION, BundleInfo.ID_BUILTIN);
1099
+ return this.getBundleInfo(id);
1100
+ }
1101
+
1102
+ private void setFallbackBundle(final BundleInfo fallback) {
1103
+ this.editor.putString(
1104
+ FALLBACK_VERSION,
1105
+ fallback == null ? BundleInfo.ID_BUILTIN : fallback.getId()
1106
+ );
1107
+ this.editor.commit();
1108
+ }
1109
+
1110
+ public BundleInfo getNextBundle() {
1111
+ final String id = this.prefs.getString(NEXT_VERSION, null);
1112
+ if (id == null) return null;
1113
+ return this.getBundleInfo(id);
1114
+ }
1115
+
1116
+ public boolean setNextBundle(final String next) {
1117
+ if (next == null) {
1118
+ this.editor.remove(NEXT_VERSION);
1119
+ } else {
1120
+ final BundleInfo newBundle = this.getBundleInfo(next);
1121
+ if (!newBundle.isBuiltin() && !this.bundleExists(next)) {
1122
+ return false;
1123
+ }
1124
+ this.editor.putString(NEXT_VERSION, next);
1125
+ this.setBundleStatus(next, BundleStatus.PENDING);
1126
+ }
1127
+ this.editor.commit();
1128
+ return true;
1129
+ }
530
1130
  }