react-native-update 10.50.0 → 10.51.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.
- package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
- package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
- package/README-CN.md +14 -1
- package/README.md +14 -1
- package/android/jni/Android.mk +5 -1
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +76 -21
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
- package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
- package/cpp/update_flow_core/flow_json.cpp +522 -0
- package/cpp/update_flow_core/flow_json.h +103 -0
- package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
- package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
- package/cpp/update_flow_core/update_flow_core.cpp +301 -0
- package/cpp/update_flow_core/update_flow_core.h +77 -0
- package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
- package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
- package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
- package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
- package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
- package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
- package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +779 -61
- package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
- package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
- package/package.json +3 -1
- package/react-native-update.podspec +2 -0
- package/src/NativePushy.ts +14 -0
- package/src/client.ts +341 -118
- package/src/endpoint.ts +7 -29
- package/src/provider.tsx +38 -2
- package/src/type.ts +26 -0
- package/src/updateFlowCore.ts +320 -0
- package/src/utils.ts +1 -12
- package/src/isInRollout.ts +0 -87
- package/src/resolveCheckResult.ts +0 -30
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
import android.os.Build;
|
|
4
|
+
import android.util.Log;
|
|
5
|
+
import java.util.HashSet;
|
|
6
|
+
import java.util.concurrent.CountDownLatch;
|
|
7
|
+
import java.util.concurrent.TimeUnit;
|
|
8
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
9
|
+
import okhttp3.MediaType;
|
|
10
|
+
import okhttp3.OkHttpClient;
|
|
11
|
+
import okhttp3.Request;
|
|
12
|
+
import okhttp3.RequestBody;
|
|
13
|
+
import okhttp3.Response;
|
|
14
|
+
import org.json.JSONArray;
|
|
15
|
+
import org.json.JSONException;
|
|
16
|
+
import org.json.JSONObject;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Native cold-start update check (NATIVE_CHECKUPDATE_DESIGN §10): once per
|
|
20
|
+
* process, a few seconds after getBundleUrl, entirely independent of the app
|
|
21
|
+
* bundle — this is what lets a device bricked by a bad hot update pull the
|
|
22
|
+
* fixed version on the next launch. All decisions come from
|
|
23
|
+
* cpp/update_flow_core via NativeUpdateFlow; this class is IO glue only.
|
|
24
|
+
* Failures are silent and bounded: one round per launch, no retry storms, no
|
|
25
|
+
* version blacklisting.
|
|
26
|
+
*/
|
|
27
|
+
final class NativeCheckOrchestrator {
|
|
28
|
+
static final String KEY_CONFIG = "nativeConfig";
|
|
29
|
+
// Raw response cache for the JS side to reuse (§10.3), scoped to the
|
|
30
|
+
// exact logical request and native config that produced it.
|
|
31
|
+
static final String KEY_RESP_CACHE = "nativeCheckResp";
|
|
32
|
+
private static final int MAX_CHECK_HTTP_ATTEMPTS = 8;
|
|
33
|
+
private static final long DOWNLOAD_PHASE_TIMEOUT_SECONDS = 600;
|
|
34
|
+
|
|
35
|
+
private static final AtomicBoolean scheduled = new AtomicBoolean(false);
|
|
36
|
+
|
|
37
|
+
private NativeCheckOrchestrator() {
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static void schedule(final UpdateContext context, final String launchRolledBackVersion) {
|
|
41
|
+
if (UpdateContext.DEBUG) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!scheduled.compareAndSet(false, true)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
Thread thread = new Thread(new Runnable() {
|
|
48
|
+
@Override
|
|
49
|
+
public void run() {
|
|
50
|
+
try {
|
|
51
|
+
// Keep the check away from the cold-start critical path
|
|
52
|
+
// (§7 R5) — its result targets the NEXT launch anyway.
|
|
53
|
+
Thread.sleep(5000);
|
|
54
|
+
runOnce(context, launchRolledBackVersion);
|
|
55
|
+
} catch (Throwable e) {
|
|
56
|
+
// The rescue path must never take the app down with it.
|
|
57
|
+
Log.w(UpdateContext.TAG, "native check failed: " + e);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}, "pushy-native-check");
|
|
61
|
+
thread.setPriority(Thread.MIN_PRIORITY + 1);
|
|
62
|
+
thread.setDaemon(true);
|
|
63
|
+
thread.start();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static void runOnce(
|
|
67
|
+
UpdateContext context,
|
|
68
|
+
String launchRolledBackVersion
|
|
69
|
+
) throws JSONException {
|
|
70
|
+
// Sampled before any IO: a reset landing while this round runs must
|
|
71
|
+
// win over the round's decision.
|
|
72
|
+
final long resetGeneration = UpdateContext.getResetGeneration();
|
|
73
|
+
String configJson = context.getKv(KEY_CONFIG);
|
|
74
|
+
if (configJson == null || configJson.isEmpty()) {
|
|
75
|
+
// No persisted config (old integration / first ever launch): the
|
|
76
|
+
// native check silently does not run — this is the rollout gate.
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
JSONObject config;
|
|
80
|
+
try {
|
|
81
|
+
config = new JSONObject(configJson);
|
|
82
|
+
} catch (JSONException e) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (config.optBoolean("disabled", false)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
String appKey = config.optString("appKey", "");
|
|
89
|
+
if (appKey.isEmpty()) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
String packageVersion = config.optString(
|
|
93
|
+
"packageVersion", context.getPackageVersion());
|
|
94
|
+
if (packageVersion.isEmpty()) {
|
|
95
|
+
packageVersion = context.getPackageVersion();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
String currentVersion = context.getCurrentVersion();
|
|
99
|
+
// Snapshot captured on the launch path before getConstants consumes
|
|
100
|
+
// the one-shot rollback marker. Reading SharedPreferences here, five
|
|
101
|
+
// seconds later, would lose the guard and could forceBoot the version
|
|
102
|
+
// that this very launch just rolled back.
|
|
103
|
+
String rolledBackVersion = launchRolledBackVersion;
|
|
104
|
+
String uuid = context.getKv("uuid");
|
|
105
|
+
if (uuid == null) {
|
|
106
|
+
uuid = "";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
JSONObject identity = new JSONObject();
|
|
110
|
+
identity.put("packageVersion", packageVersion);
|
|
111
|
+
identity.put(
|
|
112
|
+
"currentVersion",
|
|
113
|
+
currentVersion == null ? JSONObject.NULL : currentVersion
|
|
114
|
+
);
|
|
115
|
+
identity.put("uuid", uuid);
|
|
116
|
+
if (rolledBackVersion != null) {
|
|
117
|
+
identity.put("rolledBackVersion", rolledBackVersion);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
JSONObject cInfo = new JSONObject();
|
|
121
|
+
cInfo.put("rnu", config.optString("rnu", ""));
|
|
122
|
+
cInfo.put("rn", config.optString("rn", ""));
|
|
123
|
+
// React Native's Platform.Version is the Android SDK integer; use the
|
|
124
|
+
// same value so this request can be fingerprinted against the JS one.
|
|
125
|
+
cInfo.put("os", "android " + Build.VERSION.SDK_INT);
|
|
126
|
+
cInfo.put("uuid", uuid);
|
|
127
|
+
|
|
128
|
+
JSONObject input = new JSONObject();
|
|
129
|
+
input.put("packageVersion", packageVersion);
|
|
130
|
+
input.put(
|
|
131
|
+
"currentVersion",
|
|
132
|
+
currentVersion == null ? JSONObject.NULL : currentVersion
|
|
133
|
+
);
|
|
134
|
+
input.put("buildTime", context.getBuildTime());
|
|
135
|
+
input.put("cInfo", cInfo);
|
|
136
|
+
input.put("supportedDiffVersion", NativeUpdateCore.supportedDiffVersion());
|
|
137
|
+
input.put("bundleHash", context.computeBundleHash());
|
|
138
|
+
|
|
139
|
+
String body = NativeUpdateFlow.buildCheckRequestBody(input.toString());
|
|
140
|
+
if (body == null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
String responseText = runCheckRequest(config, appKey, body);
|
|
145
|
+
if (responseText == null) {
|
|
146
|
+
Log.i(UpdateContext.TAG,
|
|
147
|
+
"native check: no endpoint reachable, giving up until next launch");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// Cache freshness is anchored to when the server response arrived,
|
|
151
|
+
// not to when a potentially long download/patch/activation finished.
|
|
152
|
+
final long responseAtSeconds = System.currentTimeMillis() / 1000;
|
|
153
|
+
|
|
154
|
+
String decisionJson = NativeUpdateFlow.handleCheckResponse(
|
|
155
|
+
responseText, identity.toString(), config.optString("afterDownload", ""));
|
|
156
|
+
if (decisionJson == null) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
JSONObject decision = new JSONObject(decisionJson);
|
|
160
|
+
if (!"download".equals(decision.optString("action"))) {
|
|
161
|
+
context.commitNativeCheckResult(
|
|
162
|
+
resetGeneration, null, null, false,
|
|
163
|
+
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
164
|
+
Log.i(UpdateContext.TAG,
|
|
165
|
+
"native check: nothing to do (" + decision.optString("reason") + ")");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
String hash = decision.optString("hash", "");
|
|
169
|
+
if (!UpdateContext.isSafePathComponent(hash)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
boolean downloaded = context.hasCompletedVersion(hash);
|
|
174
|
+
if (!downloaded) {
|
|
175
|
+
downloaded = performAttempts(
|
|
176
|
+
context, decision.optJSONArray("attempts"), hash, currentVersion);
|
|
177
|
+
}
|
|
178
|
+
if (!downloaded) {
|
|
179
|
+
// The native attempt has finished, so JS may safely reuse the
|
|
180
|
+
// response and retry through its own strategy chain.
|
|
181
|
+
context.commitNativeCheckResult(
|
|
182
|
+
resetGeneration, null, null, false,
|
|
183
|
+
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Version info (mirroring the JS side's setLocalHashInfo), the
|
|
188
|
+
// activation and the response cache all land in one atomic commit —
|
|
189
|
+
// see UpdateContext.commitNativeCheckResult.
|
|
190
|
+
String hashInfoJson = null;
|
|
191
|
+
JSONObject info = decision.optJSONObject("info");
|
|
192
|
+
if (info != null) {
|
|
193
|
+
JSONObject hashInfo = new JSONObject();
|
|
194
|
+
for (String key : new String[] {"name", "description", "metaInfo"}) {
|
|
195
|
+
Object value = info.opt(key);
|
|
196
|
+
if (value instanceof String) {
|
|
197
|
+
hashInfo.put(key, value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
hashInfoJson = hashInfo.toString();
|
|
201
|
+
}
|
|
202
|
+
// Silent strategies or a server-marked forceBoot version (per-version
|
|
203
|
+
// remote override — the brick rescue) activate for the next launch;
|
|
204
|
+
// otherwise activation stays with the JS side.
|
|
205
|
+
boolean activate = decision.optBoolean("activate", false);
|
|
206
|
+
boolean committed;
|
|
207
|
+
try {
|
|
208
|
+
committed = context.commitNativeCheckResult(
|
|
209
|
+
resetGeneration,
|
|
210
|
+
hash,
|
|
211
|
+
hashInfoJson,
|
|
212
|
+
activate,
|
|
213
|
+
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
|
|
214
|
+
} catch (Exception e) {
|
|
215
|
+
Log.w(UpdateContext.TAG, "native check: commit failed: " + e);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!committed) {
|
|
219
|
+
Log.i(UpdateContext.TAG, "native check: reset during round, dropping result");
|
|
220
|
+
} else if (activate) {
|
|
221
|
+
Log.i(UpdateContext.TAG,
|
|
222
|
+
"native check: downloaded " + hash + " and set for next launch");
|
|
223
|
+
} else {
|
|
224
|
+
Log.i(UpdateContext.TAG,
|
|
225
|
+
"native check: downloaded " + hash + ", activation left to JS");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private static String buildResponseCacheJson(
|
|
230
|
+
String configJson,
|
|
231
|
+
String requestBody,
|
|
232
|
+
String responseText,
|
|
233
|
+
long responseAtSeconds
|
|
234
|
+
) throws JSONException {
|
|
235
|
+
JSONObject cacheEntry = new JSONObject();
|
|
236
|
+
cacheEntry.put("ts", responseAtSeconds);
|
|
237
|
+
cacheEntry.put("body", responseText);
|
|
238
|
+
cacheEntry.put("request", requestBody);
|
|
239
|
+
cacheEntry.put("config", configJson);
|
|
240
|
+
return cacheEntry.toString();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private static final OkHttpClient httpClient = new OkHttpClient.Builder()
|
|
244
|
+
.connectTimeout(10, TimeUnit.SECONDS)
|
|
245
|
+
.readTimeout(10, TimeUnit.SECONDS)
|
|
246
|
+
.callTimeout(15, TimeUnit.SECONDS)
|
|
247
|
+
.build();
|
|
248
|
+
|
|
249
|
+
private static String httpRequest(String url, String postBody) {
|
|
250
|
+
try {
|
|
251
|
+
Request.Builder builder =
|
|
252
|
+
new Request.Builder().url(url).header("Accept", "application/json");
|
|
253
|
+
if (postBody != null) {
|
|
254
|
+
builder.post(RequestBody.create(
|
|
255
|
+
postBody, MediaType.parse("application/json; charset=utf-8")));
|
|
256
|
+
}
|
|
257
|
+
try (Response response = httpClient.newCall(builder.build()).execute()) {
|
|
258
|
+
if (!response.isSuccessful() || response.body() == null) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return response.body().string();
|
|
262
|
+
}
|
|
263
|
+
} catch (Exception e) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private static boolean isValidCheckResponse(String responseText) {
|
|
269
|
+
if (responseText == null) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
new JSONObject(responseText);
|
|
274
|
+
return true;
|
|
275
|
+
} catch (JSONException e) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private static String normalizeEndpointBase(String base) {
|
|
281
|
+
while (base.endsWith("/")) {
|
|
282
|
+
base = base.substring(0, base.length() - 1);
|
|
283
|
+
}
|
|
284
|
+
return base;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Sequential fallback over the ordered candidates (§5.1): one request at
|
|
289
|
+
* a time with its own timeout; after the configured round fails,
|
|
290
|
+
* queryUrls discovery merges remote candidates (excluding the
|
|
291
|
+
* already-tried) for one more round. No hedged race on purpose — this
|
|
292
|
+
* path is latency-insensitive.
|
|
293
|
+
*/
|
|
294
|
+
private static String runCheckRequest(JSONObject config, String appKey, String body) {
|
|
295
|
+
JSONArray endpoints = config.optJSONArray("endpoints");
|
|
296
|
+
String orderedJson = NativeUpdateFlow.orderEndpointCandidates(
|
|
297
|
+
endpoints == null ? "[]" : endpoints.toString(), Math.random());
|
|
298
|
+
JSONArray ordered;
|
|
299
|
+
try {
|
|
300
|
+
ordered = orderedJson == null ? new JSONArray() : new JSONArray(orderedJson);
|
|
301
|
+
} catch (JSONException e) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
HashSet<String> tried = new HashSet<>();
|
|
305
|
+
int httpAttempts = 0;
|
|
306
|
+
for (int i = 0; i < ordered.length(); i++) {
|
|
307
|
+
String base = normalizeEndpointBase(ordered.optString(i, ""));
|
|
308
|
+
if (base.isEmpty() || !tried.add(base)) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (httpAttempts++ >= MAX_CHECK_HTTP_ATTEMPTS) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
String response = httpRequest(base + "/checkUpdate/" + appKey, body);
|
|
315
|
+
if (isValidCheckResponse(response)) {
|
|
316
|
+
return response;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
JSONArray queryUrls = config.optJSONArray("queryUrls");
|
|
320
|
+
if (queryUrls == null) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
for (int i = 0; i < queryUrls.length(); i++) {
|
|
324
|
+
String listUrl = queryUrls.optString(i, "");
|
|
325
|
+
if (listUrl.isEmpty()) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (httpAttempts++ >= MAX_CHECK_HTTP_ATTEMPTS) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
String listText = httpRequest(listUrl, null);
|
|
332
|
+
if (listText == null) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
JSONArray remote;
|
|
336
|
+
try {
|
|
337
|
+
remote = new JSONArray(listText);
|
|
338
|
+
} catch (JSONException e) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
for (int j = 0; j < remote.length(); j++) {
|
|
342
|
+
String base = normalizeEndpointBase(remote.optString(j, ""));
|
|
343
|
+
if (base.isEmpty() || tried.contains(base)) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (httpAttempts++ >= MAX_CHECK_HTTP_ATTEMPTS) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
tried.add(base);
|
|
350
|
+
String response = httpRequest(base + "/checkUpdate/" + appKey, body);
|
|
351
|
+
if (isValidCheckResponse(response)) {
|
|
352
|
+
return response;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// One successfully fetched remote list is enough.
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private static boolean performAttempts(
|
|
362
|
+
UpdateContext context, JSONArray attempts, String hash, String originHash
|
|
363
|
+
) {
|
|
364
|
+
if (attempts == null) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
final long incrementalDeadlineNanos = System.nanoTime()
|
|
368
|
+
+ TimeUnit.SECONDS.toNanos(DOWNLOAD_PHASE_TIMEOUT_SECONDS);
|
|
369
|
+
long fullDeadlineNanos = 0;
|
|
370
|
+
for (int i = 0; i < attempts.length(); i++) {
|
|
371
|
+
JSONObject attempt = attempts.optJSONObject(i);
|
|
372
|
+
if (attempt == null) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
String type = attempt.optString("type");
|
|
376
|
+
if ("diff".equals(type) && (originHash == null || originHash.isEmpty())) {
|
|
377
|
+
// diff patches from the running version; none is running.
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
final boolean isFullAttempt = !"diff".equals(type) && !"pdiff".equals(type);
|
|
381
|
+
if (isFullAttempt && fullDeadlineNanos == 0) {
|
|
382
|
+
// Incremental failures must not consume the last-resort full
|
|
383
|
+
// download's budget. Each phase gets one bounded 10min window.
|
|
384
|
+
fullDeadlineNanos = System.nanoTime()
|
|
385
|
+
+ TimeUnit.SECONDS.toNanos(DOWNLOAD_PHASE_TIMEOUT_SECONDS);
|
|
386
|
+
}
|
|
387
|
+
final long deadlineNanos = isFullAttempt
|
|
388
|
+
? fullDeadlineNanos : incrementalDeadlineNanos;
|
|
389
|
+
JSONArray urls = attempt.optJSONArray("urls");
|
|
390
|
+
if (urls == null) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
for (int j = 0; j < urls.length(); j++) {
|
|
394
|
+
String url = urls.optString(j, "");
|
|
395
|
+
if (url.isEmpty()) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
// Check before enqueueing: once the phase budget is gone we
|
|
399
|
+
// must not launch an orphan download that outlives the round.
|
|
400
|
+
long remainingNanos = deadlineNanos - System.nanoTime();
|
|
401
|
+
if (remainingNanos <= 0) {
|
|
402
|
+
if (isFullAttempt) {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
final CountDownLatch latch = new CountDownLatch(1);
|
|
408
|
+
final AtomicBoolean succeeded = new AtomicBoolean(false);
|
|
409
|
+
final String attemptType = type;
|
|
410
|
+
UpdateContext.DownloadFileListener listener =
|
|
411
|
+
new UpdateContext.DownloadFileListener() {
|
|
412
|
+
@Override
|
|
413
|
+
public void onDownloadCompleted(DownloadTaskParams params) {
|
|
414
|
+
succeeded.set(true);
|
|
415
|
+
latch.countDown();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@Override
|
|
419
|
+
public void onDownloadFailed(Throwable error) {
|
|
420
|
+
Log.i(UpdateContext.TAG, "native check: " + attemptType
|
|
421
|
+
+ " attempt failed: " + error);
|
|
422
|
+
latch.countDown();
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
if ("diff".equals(type)) {
|
|
426
|
+
context.downloadPatchFromPpk(
|
|
427
|
+
url, hash, originHash, listener, deadlineNanos);
|
|
428
|
+
} else if ("pdiff".equals(type)) {
|
|
429
|
+
context.downloadPatchFromApk(
|
|
430
|
+
url, hash, listener, deadlineNanos);
|
|
431
|
+
} else {
|
|
432
|
+
context.downloadFullUpdate(
|
|
433
|
+
url, hash, listener, deadlineNanos);
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
if (!latch.await(remainingNanos, TimeUnit.NANOSECONDS)) {
|
|
437
|
+
Log.w(UpdateContext.TAG,
|
|
438
|
+
"native check: download phase timed out during " + type);
|
|
439
|
+
if (isFullAttempt) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
} catch (InterruptedException e) {
|
|
445
|
+
Thread.currentThread().interrupt();
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
if (succeeded.get()) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package cn.reactnative.modules.update;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JNI bindings to the shared update-flow decision layer
|
|
5
|
+
* (cpp/update_flow_core). String-in/string-out JSON on purpose — it matches
|
|
6
|
+
* the decision layer's own boundary and keeps this surface trivially stable.
|
|
7
|
+
* A null return means the input did not parse; callers skip the check round.
|
|
8
|
+
*/
|
|
9
|
+
final class NativeUpdateFlow {
|
|
10
|
+
static {
|
|
11
|
+
NativeUpdateCore.ensureLoaded();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private NativeUpdateFlow() {
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static native String buildCheckRequestBody(String inputJson);
|
|
18
|
+
|
|
19
|
+
static native String orderEndpointCandidates(String endpointsJson, double randomSample);
|
|
20
|
+
|
|
21
|
+
static native String handleCheckResponse(
|
|
22
|
+
String responseText, String identityJson, String afterDownload);
|
|
23
|
+
}
|