@capgo/capacitor-updater 6.14.0 → 6.14.2
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/README.md +11 -11
- 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 +1 -1
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +960 -1165
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1259 -1629
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +161 -197
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +202 -256
- package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +16 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +44 -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 +378 -467
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +71 -83
- package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +19 -28
- package/dist/docs.json +17 -17
- package/dist/esm/definitions.d.ts +13 -13
- 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 +2 -2
- package/dist/esm/web.js +43 -43
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +43 -43
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +43 -43
- package/dist/plugin.js.map +1 -1
- package/ios/Plugin/CapacitorUpdater.swift +8 -8
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +1 -1
- package/package.json +5 -7
|
@@ -38,505 +38,416 @@ import org.json.JSONObject;
|
|
|
38
38
|
|
|
39
39
|
public class DownloadService extends Worker {
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
public DownloadService(
|
|
60
|
-
@NonNull Context context,
|
|
61
|
-
@NonNull WorkerParameters params
|
|
62
|
-
) {
|
|
63
|
-
super(context, params);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private void setProgress(int percent) {
|
|
67
|
-
Data progress = new Data.Builder().putInt(PERCENT, percent).build();
|
|
68
|
-
setProgressAsync(progress);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private Result createFailureResult(String error) {
|
|
72
|
-
Data output = new Data.Builder().putString(ERROR, error).build();
|
|
73
|
-
return Result.failure(output);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private Result createSuccessResult(
|
|
77
|
-
String dest,
|
|
78
|
-
String version,
|
|
79
|
-
String sessionKey,
|
|
80
|
-
String checksum,
|
|
81
|
-
boolean isManifest
|
|
82
|
-
) {
|
|
83
|
-
Data output = new Data.Builder()
|
|
84
|
-
.putString(FILEDEST, dest)
|
|
85
|
-
.putString(VERSION, version)
|
|
86
|
-
.putString(SESSIONKEY, sessionKey)
|
|
87
|
-
.putString(CHECKSUM, checksum)
|
|
88
|
-
.putBoolean(IS_MANIFEST, isManifest)
|
|
89
|
-
.build();
|
|
90
|
-
return Result.success(output);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
@NonNull
|
|
94
|
-
@Override
|
|
95
|
-
public Result doWork() {
|
|
96
|
-
try {
|
|
97
|
-
String url = getInputData().getString(URL);
|
|
98
|
-
String id = getInputData().getString(ID);
|
|
99
|
-
String documentsDir = getInputData().getString(DOCDIR);
|
|
100
|
-
String dest = getInputData().getString(FILEDEST);
|
|
101
|
-
String version = getInputData().getString(VERSION);
|
|
102
|
-
String sessionKey = getInputData().getString(SESSIONKEY);
|
|
103
|
-
String checksum = getInputData().getString(CHECKSUM);
|
|
104
|
-
String publicKey = getInputData().getString(PUBLIC_KEY);
|
|
105
|
-
boolean isManifest = getInputData().getBoolean(IS_MANIFEST, false);
|
|
106
|
-
|
|
107
|
-
Log.d(TAG, "doWork isManifest: " + isManifest);
|
|
108
|
-
|
|
109
|
-
if (isManifest) {
|
|
110
|
-
JSONArray manifest = DataManager.getInstance().getAndClearManifest();
|
|
111
|
-
if (manifest != null) {
|
|
112
|
-
handleManifestDownload(
|
|
113
|
-
id,
|
|
114
|
-
documentsDir,
|
|
115
|
-
dest,
|
|
116
|
-
version,
|
|
117
|
-
sessionKey,
|
|
118
|
-
publicKey,
|
|
119
|
-
manifest.toString()
|
|
120
|
-
);
|
|
121
|
-
return createSuccessResult(dest, version, sessionKey, checksum, true);
|
|
122
|
-
} else {
|
|
123
|
-
Log.e(TAG, "Manifest is null");
|
|
124
|
-
return createFailureResult("Manifest is null");
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
handleSingleFileDownload(
|
|
128
|
-
url,
|
|
129
|
-
id,
|
|
130
|
-
documentsDir,
|
|
131
|
-
dest,
|
|
132
|
-
version,
|
|
133
|
-
sessionKey,
|
|
134
|
-
checksum
|
|
135
|
-
);
|
|
136
|
-
return createSuccessResult(dest, version, sessionKey, checksum, false);
|
|
137
|
-
}
|
|
138
|
-
} catch (Exception e) {
|
|
139
|
-
Log.e(TAG, "Error in doWork", e);
|
|
140
|
-
return createFailureResult(e.getMessage());
|
|
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);
|
|
141
59
|
}
|
|
142
|
-
}
|
|
143
60
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
61
|
+
private void setProgress(int percent) {
|
|
62
|
+
Data progress = new Data.Builder().putInt(PERCENT, percent).build();
|
|
63
|
+
setProgressAsync(progress);
|
|
147
64
|
}
|
|
148
|
-
int percent = (int) (((double) downloadedBytes / contentLength) * 100);
|
|
149
|
-
percent = Math.max(10, percent);
|
|
150
|
-
percent = Math.min(70, percent);
|
|
151
|
-
return percent;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private void handleManifestDownload(
|
|
155
|
-
String id,
|
|
156
|
-
String documentsDir,
|
|
157
|
-
String dest,
|
|
158
|
-
String version,
|
|
159
|
-
String sessionKey,
|
|
160
|
-
String publicKey,
|
|
161
|
-
String manifestString
|
|
162
|
-
) {
|
|
163
|
-
try {
|
|
164
|
-
Log.d(TAG, "handleManifestDownload");
|
|
165
|
-
JSONArray manifest = new JSONArray(manifestString);
|
|
166
|
-
File destFolder = new File(documentsDir, dest);
|
|
167
|
-
File cacheFolder = new File(
|
|
168
|
-
getApplicationContext().getCacheDir(),
|
|
169
|
-
"capgo_downloads"
|
|
170
|
-
);
|
|
171
|
-
File builtinFolder = new File(
|
|
172
|
-
getApplicationContext().getFilesDir(),
|
|
173
|
-
"public"
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
// Ensure directories are created
|
|
177
|
-
if (!destFolder.exists() && !destFolder.mkdirs()) {
|
|
178
|
-
throw new IOException(
|
|
179
|
-
"Failed to create destination directory: " +
|
|
180
|
-
destFolder.getAbsolutePath()
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
if (!cacheFolder.exists() && !cacheFolder.mkdirs()) {
|
|
184
|
-
throw new IOException(
|
|
185
|
-
"Failed to create cache directory: " + cacheFolder.getAbsolutePath()
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
int totalFiles = manifest.length();
|
|
190
|
-
final AtomicLong completedFiles = new AtomicLong(0);
|
|
191
|
-
final AtomicBoolean hasError = new AtomicBoolean(false);
|
|
192
|
-
|
|
193
|
-
// Use more threads for I/O-bound operations
|
|
194
|
-
int threadCount = Math.min(64, Math.max(32, totalFiles));
|
|
195
|
-
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
|
196
|
-
List<Future<?>> futures = new ArrayList<>();
|
|
197
|
-
|
|
198
|
-
for (int i = 0; i < totalFiles; i++) {
|
|
199
|
-
JSONObject entry = manifest.getJSONObject(i);
|
|
200
|
-
String fileName = entry.getString("file_name");
|
|
201
|
-
String fileHash = entry.getString("file_hash");
|
|
202
|
-
String downloadUrl = entry.getString("download_url");
|
|
203
|
-
|
|
204
|
-
File targetFile = new File(destFolder, fileName);
|
|
205
|
-
File cacheFile = new File(
|
|
206
|
-
cacheFolder,
|
|
207
|
-
fileHash + "_" + new File(fileName).getName()
|
|
208
|
-
);
|
|
209
|
-
File builtinFile = new File(builtinFolder, fileName);
|
|
210
|
-
|
|
211
|
-
// Ensure parent directories of the target file exist
|
|
212
|
-
if (
|
|
213
|
-
!Objects.requireNonNull(targetFile.getParentFile()).exists() &&
|
|
214
|
-
!targetFile.getParentFile().mkdirs()
|
|
215
|
-
) {
|
|
216
|
-
throw new IOException(
|
|
217
|
-
"Failed to create parent directory for: " +
|
|
218
|
-
targetFile.getAbsolutePath()
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
65
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
fileHash,
|
|
238
|
-
sessionKey,
|
|
239
|
-
publicKey
|
|
240
|
-
);
|
|
241
|
-
}
|
|
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
|
+
}
|
|
242
81
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
} catch (Exception e) {
|
|
247
|
-
Log.e(TAG, "Error processing file: " + fileName, e);
|
|
248
|
-
hasError.set(true);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
futures.add(future);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Wait for all downloads to complete
|
|
255
|
-
for (Future<?> future : futures) {
|
|
82
|
+
@NonNull
|
|
83
|
+
@Override
|
|
84
|
+
public Result doWork() {
|
|
256
85
|
try {
|
|
257
|
-
|
|
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
|
+
}
|
|
258
111
|
} catch (Exception e) {
|
|
259
|
-
|
|
260
|
-
|
|
112
|
+
Log.e(TAG, "Error in doWork", e);
|
|
113
|
+
return createFailureResult(e.getMessage());
|
|
261
114
|
}
|
|
262
|
-
|
|
115
|
+
}
|
|
263
116
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
executor.shutdownNow();
|
|
117
|
+
private int calcTotalPercent(long downloadedBytes, long contentLength) {
|
|
118
|
+
if (contentLength <= 0) {
|
|
119
|
+
return 0;
|
|
268
120
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (hasError.get()) {
|
|
275
|
-
throw new IOException("One or more files failed to download");
|
|
276
|
-
}
|
|
277
|
-
} catch (Exception e) {
|
|
278
|
-
Log.e(TAG, "Error in handleManifestDownload", e);
|
|
121
|
+
int percent = (int) (((double) downloadedBytes / contentLength) * 100);
|
|
122
|
+
percent = Math.max(10, percent);
|
|
123
|
+
percent = Math.min(70, percent);
|
|
124
|
+
return percent;
|
|
279
125
|
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
private void handleSingleFileDownload(
|
|
283
|
-
String url,
|
|
284
|
-
String id,
|
|
285
|
-
String documentsDir,
|
|
286
|
-
String dest,
|
|
287
|
-
String version,
|
|
288
|
-
String sessionKey,
|
|
289
|
-
String checksum
|
|
290
|
-
) {
|
|
291
|
-
File target = new File(documentsDir, dest);
|
|
292
|
-
File infoFile = new File(documentsDir, UPDATE_FILE); // The file where the download progress (how much byte
|
|
293
|
-
// downloaded) is stored
|
|
294
|
-
File tempFile = new File(documentsDir, "temp" + ".tmp"); // Temp file, where the downloaded data is stored
|
|
295
|
-
try {
|
|
296
|
-
URL u = new URL(url);
|
|
297
|
-
HttpURLConnection httpConn = null;
|
|
298
|
-
try {
|
|
299
|
-
httpConn = (HttpURLConnection) u.openConnection();
|
|
300
|
-
|
|
301
|
-
// Reading progress file (if exist)
|
|
302
|
-
long downloadedBytes = 0;
|
|
303
|
-
|
|
304
|
-
if (infoFile.exists() && tempFile.exists()) {
|
|
305
|
-
try (
|
|
306
|
-
BufferedReader reader = new BufferedReader(new FileReader(infoFile))
|
|
307
|
-
) {
|
|
308
|
-
String updateVersion = reader.readLine();
|
|
309
|
-
if (!updateVersion.equals(version)) {
|
|
310
|
-
clearDownloadData(documentsDir);
|
|
311
|
-
} else {
|
|
312
|
-
downloadedBytes = tempFile.length();
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
} else {
|
|
316
|
-
clearDownloadData(documentsDir);
|
|
317
|
-
}
|
|
318
126
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
}
|
|
325
150
|
|
|
326
|
-
|
|
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
|
+
}
|
|
327
197
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
FileOutputStream outputStream = new FileOutputStream(
|
|
337
|
-
tempFile,
|
|
338
|
-
downloadedBytes > 0
|
|
339
|
-
)
|
|
340
|
-
) {
|
|
341
|
-
if (downloadedBytes == 0) {
|
|
342
|
-
try (
|
|
343
|
-
BufferedWriter writer = new BufferedWriter(
|
|
344
|
-
new FileWriter(infoFile)
|
|
345
|
-
)
|
|
346
|
-
) {
|
|
347
|
-
writer.write(String.valueOf(version));
|
|
348
|
-
}
|
|
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
|
+
}
|
|
349
206
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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();
|
|
357
216
|
}
|
|
358
217
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
int lastNotifiedPercent = 0;
|
|
362
|
-
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
363
|
-
outputStream.write(buffer, 0, bytesRead);
|
|
364
|
-
downloadedBytes += bytesRead;
|
|
365
|
-
// Saving progress (flushing every 100 Ko)
|
|
366
|
-
if (downloadedBytes % 102400 == 0) {
|
|
367
|
-
outputStream.flush();
|
|
368
|
-
}
|
|
369
|
-
// Computing percentage
|
|
370
|
-
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
371
|
-
while (lastNotifiedPercent + 10 <= percent) {
|
|
372
|
-
lastNotifiedPercent += 10;
|
|
373
|
-
// Artificial delay using CPU-bound calculation to take ~5 seconds
|
|
374
|
-
double result = 0;
|
|
375
|
-
setProgress(lastNotifiedPercent);
|
|
376
|
-
}
|
|
218
|
+
if (hasError.get()) {
|
|
219
|
+
throw new IOException("One or more files failed to download");
|
|
377
220
|
}
|
|
221
|
+
} catch (Exception e) {
|
|
222
|
+
Log.e(TAG, "Error in handleManifestDownload", e);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
378
225
|
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
}
|
|
381
327
|
|
|
382
|
-
|
|
383
|
-
|
|
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();
|
|
384
333
|
infoFile.delete();
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
} finally {
|
|
390
|
-
if (httpConn != null) {
|
|
391
|
-
httpConn.disconnect();
|
|
334
|
+
infoFile.createNewFile();
|
|
335
|
+
tempFile.createNewFile();
|
|
336
|
+
} catch (IOException e) {
|
|
337
|
+
e.printStackTrace();
|
|
392
338
|
}
|
|
393
|
-
}
|
|
394
|
-
} catch (OutOfMemoryError e) {
|
|
395
|
-
e.printStackTrace();
|
|
396
|
-
throw new RuntimeException("low_mem_fail");
|
|
397
|
-
} catch (Exception e) {
|
|
398
|
-
e.printStackTrace();
|
|
399
|
-
throw new RuntimeException(e.getLocalizedMessage());
|
|
400
339
|
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
private void clearDownloadData(String docDir) {
|
|
404
|
-
File tempFile = new File(docDir, "temp" + ".tmp");
|
|
405
|
-
File infoFile = new File(docDir, UPDATE_FILE);
|
|
406
|
-
try {
|
|
407
|
-
tempFile.delete();
|
|
408
|
-
infoFile.delete();
|
|
409
|
-
infoFile.createNewFile();
|
|
410
|
-
tempFile.createNewFile();
|
|
411
|
-
} catch (IOException e) {
|
|
412
|
-
e.printStackTrace();
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
340
|
|
|
416
|
-
|
|
341
|
+
// Helper methods
|
|
417
342
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
private void downloadAndVerify(
|
|
430
|
-
String downloadUrl,
|
|
431
|
-
File targetFile,
|
|
432
|
-
File cacheFile,
|
|
433
|
-
String expectedHash,
|
|
434
|
-
String sessionKey,
|
|
435
|
-
String publicKey
|
|
436
|
-
) throws Exception {
|
|
437
|
-
Log.d(TAG, "downloadAndVerify " + downloadUrl);
|
|
438
|
-
|
|
439
|
-
Request request = new Request.Builder().url(downloadUrl).build();
|
|
440
|
-
|
|
441
|
-
// Create a temporary file for the compressed data
|
|
442
|
-
File compressedFile = new File(
|
|
443
|
-
getApplicationContext().getCacheDir(),
|
|
444
|
-
"temp_" + targetFile.getName() + ".br"
|
|
445
|
-
);
|
|
446
|
-
|
|
447
|
-
try (Response response = client.newCall(request).execute()) {
|
|
448
|
-
if (!response.isSuccessful()) {
|
|
449
|
-
throw new IOException("Unexpected response code: " + response.code());
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Download compressed file
|
|
453
|
-
try (
|
|
454
|
-
ResponseBody responseBody = response.body();
|
|
455
|
-
FileOutputStream compressedFos = new FileOutputStream(compressedFile)
|
|
456
|
-
) {
|
|
457
|
-
if (responseBody == null) {
|
|
458
|
-
throw new IOException("Response body is null");
|
|
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);
|
|
459
351
|
}
|
|
352
|
+
}
|
|
460
353
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
+
}
|
|
494
422
|
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Delete the compressed file
|
|
498
|
-
compressedFile.delete();
|
|
499
|
-
String calculatedHash = CryptoCipherV2.calcChecksum(targetFile);
|
|
500
|
-
|
|
501
|
-
// Verify checksum
|
|
502
|
-
if (calculatedHash.equals(decryptedExpectedHash)) {
|
|
503
|
-
// Only cache if checksum is correct
|
|
504
|
-
copyFile(targetFile, cacheFile);
|
|
505
|
-
} else {
|
|
506
|
-
targetFile.delete();
|
|
507
|
-
throw new IOException(
|
|
508
|
-
"Checksum verification failed for " + targetFile.getName()
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
423
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
+
}
|
|
521
433
|
}
|
|
522
|
-
}
|
|
523
434
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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;
|
|
529
440
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
441
|
+
while ((bytesCount = fis.read(byteArray)) != -1) {
|
|
442
|
+
digest.update(byteArray, 0, bytesCount);
|
|
443
|
+
}
|
|
444
|
+
fis.close();
|
|
534
445
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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();
|
|
539
452
|
}
|
|
540
|
-
return sb.toString();
|
|
541
|
-
}
|
|
542
453
|
}
|