-
Notifications
You must be signed in to change notification settings - Fork 280
Expand file tree
/
Copy pathUpdateContext.java
More file actions
439 lines (382 loc) Β· 15.1 KB
/
Copy pathUpdateContext.java
File metadata and controls
439 lines (382 loc) Β· 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
package cn.reactnative.modules.update;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import com.facebook.react.ReactInstanceManager;
import java.io.File;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class UpdateContext {
static {
NativeUpdateCore.ensureLoaded();
}
static final String TAG = "react-native-update";
static final boolean DEBUG = BuildConfig.DEBUG;
private final Context context;
private final File rootDir;
private final Executor executor;
private final SharedPreferences sp;
private ReactInstanceManager reactInstanceManager;
private boolean isUsingBundleUrl;
private boolean ignoreRollback;
private static final int STATE_OP_SWITCH_VERSION = 1;
private static final int STATE_OP_MARK_SUCCESS = 2;
private static final int STATE_OP_ROLLBACK = 3;
private static final int STATE_OP_CLEAR_FIRST_TIME = 4;
private static final int STATE_OP_CLEAR_ROLLBACK_MARK = 5;
private static final int STATE_OP_RESOLVE_LAUNCH = 6;
private static final String KEY_FIRST_LOAD_MARKED = "firstLoadMarked";
// Singleton instance
private static volatile UpdateContext sInstance;
private static final Object sLock = new Object();
private static ReactInstanceManager pendingReactInstanceManager;
private static native StateCoreResult syncStateWithBinaryVersion(
String packageVersion,
String buildTime,
StateCoreResult state
);
private static native StateCoreResult runStateCore(
int operation,
StateCoreResult state,
String stringArg,
boolean flagA,
boolean flagB
);
private UpdateContext(Context context) {
this.context = context.getApplicationContext();
this.executor = Executors.newSingleThreadExecutor();
this.rootDir = new File(this.context.getFilesDir(), "_update");
if (!rootDir.exists() && !rootDir.mkdirs() && !rootDir.exists()) {
throw new IllegalStateException("Failed to create update root dir: " + rootDir);
}
this.sp = this.context.getSharedPreferences("update", Context.MODE_PRIVATE);
this.reactInstanceManager = pendingReactInstanceManager;
String packageVersion = getPackageVersion();
String buildTime = getBuildTime();
StateCoreResult nextState = syncStateWithBinaryVersion(
packageVersion,
buildTime,
getStateSnapshot()
);
if (nextState.changed) {
// Execute cleanUp before clearing SharedPreferences to avoid race condition
this.cleanUp();
SharedPreferences.Editor editor = this.sp.edit();
editor.clear();
applyState(editor, nextState);
persistEditor(editor, "sync state with binary version");
}
}
public String getRootDir() {
return rootDir.toString();
}
public String getPackageVersion() {
PackageManager pm = context.getPackageManager();
PackageInfo pi = null;
try {
pi = pm.getPackageInfo(context.getPackageName(), 0);
return pi.versionName;
} catch( PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}
public String getBuildTime() {
return context.getString(R.string.pushy_build_time);
}
public boolean getIsUsingBundleUrl() {
return isUsingBundleUrl;
}
private void enqueue(DownloadTaskParams params) {
executor.execute(new DownloadTask(context, params));
}
public interface DownloadFileListener {
void onDownloadCompleted(DownloadTaskParams params);
void onDownloadFailed(Throwable error);
}
public void downloadFullUpdate(String url, String hash, DownloadFileListener listener) {
DownloadTaskParams params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FULL;
params.url = url;
params.hash = hash;
params.listener = listener;
params.targetFile = new File(rootDir, hash + ".ppk");
params.unzipDirectory = new File(rootDir, hash);
enqueue(params);
}
public void downloadFile(String url, String hash, String fileName, DownloadFileListener listener) {
DownloadTaskParams params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_PLAIN_DOWNLOAD;
params.url = url;
params.hash = hash;
params.listener = listener;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && fileName.equals("update.apk")) {
params.targetFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "pushy_update.apk");
} else {
params.targetFile = new File(rootDir, fileName);
}
// params.unzipDirectory = new File(rootDir, hash);
enqueue(params);
}
public void downloadPatchFromApk(String url, String hash, DownloadFileListener listener) {
DownloadTaskParams params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_APK;
params.url = url;
params.hash = hash;
params.listener = listener;
params.targetFile = new File(rootDir, hash + ".apk.patch");
params.unzipDirectory = new File(rootDir, hash);
enqueue(params);
}
public void downloadPatchFromPpk(String url, String hash, String originHash, DownloadFileListener listener) {
DownloadTaskParams params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_PATCH_FROM_PPK;
params.url = url;
params.hash = hash;
params.originHash = originHash;
params.listener = listener;
params.targetFile = new File(rootDir, originHash + "-" + hash + ".ppk.patch");
params.unzipDirectory = new File(rootDir, hash);
params.originDirectory = new File(rootDir, originHash);
enqueue(params);
}
private StateCoreResult getStateSnapshot() {
StateCoreResult state = new StateCoreResult();
state.packageVersion = sp.getString("packageVersion", null);
state.buildTime = sp.getString("buildTime", null);
state.currentVersion = sp.getString("currentVersion", null);
state.lastVersion = sp.getString("lastVersion", null);
state.firstTime = sp.getBoolean("firstTime", false);
state.firstTimeOk = sp.getBoolean("firstTimeOk", true);
state.rolledBackVersion = sp.getString("rolledBackVersion", null);
return state;
}
private static void putNullableString(
SharedPreferences.Editor editor,
String key,
String value
) {
if (value == null) {
editor.remove(key);
} else {
editor.putString(key, value);
}
}
private void applyState(SharedPreferences.Editor editor, StateCoreResult state) {
putNullableString(editor, "packageVersion", state.packageVersion);
putNullableString(editor, "buildTime", state.buildTime);
putNullableString(editor, "currentVersion", state.currentVersion);
putNullableString(editor, "lastVersion", state.lastVersion);
editor.putBoolean("firstTime", state.firstTime);
editor.putBoolean("firstTimeOk", state.firstTimeOk);
putNullableString(editor, "rolledBackVersion", state.rolledBackVersion);
}
private void persistEditor(SharedPreferences.Editor editor, String reason) {
// A lost state write can mean a missed rollback or a version switch
// that silently never happens, so this must be visible in release too.
if (!editor.commit()) {
Log.e(TAG, "Failed to persist update state for " + reason);
}
}
public void switchVersion(String hash) {
if (!new File(rootDir, hash+"/index.bundlejs").exists()) {
throw new IllegalStateException("Bundle version " + hash + " not found.");
}
StateCoreResult currentState = getStateSnapshot();
StateCoreResult nextState = runStateCore(
STATE_OP_SWITCH_VERSION,
currentState,
hash
,
false,
false
);
SharedPreferences.Editor editor = sp.edit();
applyState(editor, nextState);
persistEditor(editor, "switch version");
ignoreRollback = false;
}
public void setKv(String key, String value) {
SharedPreferences.Editor editor = sp.edit();
editor.putString(key, value);
persistEditor(editor, "set key " + key);
}
public String getKv(String key) {
return sp.getString(key, null);
}
public String getCurrentVersion() {
return sp.getString("currentVersion", null);
}
public boolean isFirstTime() {
return sp.getBoolean("firstTime", false);
}
public boolean consumeFirstLoadMarker() {
boolean isFirstLoadMarked = sp.getBoolean(KEY_FIRST_LOAD_MARKED, false);
if (isFirstLoadMarked) {
SharedPreferences.Editor editor = sp.edit();
editor.remove(KEY_FIRST_LOAD_MARKED);
persistEditor(editor, "clear first load marker");
}
return isFirstLoadMarked;
}
public String rolledBackVersion() {
return sp.getString("rolledBackVersion", null);
}
public void markSuccess() {
if (!BuildConfig.DEBUG) {
StateCoreResult currentState = getStateSnapshot();
StateCoreResult nextState = runStateCore(
STATE_OP_MARK_SUCCESS,
currentState,
null,
false,
false
);
SharedPreferences.Editor editor = sp.edit();
applyState(editor, nextState);
if (nextState.staleVersionToDelete != null) {
editor.remove("hash_" + nextState.staleVersionToDelete);
}
persistEditor(editor, "mark success");
this.cleanUp();
}
}
public void clearFirstTime() {
StateCoreResult currentState = getStateSnapshot();
StateCoreResult nextState = runStateCore(
STATE_OP_CLEAR_FIRST_TIME,
currentState,
null,
false,
false
);
SharedPreferences.Editor editor = sp.edit();
applyState(editor, nextState);
editor.remove(KEY_FIRST_LOAD_MARKED);
persistEditor(editor, "clear first time");
this.cleanUp();
}
public void clearRollbackMark() {
StateCoreResult currentState = getStateSnapshot();
StateCoreResult nextState = runStateCore(
STATE_OP_CLEAR_ROLLBACK_MARK,
currentState,
null,
false,
false
);
SharedPreferences.Editor editor = sp.edit();
applyState(editor, nextState);
persistEditor(editor, "clear rollback mark");
this.cleanUp();
}
public static void setCustomInstanceManager(ReactInstanceManager instanceManager) {
synchronized (sLock) {
pendingReactInstanceManager = instanceManager;
if (sInstance != null) {
sInstance.reactInstanceManager = instanceManager;
}
}
}
public ReactInstanceManager getCustomReactInstanceManager() {
return reactInstanceManager;
}
/**
* Get singleton instance of UpdateContext
*/
public static UpdateContext getInstance(Context context) {
if (sInstance == null) {
synchronized (sLock) {
if (sInstance == null) {
sInstance = new UpdateContext(context.getApplicationContext());
}
}
}
return sInstance;
}
public static String getBundleUrl(Context context) {
return getInstance(context).getBundleUrl();
}
public static String getBundleUrl(Context context, String defaultAssetsUrl) {
return getInstance(context).getBundleUrl(defaultAssetsUrl);
}
public String getBundleUrl() {
return this.getBundleUrl((String) null);
}
public String getBundleUrl(String defaultAssetsUrl) {
isUsingBundleUrl = true;
StateCoreResult currentState = getStateSnapshot();
StateCoreResult launchState = runStateCore(
STATE_OP_RESOLVE_LAUNCH,
currentState,
null,
ignoreRollback,
true
);
if (launchState.didRollback) {
// The crash-protection rollback: the new version never called
// markSuccess. Keep this visible in release logs.
Log.e(TAG, "Version " + currentState.currentVersion
+ " was not marked as successful, rolling back to "
+ launchState.currentVersion);
}
if (launchState.didRollback || launchState.consumedFirstTime) {
SharedPreferences.Editor editor = sp.edit();
applyState(editor, launchState);
if (launchState.consumedFirstTime) {
editor.putBoolean(KEY_FIRST_LOAD_MARKED, true);
}
persistEditor(editor, "resolve launch");
}
if (launchState.consumedFirstTime) {
// bundleURL may be resolved multiple times in one process.
ignoreRollback = true;
}
String currentVersion = launchState.loadVersion;
if (currentVersion == null) {
return defaultAssetsUrl;
}
// Guard the rollback chain against cycles: a corrupted state returning
// an already-visited version would otherwise spin this loop forever on
// the main thread.
java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
while (currentVersion != null && visitedVersions.add(currentVersion)) {
File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
if (!bundleFile.exists()) {
Log.e(TAG, "Bundle version " + currentVersion + " not found.");
currentVersion = this.rollBack();
continue;
}
return bundleFile.toString();
}
return defaultAssetsUrl;
}
private String rollBack() {
StateCoreResult currentState = getStateSnapshot();
StateCoreResult nextState = runStateCore(
STATE_OP_ROLLBACK,
currentState,
null,
false,
false
);
Log.e(TAG, "Rolling back version " + currentState.currentVersion
+ " to " + nextState.currentVersion);
SharedPreferences.Editor editor = sp.edit();
applyState(editor, nextState);
persistEditor(editor, "rollback");
return nextState.currentVersion;
}
private void cleanUp() {
DownloadTaskParams params = new DownloadTaskParams();
params.type = DownloadTaskParams.TASK_TYPE_CLEANUP;
params.hash = sp.getString("currentVersion", null);
params.originHash = sp.getString("lastVersion", null);
params.unzipDirectory = rootDir;
enqueue(params);
}
}