@hot-updater/react-native 1.0.7 → 1.1.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/HotUpdater.podspec +14 -2
- package/Package.swift +24 -3
- package/android/build.gradle +2 -6
- package/android/hot-updater-fingerprint.gradle +135 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
- package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
- package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
- package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
- package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
- package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
- package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +492 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
- package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
- package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
- package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
- package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
- package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
- package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
- package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
- package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
- package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
- package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
- package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
- package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
- package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
- package/android/src/newarch/HotUpdater.kt +53 -15
- package/android/src/newarch/HotUpdaterModule.kt +42 -17
- package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
- package/android/src/oldarch/HotUpdater.kt +51 -12
- package/android/src/oldarch/HotUpdaterModule.kt +43 -17
- package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
- package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
- package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
- package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
- package/ios/HotUpdater/Internal/CohortService.swift +8 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
- package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
- package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
- package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
- package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
- package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
- package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
- package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
- package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
- package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
- package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
- package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
- package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
- package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
- package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
- package/ios/HotUpdater/Package.swift +11 -0
- package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
- package/ios/HotUpdater/Public/HotUpdater.h +21 -2
- package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
- package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
- package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
- package/lib/commonjs/checkForUpdate.js +3 -1
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +9 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +24 -79
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/nativeUpdateEvents.js +66 -0
- package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +10 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +19 -73
- package/lib/module/native.js.map +1 -1
- package/lib/module/nativeUpdateEvents.js +61 -0
- package/lib/module/nativeUpdateEvents.js.map +1 -0
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +4 -14
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +9 -46
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +4 -14
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +9 -46
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
- package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/checkForUpdate.ts +7 -1
- package/src/index.ts +9 -17
- package/src/native.ts +19 -91
- package/src/nativeUpdateEvents.ts +77 -0
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +20 -3
- package/android/libs/README.md +0 -50
- package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
- package/android/src/bridge/BridgeReactInstance.kt +0 -38
- package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
- package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
- package/android/src/newarch/ReactIntegrationManager.kt +0 -185
- package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The native counterpart of the instance JavaScript's `HotUpdater.init`
|
|
7
|
+
* returns, for apps that check for and stage updates before React Native
|
|
8
|
+
* starts: [checkForUpdate], the returned update's `updateBundle`,
|
|
9
|
+
* [resetChannel], and the getters, with this instance's server and request
|
|
10
|
+
* defaults. [getJSBundleFile] is the bundle React Native starts with.
|
|
11
|
+
*
|
|
12
|
+
* An update staged before [getJSBundleFile] is read applies at this launch;
|
|
13
|
+
* one staged after it applies at the next launch. Create one with
|
|
14
|
+
* `HotUpdater.init`. Instances with the same [HotUpdaterOptions.name] share
|
|
15
|
+
* one native state; `main` is the one React Native's `HotUpdater` uses.
|
|
16
|
+
*/
|
|
17
|
+
class HotUpdaterInstance internal constructor(
|
|
18
|
+
private val context: Context,
|
|
19
|
+
val options: HotUpdaterOptions,
|
|
20
|
+
private val environment: HotUpdaterInstanceEnvironment,
|
|
21
|
+
) {
|
|
22
|
+
internal companion object {
|
|
23
|
+
/**
|
|
24
|
+
* @throws IllegalArgumentException when the name is invalid, or was
|
|
25
|
+
* already created with another configuration
|
|
26
|
+
*/
|
|
27
|
+
fun create(
|
|
28
|
+
context: Context,
|
|
29
|
+
options: HotUpdaterOptions,
|
|
30
|
+
): HotUpdaterInstance {
|
|
31
|
+
val environment = HotUpdaterInstanceEnvironment.of(options)
|
|
32
|
+
if (environment.isMain) {
|
|
33
|
+
MainInstanceConfiguration.shared.claim(options)
|
|
34
|
+
} else {
|
|
35
|
+
HotUpdaterInstanceRegistry.shared.register(context, environment)
|
|
36
|
+
}
|
|
37
|
+
return HotUpdaterInstance(context, options, environment)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private val impl: HotUpdaterImpl
|
|
42
|
+
get() =
|
|
43
|
+
if (environment.isMain) {
|
|
44
|
+
HotUpdater.getInstance(context)
|
|
45
|
+
} else {
|
|
46
|
+
HotUpdaterInstanceRegistry.shared.impl(context, environment)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The update the app should apply, or null when there is none. Unlike
|
|
51
|
+
* JavaScript, which returns null and calls `onError`, a failed check
|
|
52
|
+
* throws; it is still reported to plugins as `onUpdateError`. Waits for
|
|
53
|
+
* other native update work; cancelling the caller ends the wait.
|
|
54
|
+
*/
|
|
55
|
+
suspend fun checkForUpdate(options: HotUpdaterCheckOptions): HotUpdaterUpdate? =
|
|
56
|
+
NativeUpdateCheck.checkForUpdate(context, environment.name, { impl }, NativeCheckRequest(this.options, options))
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The bundle React Native starts with: the staged update, or the bundle
|
|
60
|
+
* embedded in the app. Reading it fixes this launch's bundle.
|
|
61
|
+
*/
|
|
62
|
+
fun getJSBundleFile(): String =
|
|
63
|
+
if (environment.isMain) {
|
|
64
|
+
impl.getJSBundleFile()
|
|
65
|
+
} else {
|
|
66
|
+
HotUpdaterInstanceRegistry.shared.launchURL(context, environment)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Clears the runtime channel switch and returns to the default channel's
|
|
71
|
+
* bundle at the next launch, once other native update work finishes.
|
|
72
|
+
*/
|
|
73
|
+
suspend fun resetChannel(): Boolean = NativeUpdateCheck.resetChannel(context, environment.name) { impl }
|
|
74
|
+
|
|
75
|
+
fun getAppVersion(): String? = HotUpdaterImpl.getAppVersion(context)
|
|
76
|
+
|
|
77
|
+
/** The bundle this launch runs, or null for the built-in bundle or before [getJSBundleFile] is read. */
|
|
78
|
+
fun getBundleId(): String? = impl.getRunningBundleId()
|
|
79
|
+
|
|
80
|
+
fun getMinBundleId(): String = HotUpdaterImpl.getMinBundleId()
|
|
81
|
+
|
|
82
|
+
fun getChannel(): String = impl.getChannel()
|
|
83
|
+
|
|
84
|
+
fun getDefaultChannel(): String = impl.getDefaultChannel()
|
|
85
|
+
|
|
86
|
+
fun getFingerprintHash(): String? = impl.getFingerprintHash()
|
|
87
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import java.io.File
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where an instance keeps its native state and which configuration it reads,
|
|
8
|
+
* chosen by [HotUpdaterOptions.name].
|
|
9
|
+
*
|
|
10
|
+
* `main` is the state React Native's `HotUpdater`, the TurboModule, and the
|
|
11
|
+
* static entry points use: every path Hot Updater has always used, and the
|
|
12
|
+
* configuration its [HotUpdaterOptions] and the deprecated
|
|
13
|
+
* `HotUpdater.configure` record in [HotUpdaterConfig] before the manifest. Any
|
|
14
|
+
* other name keeps its state beside main's, never inside `bundle-store`, whose
|
|
15
|
+
* sweeps delete entries they don't know, and reads its configuration from its
|
|
16
|
+
* [HotUpdaterOptions] and then the manifest, never from [HotUpdaterConfig].
|
|
17
|
+
* The install id and the cohort belong to the device and stay shared.
|
|
18
|
+
*/
|
|
19
|
+
internal class HotUpdaterInstanceEnvironment private constructor(
|
|
20
|
+
val name: String,
|
|
21
|
+
private val channel: String?,
|
|
22
|
+
private val fingerprintHash: String?,
|
|
23
|
+
private val publicKey: String?,
|
|
24
|
+
private val bundleAssetName: String,
|
|
25
|
+
// Main's impl: the values it was created with, which it reads instead of
|
|
26
|
+
// HotUpdaterConfig. Null for main's static entry points, which read
|
|
27
|
+
// HotUpdaterConfig, and for named instances.
|
|
28
|
+
private val mainValues: MainImplValues? = null,
|
|
29
|
+
) {
|
|
30
|
+
companion object {
|
|
31
|
+
const val MAIN_NAME = "main"
|
|
32
|
+
const val DEFAULT_BUNDLE_ASSET_NAME = "index.android.bundle"
|
|
33
|
+
|
|
34
|
+
/** The family every main isolation key is in, whatever its version and channel. */
|
|
35
|
+
const val MAIN_PREFERENCES_PREFIX = "HotUpdaterPrefs_"
|
|
36
|
+
|
|
37
|
+
// Main's sweep deletes every other HotUpdaterPrefs_ file, so named
|
|
38
|
+
// families must not start with it. Names have no underscore, so one
|
|
39
|
+
// name's family is never a prefix of another's.
|
|
40
|
+
private const val INSTANCE_PREFERENCES_PREFIX = "HotUpdaterInstancePrefs_"
|
|
41
|
+
|
|
42
|
+
/** Beside `bundle-store` in the app's files directory: one directory per named instance. */
|
|
43
|
+
const val INSTANCES_DIRECTORY = "hot-updater-instances"
|
|
44
|
+
private const val INSTANCES_NO_BACKUP_DIRECTORY = "instances"
|
|
45
|
+
private const val RELEASE_CATALOG_CACHE_DIRECTORY = "release-catalog-cache"
|
|
46
|
+
private val NAME_PATTERN = Regex("^[a-z0-9-]{1,32}$")
|
|
47
|
+
|
|
48
|
+
val main = HotUpdaterInstanceEnvironment(MAIN_NAME, null, null, null, DEFAULT_BUNDLE_ASSET_NAME)
|
|
49
|
+
|
|
50
|
+
/** Main, fixed to [values]: what main's impl reads, so a later write to HotUpdaterConfig never reaches it. */
|
|
51
|
+
fun main(values: MainImplValues) = HotUpdaterInstanceEnvironment(MAIN_NAME, null, null, null, DEFAULT_BUNDLE_ASSET_NAME, values)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The environment of [options]. `main`'s configuration lives in
|
|
55
|
+
* [HotUpdaterConfig], where `HotUpdater.init` records its options.
|
|
56
|
+
* @throws IllegalArgumentException when the name is not 1 to 32
|
|
57
|
+
* lowercase letters, digits, or hyphens, or when `main` names a
|
|
58
|
+
* bundle asset other than its own
|
|
59
|
+
*/
|
|
60
|
+
fun of(options: HotUpdaterOptions): HotUpdaterInstanceEnvironment {
|
|
61
|
+
require(NAME_PATTERN.matches(options.name)) {
|
|
62
|
+
"HotUpdater instance name must be 1 to 32 lowercase letters, digits, or hyphens: \"${options.name}\""
|
|
63
|
+
}
|
|
64
|
+
if (options.name == MAIN_NAME) {
|
|
65
|
+
// React Native's own HotUpdater starts main from this asset.
|
|
66
|
+
require(options.bundleAssetName == DEFAULT_BUNDLE_ASSET_NAME) {
|
|
67
|
+
"HotUpdater instance \"main\" launches $DEFAULT_BUNDLE_ASSET_NAME, not \"${options.bundleAssetName}\""
|
|
68
|
+
}
|
|
69
|
+
return main
|
|
70
|
+
}
|
|
71
|
+
// Main's runtime is told apart by the bundle it runs, so a named
|
|
72
|
+
// instance embeds a bundle of its own.
|
|
73
|
+
require(options.bundleAssetName != DEFAULT_BUNDLE_ASSET_NAME) {
|
|
74
|
+
"A named HotUpdater instance needs its own bundleAssetName, not $DEFAULT_BUNDLE_ASSET_NAME: \"${options.name}\""
|
|
75
|
+
}
|
|
76
|
+
return HotUpdaterInstanceEnvironment(
|
|
77
|
+
name = options.name,
|
|
78
|
+
channel = options.channel?.takeIf { it.isNotEmpty() },
|
|
79
|
+
fingerprintHash = options.fingerprintHash?.takeIf { it.isNotEmpty() },
|
|
80
|
+
publicKey = options.publicKey?.takeIf { it.isNotEmpty() },
|
|
81
|
+
bundleAssetName = options.bundleAssetName,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
val isMain: Boolean
|
|
87
|
+
get() = name == MAIN_NAME
|
|
88
|
+
|
|
89
|
+
/** The directory that holds `bundle-store` and `bundle-manifest-temp`. */
|
|
90
|
+
fun filesRoot(context: Context): File? =
|
|
91
|
+
if (isMain) {
|
|
92
|
+
context.filesDir
|
|
93
|
+
} else {
|
|
94
|
+
context.filesDir?.let { File(File(it, INSTANCES_DIRECTORY), name) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The client plugins' key-value store, which also holds the native update event queue. */
|
|
98
|
+
fun keyValueStorageFile(context: Context): File =
|
|
99
|
+
if (isMain) {
|
|
100
|
+
NoBackupStorage.file(context, KeyValueStorageService.FILENAME)
|
|
101
|
+
} else {
|
|
102
|
+
File(noBackupRoot(context), KeyValueStorageService.FILENAME)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Each directory trims to its own entries, so one instance never evicts another's. */
|
|
106
|
+
fun releaseCatalogCacheDirectory(context: Context): File =
|
|
107
|
+
if (isMain) {
|
|
108
|
+
File(context.noBackupFilesDir, "hot-updater-release-catalog-cache")
|
|
109
|
+
} else {
|
|
110
|
+
File(noBackupRoot(context), RELEASE_CATALOG_CACHE_DIRECTORY)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The prefix of every isolation key of this instance, which its preferences sweep owns. */
|
|
114
|
+
val preferencesPrefix: String
|
|
115
|
+
get() = if (isMain) MAIN_PREFERENCES_PREFIX else "$INSTANCE_PREFERENCES_PREFIX${name}_"
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The key that names this instance's preferences and stamps its bundle
|
|
119
|
+
* metadata; a new key discards the stored bundles.
|
|
120
|
+
*
|
|
121
|
+
* With a fingerprint, the key is the fingerprint, the build's
|
|
122
|
+
* [minBundleId], and the channel: a host release that ships the same
|
|
123
|
+
* artifact keeps its downloads, while a new build, whose embedded bundle
|
|
124
|
+
* is newer, starts from it. Without one, the app version takes the
|
|
125
|
+
* fingerprint's and the build's place, as the appVersion strategy's
|
|
126
|
+
* releases are keyed by it.
|
|
127
|
+
*/
|
|
128
|
+
fun isolationKey(
|
|
129
|
+
context: Context,
|
|
130
|
+
minBundleId: String = HotUpdaterImpl.getMinBundleId(),
|
|
131
|
+
): String {
|
|
132
|
+
if (isMain) {
|
|
133
|
+
// The deprecated HotUpdater.configure(isolationKey:) override is
|
|
134
|
+
// used verbatim. It is main's own key, so a named instance never
|
|
135
|
+
// uses it.
|
|
136
|
+
val override = if (mainValues != null) mainValues.isolationKey else HotUpdaterConfig.isolationKey
|
|
137
|
+
override?.takeIf { it.isNotEmpty() }?.let { return it }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
val fingerprintHash = fingerprintHash(context)
|
|
141
|
+
val appChannel = defaultChannel(context)
|
|
142
|
+
val baseKey =
|
|
143
|
+
if (!fingerprintHash.isNullOrEmpty()) {
|
|
144
|
+
"${fingerprintHash}_$minBundleId"
|
|
145
|
+
} else {
|
|
146
|
+
HotUpdaterImpl.getAppVersion(context) ?: "unknown"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return "$preferencesPrefix${baseKey}_$appChannel"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The channel the instance's build ships with, before any runtime switch. */
|
|
153
|
+
fun defaultChannel(context: Context): String =
|
|
154
|
+
if (isMain) {
|
|
155
|
+
mainValues?.channel ?: HotUpdaterImpl.getChannel(context)
|
|
156
|
+
} else {
|
|
157
|
+
channel
|
|
158
|
+
?: NativeConfigUtils.getManifestString(context, NativeConfigUtils.CHANNEL_META_DATA_KEY)
|
|
159
|
+
?: HotUpdaterImpl.DEFAULT_CHANNEL
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fun fingerprintHash(context: Context): String? =
|
|
163
|
+
if (isMain) {
|
|
164
|
+
if (mainValues != null) mainValues.fingerprintHash else HotUpdaterImpl.getFingerprintHash(context)
|
|
165
|
+
} else {
|
|
166
|
+
// Every instance's bundle comes from the project the build
|
|
167
|
+
// fingerprinted.
|
|
168
|
+
fingerprintHash ?: HotUpdaterConfig.buildFingerprintHash
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The PEM key bundles must be signed with, or null when signing is off. */
|
|
172
|
+
fun publicKey(context: Context): String? =
|
|
173
|
+
if (isMain) {
|
|
174
|
+
if (mainValues != null) mainValues.publicKey else SignatureVerifier.getPublicKeyFromConfig(context)
|
|
175
|
+
} else {
|
|
176
|
+
publicKey ?: NativeConfigUtils.getManifestString(context, NativeConfigUtils.PUBLIC_KEY_META_DATA_KEY)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The JavaScript bundle embedded in the app that the instance launches without an update. */
|
|
180
|
+
val fallbackBundleURL: String
|
|
181
|
+
get() = "assets://$bundleAssetName"
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Whether [other] resolves to the same configuration, so both can share
|
|
185
|
+
* this name's state: an option left out and one that names the value the
|
|
186
|
+
* manifest or the default gives are the same.
|
|
187
|
+
*/
|
|
188
|
+
fun hasSameConfiguration(
|
|
189
|
+
other: HotUpdaterInstanceEnvironment,
|
|
190
|
+
context: Context,
|
|
191
|
+
): Boolean =
|
|
192
|
+
name == other.name &&
|
|
193
|
+
defaultChannel(context) == other.defaultChannel(context) &&
|
|
194
|
+
fingerprintHash(context) == other.fingerprintHash(context) &&
|
|
195
|
+
publicKey(context) == other.publicKey(context) &&
|
|
196
|
+
bundleAssetName == other.bundleAssetName
|
|
197
|
+
|
|
198
|
+
private fun noBackupRoot(context: Context): File = File(NoBackupStorage.file(context, INSTANCES_NO_BACKUP_DIRECTORY), name)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The one [HotUpdaterImpl] of each named instance, so instances with the same
|
|
203
|
+
* name share state, and the launch URLs they handed out, which tell each
|
|
204
|
+
* React Native runtime's TurboModule which instance it serves. Main's impl is
|
|
205
|
+
* `HotUpdater.getInstance`, which every static entry point uses.
|
|
206
|
+
*/
|
|
207
|
+
internal class HotUpdaterInstanceRegistry(
|
|
208
|
+
private val create: (Context, HotUpdaterInstanceEnvironment) -> HotUpdaterImpl,
|
|
209
|
+
) {
|
|
210
|
+
companion object {
|
|
211
|
+
val shared = HotUpdaterInstanceRegistry { context, environment -> HotUpdaterImpl(context, environment) }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private val environments = HashMap<String, HotUpdaterInstanceEnvironment>()
|
|
215
|
+
private val impls = HashMap<String, HotUpdaterImpl>()
|
|
216
|
+
|
|
217
|
+
// The last launch URL each name handed out, which the runtime that loads
|
|
218
|
+
// it reports back as its source URL.
|
|
219
|
+
|
|
220
|
+
/** Every launch URL each name handed out: a runtime may still run an earlier one. */
|
|
221
|
+
private val launchURLs = HashMap<String, MutableSet<String>>()
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Records the configuration of [environment]'s name. A name reads one
|
|
225
|
+
* configuration, since its stored bundles were verified and keyed with it.
|
|
226
|
+
* @throws IllegalArgumentException when the name was registered with another
|
|
227
|
+
*/
|
|
228
|
+
@Synchronized
|
|
229
|
+
fun register(
|
|
230
|
+
context: Context,
|
|
231
|
+
environment: HotUpdaterInstanceEnvironment,
|
|
232
|
+
) {
|
|
233
|
+
require(!environment.isMain) { "main's state belongs to HotUpdater.getInstance" }
|
|
234
|
+
val registered = environments.getOrPut(environment.name) { environment }
|
|
235
|
+
require(registered.hasSameConfiguration(environment, context)) {
|
|
236
|
+
"HotUpdater instance \"${environment.name}\" was already created with another channel, " +
|
|
237
|
+
"fingerprintHash, publicKey, or bundleAssetName"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@Synchronized
|
|
242
|
+
fun impl(
|
|
243
|
+
context: Context,
|
|
244
|
+
environment: HotUpdaterInstanceEnvironment,
|
|
245
|
+
): HotUpdaterImpl {
|
|
246
|
+
register(context, environment)
|
|
247
|
+
return impls.getOrPut(environment.name) { create(context, environment) }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The impl of [name], or null when no named instance of it has one yet. */
|
|
251
|
+
@Synchronized
|
|
252
|
+
fun impl(name: String): HotUpdaterImpl? = impls[name]
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The bundle a React Native host of [environment] starts with, recorded
|
|
256
|
+
* so the TurboModule of the runtime that loads it finds this instance.
|
|
257
|
+
*/
|
|
258
|
+
fun launchURL(
|
|
259
|
+
context: Context,
|
|
260
|
+
environment: HotUpdaterInstanceEnvironment,
|
|
261
|
+
): String {
|
|
262
|
+
// Reading it prepares the launch, which reads storage, so it runs
|
|
263
|
+
// outside the registry's lock.
|
|
264
|
+
val url = impl(context, environment).getJSBundleFile()
|
|
265
|
+
synchronized(this) { launchURLs.getOrPut(environment.name) { mutableSetOf() }.add(url) }
|
|
266
|
+
return url
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The name of the instance whose bundle a React Native runtime runs, from
|
|
271
|
+
* the URL the runtime loaded it from: the launch URL a named instance
|
|
272
|
+
* handed out, then a file inside a named instance's storage (a bundle it
|
|
273
|
+
* staged, loaded after a reload), then main, which covers main's own
|
|
274
|
+
* bundles, the development server, and URLs no instance knows. Only
|
|
275
|
+
* names with an impl are candidates.
|
|
276
|
+
*/
|
|
277
|
+
@Synchronized
|
|
278
|
+
fun resolveName(
|
|
279
|
+
context: Context,
|
|
280
|
+
sourceURL: String?,
|
|
281
|
+
): String {
|
|
282
|
+
// Crash recovery asks for every view it checks; without a named
|
|
283
|
+
// instance, main's answer needs no path work.
|
|
284
|
+
if (sourceURL.isNullOrEmpty() || impls.isEmpty()) return HotUpdaterInstanceEnvironment.MAIN_NAME
|
|
285
|
+
|
|
286
|
+
// Two names that hand out the same URL, such as the same embedded
|
|
287
|
+
// bundle, can't be told apart by it.
|
|
288
|
+
launchURLs
|
|
289
|
+
.filter { (name, urls) -> sourceURL in urls && name in impls }
|
|
290
|
+
.keys
|
|
291
|
+
.singleOrNull()
|
|
292
|
+
?.let { return it }
|
|
293
|
+
|
|
294
|
+
val sourceSegments = pathSegments(canonicalPath(sourceURL.removePrefix("file://")))
|
|
295
|
+
impls.keys
|
|
296
|
+
.firstOrNull { name ->
|
|
297
|
+
val root = environments[name]?.filesRoot(context) ?: return@firstOrNull false
|
|
298
|
+
val rootSegments = pathSegments(canonicalPath(root.path))
|
|
299
|
+
// Whole segments, so "app" never claims "app-2"'s files.
|
|
300
|
+
sourceSegments.size > rootSegments.size && sourceSegments.subList(0, rootSegments.size) == rootSegments
|
|
301
|
+
}?.let { return it }
|
|
302
|
+
|
|
303
|
+
return HotUpdaterInstanceEnvironment.MAIN_NAME
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// React Native may hold `/data/data/...` where `filesDir` is
|
|
307
|
+
// `/data/user/0/...`, a symlink to the same directory.
|
|
308
|
+
private fun canonicalPath(path: String): String =
|
|
309
|
+
if (path.startsWith("/")) runCatching { File(path).canonicalPath }.getOrDefault(path) else path
|
|
310
|
+
|
|
311
|
+
private fun pathSegments(path: String): List<String> {
|
|
312
|
+
if (!path.startsWith("/")) return emptyList()
|
|
313
|
+
val segments = ArrayList<String>()
|
|
314
|
+
path.split('/').forEach { segment ->
|
|
315
|
+
when (segment) {
|
|
316
|
+
"", "." -> Unit
|
|
317
|
+
".." -> if (segments.isNotEmpty()) segments.removeAt(segments.size - 1)
|
|
318
|
+
else -> segments += segment
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return segments
|
|
322
|
+
}
|
|
323
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The main thread, where views, lifecycle callbacks, and crash recovery's
|
|
8
|
+
* delayed work run. Tests stand in for it to run posted work by hand.
|
|
9
|
+
*/
|
|
10
|
+
internal interface HotUpdaterMainThread {
|
|
11
|
+
fun isCurrent(): Boolean
|
|
12
|
+
|
|
13
|
+
fun post(task: Runnable)
|
|
14
|
+
|
|
15
|
+
fun postDelayed(
|
|
16
|
+
task: Runnable,
|
|
17
|
+
delayMs: Long,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
/** Removes [task] while it waits; one already running is not stopped. */
|
|
21
|
+
fun remove(task: Runnable)
|
|
22
|
+
|
|
23
|
+
companion object {
|
|
24
|
+
val android: HotUpdaterMainThread =
|
|
25
|
+
object : HotUpdaterMainThread {
|
|
26
|
+
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
|
27
|
+
|
|
28
|
+
override fun isCurrent(): Boolean = Looper.myLooper() == Looper.getMainLooper()
|
|
29
|
+
|
|
30
|
+
override fun post(task: Runnable) {
|
|
31
|
+
handler.post(task)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override fun postDelayed(
|
|
35
|
+
task: Runnable,
|
|
36
|
+
delayMs: Long,
|
|
37
|
+
) {
|
|
38
|
+
handler.postDelayed(task, delayMs)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun remove(task: Runnable) {
|
|
42
|
+
handler.removeCallbacks(task)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package com.hotupdater
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import java.io.File
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The process [HotUpdaterRestartActivity] runs in (its manifest's
|
|
10
|
+
* `android:process`). Android runs the app's `Application.onCreate` there too,
|
|
11
|
+
* so an app that starts React Native from it, as a brownfield app does, reaches
|
|
12
|
+
* Hot Updater in this process as well.
|
|
13
|
+
*/
|
|
14
|
+
internal object HotUpdaterProcess {
|
|
15
|
+
private const val RESTART_PROCESS_SUFFIX = ":hotupdater_restart"
|
|
16
|
+
|
|
17
|
+
fun isRestartProcess(context: Context): Boolean = isRestartProcessName(context.packageName, currentProcessName())
|
|
18
|
+
|
|
19
|
+
fun isRestartProcessName(
|
|
20
|
+
packageName: String,
|
|
21
|
+
processName: String?,
|
|
22
|
+
): Boolean = processName == packageName + RESTART_PROCESS_SUFFIX
|
|
23
|
+
|
|
24
|
+
private fun currentProcessName(): String? =
|
|
25
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
26
|
+
Application.getProcessName()
|
|
27
|
+
} else {
|
|
28
|
+
runCatching { File("/proc/self/cmdline").readText().trimEnd('\u0000') }.getOrNull()
|
|
29
|
+
}
|
|
30
|
+
}
|