@capgo/capacitor-updater 8.0.0 → 8.0.1
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 +2 -2
- package/Package.swift +35 -0
- package/README.md +667 -206
- package/android/build.gradle +16 -11
- package/android/proguard-rules.pro +28 -0
- package/android/src/main/AndroidManifest.xml +0 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +134 -194
- 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/CapacitorUpdater.java +967 -1027
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1283 -1180
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +276 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +28 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +45 -48
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +440 -113
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +101 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +32 -0
- package/dist/docs.json +1316 -473
- package/dist/esm/definitions.d.ts +518 -248
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +4 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +25 -41
- package/dist/esm/web.js +67 -35
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +67 -35
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +67 -35
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/CapacitorUpdater.swift +736 -361
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +436 -136
- package/ios/Plugin/CryptoCipherV2.swift +310 -0
- package/ios/Plugin/InternalUtils.swift +258 -0
- package/package.json +33 -29
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +0 -153
- package/ios/Plugin/CapacitorUpdaterPlugin.h +0 -10
- package/ios/Plugin/CapacitorUpdaterPlugin.m +0 -27
- package/ios/Plugin/CryptoCipher.swift +0 -240
|
@@ -3,124 +3,451 @@
|
|
|
3
3
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
4
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
6
|
package ee.forgr.capacitor_updater;
|
|
8
7
|
|
|
9
|
-
import android.
|
|
10
|
-
import android.
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
8
|
+
import android.content.Context;
|
|
9
|
+
import android.util.Log;
|
|
10
|
+
import androidx.annotation.NonNull;
|
|
11
|
+
import androidx.work.Data;
|
|
12
|
+
import androidx.work.Worker;
|
|
13
|
+
import androidx.work.WorkerParameters;
|
|
14
|
+
import java.io.*;
|
|
15
|
+
import java.io.FileInputStream;
|
|
16
|
+
import java.net.HttpURLConnection;
|
|
15
17
|
import java.net.URL;
|
|
16
|
-
import java.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
final
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
String
|
|
48
|
-
String
|
|
49
|
-
String
|
|
50
|
-
String
|
|
51
|
-
String
|
|
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
|
-
|
|
18
|
+
import java.nio.channels.FileChannel;
|
|
19
|
+
import java.security.MessageDigest;
|
|
20
|
+
import java.util.ArrayList;
|
|
21
|
+
import java.util.Arrays;
|
|
22
|
+
import java.util.List;
|
|
23
|
+
import java.util.Objects;
|
|
24
|
+
import java.util.concurrent.ExecutorService;
|
|
25
|
+
import java.util.concurrent.Executors;
|
|
26
|
+
import java.util.concurrent.Future;
|
|
27
|
+
import java.util.concurrent.TimeUnit;
|
|
28
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
29
|
+
import java.util.concurrent.atomic.AtomicLong;
|
|
30
|
+
import okhttp3.OkHttpClient;
|
|
31
|
+
import okhttp3.Protocol;
|
|
32
|
+
import okhttp3.Request;
|
|
33
|
+
import okhttp3.Response;
|
|
34
|
+
import okhttp3.ResponseBody;
|
|
35
|
+
import org.brotli.dec.BrotliInputStream;
|
|
36
|
+
import org.json.JSONArray;
|
|
37
|
+
import org.json.JSONObject;
|
|
38
|
+
|
|
39
|
+
public class DownloadService extends Worker {
|
|
40
|
+
|
|
41
|
+
public static final String TAG = "Capacitor-updater";
|
|
42
|
+
public static final String URL = "URL";
|
|
43
|
+
public static final String ID = "id";
|
|
44
|
+
public static final String PERCENT = "percent";
|
|
45
|
+
public static final String FILEDEST = "filendest";
|
|
46
|
+
public static final String DOCDIR = "docdir";
|
|
47
|
+
public static final String ERROR = "error";
|
|
48
|
+
public static final String VERSION = "version";
|
|
49
|
+
public static final String SESSIONKEY = "sessionkey";
|
|
50
|
+
public static final String CHECKSUM = "checksum";
|
|
51
|
+
public static final String PUBLIC_KEY = "publickey";
|
|
52
|
+
public static final String IS_MANIFEST = "is_manifest";
|
|
53
|
+
private static final String UPDATE_FILE = "update.dat";
|
|
54
|
+
|
|
55
|
+
private final OkHttpClient client = new OkHttpClient.Builder().protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1)).build();
|
|
56
|
+
|
|
57
|
+
public DownloadService(@NonNull Context context, @NonNull WorkerParameters params) {
|
|
58
|
+
super(context, params);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private void setProgress(int percent) {
|
|
62
|
+
Data progress = new Data.Builder().putInt(PERCENT, percent).build();
|
|
63
|
+
setProgressAsync(progress);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private Result createFailureResult(String error) {
|
|
67
|
+
Data output = new Data.Builder().putString(ERROR, error).build();
|
|
68
|
+
return Result.failure(output);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private Result createSuccessResult(String dest, String version, String sessionKey, String checksum, boolean isManifest) {
|
|
72
|
+
Data output = new Data.Builder()
|
|
73
|
+
.putString(FILEDEST, dest)
|
|
74
|
+
.putString(VERSION, version)
|
|
75
|
+
.putString(SESSIONKEY, sessionKey)
|
|
76
|
+
.putString(CHECKSUM, checksum)
|
|
77
|
+
.putBoolean(IS_MANIFEST, isManifest)
|
|
78
|
+
.build();
|
|
79
|
+
return Result.success(output);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@NonNull
|
|
83
|
+
@Override
|
|
84
|
+
public Result doWork() {
|
|
85
|
+
try {
|
|
86
|
+
String url = getInputData().getString(URL);
|
|
87
|
+
String id = getInputData().getString(ID);
|
|
88
|
+
String documentsDir = getInputData().getString(DOCDIR);
|
|
89
|
+
String dest = getInputData().getString(FILEDEST);
|
|
90
|
+
String version = getInputData().getString(VERSION);
|
|
91
|
+
String sessionKey = getInputData().getString(SESSIONKEY);
|
|
92
|
+
String checksum = getInputData().getString(CHECKSUM);
|
|
93
|
+
String publicKey = getInputData().getString(PUBLIC_KEY);
|
|
94
|
+
boolean isManifest = getInputData().getBoolean(IS_MANIFEST, false);
|
|
95
|
+
|
|
96
|
+
Log.d(TAG, "doWork isManifest: " + isManifest);
|
|
97
|
+
|
|
98
|
+
if (isManifest) {
|
|
99
|
+
JSONArray manifest = DataManager.getInstance().getAndClearManifest();
|
|
100
|
+
if (manifest != null) {
|
|
101
|
+
handleManifestDownload(id, documentsDir, dest, version, sessionKey, publicKey, manifest.toString());
|
|
102
|
+
return createSuccessResult(dest, version, sessionKey, checksum, true);
|
|
103
|
+
} else {
|
|
104
|
+
Log.e(TAG, "Manifest is null");
|
|
105
|
+
return createFailureResult("Manifest is null");
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
handleSingleFileDownload(url, id, documentsDir, dest, version, sessionKey, checksum);
|
|
109
|
+
return createSuccessResult(dest, version, sessionKey, checksum, false);
|
|
110
|
+
}
|
|
111
|
+
} catch (Exception e) {
|
|
112
|
+
Log.e(TAG, "Error in doWork", e);
|
|
113
|
+
return createFailureResult(e.getMessage());
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private int calcTotalPercent(long downloadedBytes, long contentLength) {
|
|
118
|
+
if (contentLength <= 0) {
|
|
119
|
+
return 0;
|
|
80
120
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
e.printStackTrace();
|
|
86
|
-
publishResults(
|
|
87
|
-
"",
|
|
88
|
-
id,
|
|
89
|
-
version,
|
|
90
|
-
checksum,
|
|
91
|
-
sessionKey,
|
|
92
|
-
e.getLocalizedMessage()
|
|
93
|
-
);
|
|
121
|
+
int percent = (int) (((double) downloadedBytes / contentLength) * 100);
|
|
122
|
+
percent = Math.max(10, percent);
|
|
123
|
+
percent = Math.min(70, percent);
|
|
124
|
+
return percent;
|
|
94
125
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
|
|
127
|
+
private void handleManifestDownload(
|
|
128
|
+
String id,
|
|
129
|
+
String documentsDir,
|
|
130
|
+
String dest,
|
|
131
|
+
String version,
|
|
132
|
+
String sessionKey,
|
|
133
|
+
String publicKey,
|
|
134
|
+
String manifestString
|
|
135
|
+
) {
|
|
136
|
+
try {
|
|
137
|
+
Log.d(TAG, "handleManifestDownload");
|
|
138
|
+
JSONArray manifest = new JSONArray(manifestString);
|
|
139
|
+
File destFolder = new File(documentsDir, dest);
|
|
140
|
+
File cacheFolder = new File(getApplicationContext().getCacheDir(), "capgo_downloads");
|
|
141
|
+
File builtinFolder = new File(getApplicationContext().getFilesDir(), "public");
|
|
142
|
+
|
|
143
|
+
// Ensure directories are created
|
|
144
|
+
if (!destFolder.exists() && !destFolder.mkdirs()) {
|
|
145
|
+
throw new IOException("Failed to create destination directory: " + destFolder.getAbsolutePath());
|
|
146
|
+
}
|
|
147
|
+
if (!cacheFolder.exists() && !cacheFolder.mkdirs()) {
|
|
148
|
+
throw new IOException("Failed to create cache directory: " + cacheFolder.getAbsolutePath());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
int totalFiles = manifest.length();
|
|
152
|
+
final AtomicLong completedFiles = new AtomicLong(0);
|
|
153
|
+
final AtomicBoolean hasError = new AtomicBoolean(false);
|
|
154
|
+
|
|
155
|
+
// Use more threads for I/O-bound operations
|
|
156
|
+
int threadCount = Math.min(64, Math.max(32, totalFiles));
|
|
157
|
+
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
|
158
|
+
List<Future<?>> futures = new ArrayList<>();
|
|
159
|
+
|
|
160
|
+
for (int i = 0; i < totalFiles; i++) {
|
|
161
|
+
JSONObject entry = manifest.getJSONObject(i);
|
|
162
|
+
String fileName = entry.getString("file_name");
|
|
163
|
+
String fileHash = entry.getString("file_hash");
|
|
164
|
+
String downloadUrl = entry.getString("download_url");
|
|
165
|
+
|
|
166
|
+
File targetFile = new File(destFolder, fileName);
|
|
167
|
+
File cacheFile = new File(cacheFolder, fileHash + "_" + new File(fileName).getName());
|
|
168
|
+
File builtinFile = new File(builtinFolder, fileName);
|
|
169
|
+
|
|
170
|
+
// Ensure parent directories of the target file exist
|
|
171
|
+
if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
|
|
172
|
+
throw new IOException("Failed to create parent directory for: " + targetFile.getAbsolutePath());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Future<?> future = executor.submit(() -> {
|
|
176
|
+
try {
|
|
177
|
+
if (builtinFile.exists() && verifyChecksum(builtinFile, fileHash)) {
|
|
178
|
+
copyFile(builtinFile, targetFile);
|
|
179
|
+
Log.d(TAG, "using builtin file " + fileName);
|
|
180
|
+
} else if (cacheFile.exists() && verifyChecksum(cacheFile, fileHash)) {
|
|
181
|
+
copyFile(cacheFile, targetFile);
|
|
182
|
+
Log.d(TAG, "already cached " + fileName);
|
|
183
|
+
} else {
|
|
184
|
+
downloadAndVerify(downloadUrl, targetFile, cacheFile, fileHash, sessionKey, publicKey);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
long completed = completedFiles.incrementAndGet();
|
|
188
|
+
int percent = calcTotalPercent(completed, totalFiles);
|
|
189
|
+
setProgress(percent);
|
|
190
|
+
} catch (Exception e) {
|
|
191
|
+
Log.e(TAG, "Error processing file: " + fileName, e);
|
|
192
|
+
hasError.set(true);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
futures.add(future);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Wait for all downloads to complete
|
|
199
|
+
for (Future<?> future : futures) {
|
|
200
|
+
try {
|
|
201
|
+
future.get();
|
|
202
|
+
} catch (Exception e) {
|
|
203
|
+
Log.e(TAG, "Error waiting for download", e);
|
|
204
|
+
hasError.set(true);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
executor.shutdown();
|
|
209
|
+
try {
|
|
210
|
+
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
|
|
211
|
+
executor.shutdownNow();
|
|
212
|
+
}
|
|
213
|
+
} catch (InterruptedException e) {
|
|
214
|
+
executor.shutdownNow();
|
|
215
|
+
Thread.currentThread().interrupt();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (hasError.get()) {
|
|
219
|
+
throw new IOException("One or more files failed to download");
|
|
220
|
+
}
|
|
221
|
+
} catch (Exception e) {
|
|
222
|
+
Log.e(TAG, "Error in handleManifestDownload", e);
|
|
223
|
+
}
|
|
115
224
|
}
|
|
116
|
-
|
|
117
|
-
|
|
225
|
+
|
|
226
|
+
private void handleSingleFileDownload(
|
|
227
|
+
String url,
|
|
228
|
+
String id,
|
|
229
|
+
String documentsDir,
|
|
230
|
+
String dest,
|
|
231
|
+
String version,
|
|
232
|
+
String sessionKey,
|
|
233
|
+
String checksum
|
|
234
|
+
) {
|
|
235
|
+
File target = new File(documentsDir, dest);
|
|
236
|
+
File infoFile = new File(documentsDir, UPDATE_FILE); // The file where the download progress (how much byte
|
|
237
|
+
// downloaded) is stored
|
|
238
|
+
File tempFile = new File(documentsDir, "temp" + ".tmp"); // Temp file, where the downloaded data is stored
|
|
239
|
+
try {
|
|
240
|
+
URL u = new URL(url);
|
|
241
|
+
HttpURLConnection httpConn = null;
|
|
242
|
+
try {
|
|
243
|
+
httpConn = (HttpURLConnection) u.openConnection();
|
|
244
|
+
|
|
245
|
+
// Reading progress file (if exist)
|
|
246
|
+
long downloadedBytes = 0;
|
|
247
|
+
|
|
248
|
+
if (infoFile.exists() && tempFile.exists()) {
|
|
249
|
+
try (BufferedReader reader = new BufferedReader(new FileReader(infoFile))) {
|
|
250
|
+
String updateVersion = reader.readLine();
|
|
251
|
+
if (!updateVersion.equals(version)) {
|
|
252
|
+
clearDownloadData(documentsDir);
|
|
253
|
+
} else {
|
|
254
|
+
downloadedBytes = tempFile.length();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
clearDownloadData(documentsDir);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (downloadedBytes > 0) {
|
|
262
|
+
httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
int responseCode = httpConn.getResponseCode();
|
|
266
|
+
|
|
267
|
+
if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
|
|
268
|
+
long contentLength = httpConn.getContentLength() + downloadedBytes;
|
|
269
|
+
|
|
270
|
+
try (
|
|
271
|
+
InputStream inputStream = httpConn.getInputStream();
|
|
272
|
+
FileOutputStream outputStream = new FileOutputStream(tempFile, downloadedBytes > 0)
|
|
273
|
+
) {
|
|
274
|
+
if (downloadedBytes == 0) {
|
|
275
|
+
try (BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))) {
|
|
276
|
+
writer.write(String.valueOf(version));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Updating the info file
|
|
280
|
+
try (BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))) {
|
|
281
|
+
writer.write(String.valueOf(version));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
int bytesRead = -1;
|
|
285
|
+
byte[] buffer = new byte[4096];
|
|
286
|
+
int lastNotifiedPercent = 0;
|
|
287
|
+
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
288
|
+
outputStream.write(buffer, 0, bytesRead);
|
|
289
|
+
downloadedBytes += bytesRead;
|
|
290
|
+
// Saving progress (flushing every 100 Ko)
|
|
291
|
+
if (downloadedBytes % 102400 == 0) {
|
|
292
|
+
outputStream.flush();
|
|
293
|
+
}
|
|
294
|
+
// Computing percentage
|
|
295
|
+
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
296
|
+
while (lastNotifiedPercent + 10 <= percent) {
|
|
297
|
+
lastNotifiedPercent += 10;
|
|
298
|
+
// Artificial delay using CPU-bound calculation to take ~5 seconds
|
|
299
|
+
double result = 0;
|
|
300
|
+
setProgress(lastNotifiedPercent);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
outputStream.close();
|
|
305
|
+
inputStream.close();
|
|
306
|
+
|
|
307
|
+
// Rename the temp file with the final name (dest)
|
|
308
|
+
tempFile.renameTo(new File(documentsDir, dest));
|
|
309
|
+
infoFile.delete();
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
infoFile.delete();
|
|
313
|
+
}
|
|
314
|
+
} finally {
|
|
315
|
+
if (httpConn != null) {
|
|
316
|
+
httpConn.disconnect();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch (OutOfMemoryError e) {
|
|
320
|
+
e.printStackTrace();
|
|
321
|
+
throw new RuntimeException("low_mem_fail");
|
|
322
|
+
} catch (Exception e) {
|
|
323
|
+
e.printStackTrace();
|
|
324
|
+
throw new RuntimeException(e.getLocalizedMessage());
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private void clearDownloadData(String docDir) {
|
|
329
|
+
File tempFile = new File(docDir, "temp" + ".tmp");
|
|
330
|
+
File infoFile = new File(docDir, UPDATE_FILE);
|
|
331
|
+
try {
|
|
332
|
+
tempFile.delete();
|
|
333
|
+
infoFile.delete();
|
|
334
|
+
infoFile.createNewFile();
|
|
335
|
+
tempFile.createNewFile();
|
|
336
|
+
} catch (IOException e) {
|
|
337
|
+
e.printStackTrace();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Helper methods
|
|
342
|
+
|
|
343
|
+
private void copyFile(File source, File dest) throws IOException {
|
|
344
|
+
try (
|
|
345
|
+
FileInputStream inStream = new FileInputStream(source);
|
|
346
|
+
FileOutputStream outStream = new FileOutputStream(dest);
|
|
347
|
+
FileChannel inChannel = inStream.getChannel();
|
|
348
|
+
FileChannel outChannel = outStream.getChannel()
|
|
349
|
+
) {
|
|
350
|
+
inChannel.transferTo(0, inChannel.size(), outChannel);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private void downloadAndVerify(
|
|
355
|
+
String downloadUrl,
|
|
356
|
+
File targetFile,
|
|
357
|
+
File cacheFile,
|
|
358
|
+
String expectedHash,
|
|
359
|
+
String sessionKey,
|
|
360
|
+
String publicKey
|
|
361
|
+
) throws Exception {
|
|
362
|
+
Log.d(TAG, "downloadAndVerify " + downloadUrl);
|
|
363
|
+
|
|
364
|
+
Request request = new Request.Builder().url(downloadUrl).build();
|
|
365
|
+
|
|
366
|
+
// Create a temporary file for the compressed data
|
|
367
|
+
File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".br");
|
|
368
|
+
|
|
369
|
+
try (Response response = client.newCall(request).execute()) {
|
|
370
|
+
if (!response.isSuccessful()) {
|
|
371
|
+
throw new IOException("Unexpected response code: " + response.code());
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Download compressed file
|
|
375
|
+
try (ResponseBody responseBody = response.body(); FileOutputStream compressedFos = new FileOutputStream(compressedFile)) {
|
|
376
|
+
if (responseBody == null) {
|
|
377
|
+
throw new IOException("Response body is null");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
byte[] buffer = new byte[8192];
|
|
381
|
+
int bytesRead;
|
|
382
|
+
try (InputStream inputStream = responseBody.byteStream()) {
|
|
383
|
+
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
384
|
+
compressedFos.write(buffer, 0, bytesRead);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
String decryptedExpectedHash = expectedHash;
|
|
390
|
+
|
|
391
|
+
if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
392
|
+
Log.d(CapacitorUpdater.TAG + " DLSrv", "Decrypting file " + targetFile.getName());
|
|
393
|
+
CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
|
|
394
|
+
decryptedExpectedHash = CryptoCipherV2.decryptChecksum(decryptedExpectedHash, publicKey);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Decompress the file
|
|
398
|
+
try (
|
|
399
|
+
FileInputStream fis = new FileInputStream(compressedFile);
|
|
400
|
+
BrotliInputStream brotliInputStream = new BrotliInputStream(fis);
|
|
401
|
+
FileOutputStream fos = new FileOutputStream(targetFile)
|
|
402
|
+
) {
|
|
403
|
+
byte[] buffer = new byte[8192];
|
|
404
|
+
int len;
|
|
405
|
+
while ((len = brotliInputStream.read(buffer)) != -1) {
|
|
406
|
+
fos.write(buffer, 0, len);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Delete the compressed file
|
|
411
|
+
compressedFile.delete();
|
|
412
|
+
String calculatedHash = CryptoCipherV2.calcChecksum(targetFile);
|
|
413
|
+
|
|
414
|
+
// Verify checksum
|
|
415
|
+
if (calculatedHash.equals(decryptedExpectedHash)) {
|
|
416
|
+
// Only cache if checksum is correct
|
|
417
|
+
copyFile(targetFile, cacheFile);
|
|
418
|
+
} else {
|
|
419
|
+
targetFile.delete();
|
|
420
|
+
throw new IOException("Checksum verification failed for " + targetFile.getName());
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private boolean verifyChecksum(File file, String expectedHash) {
|
|
426
|
+
try {
|
|
427
|
+
String actualHash = calculateFileHash(file);
|
|
428
|
+
return actualHash.equals(expectedHash);
|
|
429
|
+
} catch (Exception e) {
|
|
430
|
+
e.printStackTrace();
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private String calculateFileHash(File file) throws Exception {
|
|
436
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
437
|
+
FileInputStream fis = new FileInputStream(file);
|
|
438
|
+
byte[] byteArray = new byte[1024];
|
|
439
|
+
int bytesCount = 0;
|
|
440
|
+
|
|
441
|
+
while ((bytesCount = fis.read(byteArray)) != -1) {
|
|
442
|
+
digest.update(byteArray, 0, bytesCount);
|
|
443
|
+
}
|
|
444
|
+
fis.close();
|
|
445
|
+
|
|
446
|
+
byte[] bytes = digest.digest();
|
|
447
|
+
StringBuilder sb = new StringBuilder();
|
|
448
|
+
for (byte aByte : bytes) {
|
|
449
|
+
sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
|
|
450
|
+
}
|
|
451
|
+
return sb.toString();
|
|
118
452
|
}
|
|
119
|
-
intent.putExtra(ID, id);
|
|
120
|
-
intent.putExtra(VERSION, version);
|
|
121
|
-
intent.putExtra(SESSIONKEY, sessionKey);
|
|
122
|
-
intent.putExtra(CHECKSUM, checksum);
|
|
123
|
-
intent.putExtra(ERROR, error);
|
|
124
|
-
sendBroadcast(intent);
|
|
125
|
-
}
|
|
126
453
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
package ee.forgr.capacitor_updater;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.util.Log;
|
|
5
|
+
import androidx.work.BackoffPolicy;
|
|
6
|
+
import androidx.work.Configuration;
|
|
7
|
+
import androidx.work.Constraints;
|
|
8
|
+
import androidx.work.Data;
|
|
9
|
+
import androidx.work.NetworkType;
|
|
10
|
+
import androidx.work.OneTimeWorkRequest;
|
|
11
|
+
import androidx.work.WorkManager;
|
|
12
|
+
import androidx.work.WorkRequest;
|
|
13
|
+
import java.util.HashSet;
|
|
14
|
+
import java.util.Set;
|
|
15
|
+
import java.util.concurrent.TimeUnit;
|
|
16
|
+
|
|
17
|
+
public class DownloadWorkerManager {
|
|
18
|
+
|
|
19
|
+
private static final String TAG = "DownloadWorkerManager";
|
|
20
|
+
private static volatile boolean isInitialized = false;
|
|
21
|
+
private static final Set<String> activeVersions = new HashSet<>();
|
|
22
|
+
|
|
23
|
+
private static synchronized void initializeIfNeeded(Context context) {
|
|
24
|
+
if (!isInitialized) {
|
|
25
|
+
try {
|
|
26
|
+
Configuration config = new Configuration.Builder().setMinimumLoggingLevel(android.util.Log.INFO).build();
|
|
27
|
+
WorkManager.initialize(context, config);
|
|
28
|
+
isInitialized = true;
|
|
29
|
+
} catch (IllegalStateException e) {
|
|
30
|
+
// WorkManager was already initialized, ignore
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static synchronized boolean isVersionDownloading(String version) {
|
|
36
|
+
return activeVersions.contains(version);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public static void enqueueDownload(
|
|
40
|
+
Context context,
|
|
41
|
+
String url,
|
|
42
|
+
String id,
|
|
43
|
+
String documentsDir,
|
|
44
|
+
String dest,
|
|
45
|
+
String version,
|
|
46
|
+
String sessionKey,
|
|
47
|
+
String checksum,
|
|
48
|
+
String publicKey,
|
|
49
|
+
boolean isManifest
|
|
50
|
+
) {
|
|
51
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
52
|
+
|
|
53
|
+
// If version is already downloading, don't start another one
|
|
54
|
+
if (isVersionDownloading(version)) {
|
|
55
|
+
Log.i(TAG, "Version " + version + " is already downloading");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
activeVersions.add(version);
|
|
59
|
+
|
|
60
|
+
// Create input data
|
|
61
|
+
Data inputData = new Data.Builder()
|
|
62
|
+
.putString(DownloadService.URL, url)
|
|
63
|
+
.putString(DownloadService.ID, id)
|
|
64
|
+
.putString(DownloadService.DOCDIR, documentsDir)
|
|
65
|
+
.putString(DownloadService.FILEDEST, dest)
|
|
66
|
+
.putString(DownloadService.VERSION, version)
|
|
67
|
+
.putString(DownloadService.SESSIONKEY, sessionKey)
|
|
68
|
+
.putString(DownloadService.CHECKSUM, checksum)
|
|
69
|
+
.putBoolean(DownloadService.IS_MANIFEST, isManifest)
|
|
70
|
+
.putString(DownloadService.PUBLIC_KEY, publicKey)
|
|
71
|
+
.build();
|
|
72
|
+
|
|
73
|
+
// Create network constraints
|
|
74
|
+
Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
|
|
75
|
+
|
|
76
|
+
// Create work request with tags for tracking
|
|
77
|
+
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(DownloadService.class)
|
|
78
|
+
.setConstraints(constraints)
|
|
79
|
+
.setInputData(inputData)
|
|
80
|
+
.addTag(id)
|
|
81
|
+
.addTag(version) // Add version tag for tracking
|
|
82
|
+
.addTag("capacitor_updater_download")
|
|
83
|
+
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS)
|
|
84
|
+
.build();
|
|
85
|
+
|
|
86
|
+
// Enqueue work
|
|
87
|
+
WorkManager.getInstance(context).enqueue(workRequest);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public static void cancelVersionDownload(Context context, String version) {
|
|
91
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
92
|
+
WorkManager.getInstance(context).cancelAllWorkByTag(version);
|
|
93
|
+
activeVersions.remove(version);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public static void cancelAllDownloads(Context context) {
|
|
97
|
+
initializeIfNeeded(context.getApplicationContext());
|
|
98
|
+
WorkManager.getInstance(context).cancelAllWorkByTag("capacitor_updater_download");
|
|
99
|
+
activeVersions.clear();
|
|
100
|
+
}
|
|
101
|
+
}
|