@otakit/capacitor-updater 2.1.0 → 2.1.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/Package.swift +27 -0
- package/README.md +164 -128
- package/android/src/main/java/com/otakit/updater/BundleStore.java +37 -8
- package/android/src/main/java/com/otakit/updater/DeviceEventClient.java +2 -3
- package/android/src/main/java/com/otakit/updater/ManifestClient.java +20 -7
- package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +726 -0
- package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +551 -493
- package/dist/esm/definitions.d.ts +41 -66
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +5 -47
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +4 -3
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +5 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +10 -49
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +10 -49
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +28 -6
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +7 -3
- package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +635 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +1 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +498 -489
- package/package.json +5 -3
package/Package.swift
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "OtakitCapacitorUpdater",
|
|
6
|
+
platforms: [.iOS(.v15)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(
|
|
9
|
+
name: "OtakitCapacitorUpdater",
|
|
10
|
+
targets: ["UpdaterPlugin"])
|
|
11
|
+
],
|
|
12
|
+
dependencies: [
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", "7.0.0"..<"9.0.0"),
|
|
14
|
+
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0"))
|
|
15
|
+
],
|
|
16
|
+
targets: [
|
|
17
|
+
.target(
|
|
18
|
+
name: "UpdaterPlugin",
|
|
19
|
+
dependencies: [
|
|
20
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
21
|
+
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
|
22
|
+
.product(name: "ZIPFoundation", package: "ZIPFoundation")
|
|
23
|
+
],
|
|
24
|
+
path: "ios/Sources/UpdaterPlugin",
|
|
25
|
+
exclude: ["UpdaterPlugin.m"])
|
|
26
|
+
]
|
|
27
|
+
)
|
package/README.md
CHANGED
|
@@ -7,38 +7,17 @@ Capacitor OTA updater plugin for OtaKit.
|
|
|
7
7
|
- fetches the latest manifest for its release lane from the CDN
|
|
8
8
|
- downloads and verifies OTA bundles
|
|
9
9
|
- stages updates safely
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- supports optional `runtimeVersion` lanes for native compatibility boundaries
|
|
14
|
-
- requires `notifyAppReady()` as the success handshake
|
|
15
|
-
- rolls back automatically if the new bundle does not prove healthy
|
|
10
|
+
- applies staged bundles on cold start, resume, or manual command
|
|
11
|
+
- uses `notifyAppReady()` as the health handshake
|
|
12
|
+
- rolls back automatically if a newly applied bundle does not prove healthy
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
plugin fetches the manifest for its `appId + channel + runtimeVersion` lane,
|
|
19
|
-
verifies it, compares it against the current and staged bundle locally, and
|
|
20
|
-
only downloads when the manifest actually points at something newer.
|
|
14
|
+
The plugin is intentionally small:
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
- three automatic lifecycle entry points: runtime, launch, resume
|
|
17
|
+
- one shared set of update primitives: check, download, apply
|
|
18
|
+
- one rollback safety loop
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
const state = await OtaKit.getState();
|
|
26
|
-
const latest = await OtaKit.check();
|
|
27
|
-
|
|
28
|
-
// Low-level manual flow:
|
|
29
|
-
const bundle = await OtaKit.download();
|
|
30
|
-
if (bundle) {
|
|
31
|
-
await OtaKit.apply();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Or the one-shot manual helper:
|
|
35
|
-
await OtaKit.update();
|
|
36
|
-
await OtaKit.notifyAppReady();
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
For manual mode, `getState()` tells you if something is already staged,
|
|
40
|
-
`check()` tells you whether a newer update exists, `download()` stages it, and
|
|
41
|
-
`update()` is the one-shot helper that downloads and applies the newest update.
|
|
20
|
+
There is no built-in splash or overlay manager in this model.
|
|
42
21
|
|
|
43
22
|
## Hosted config
|
|
44
23
|
|
|
@@ -47,187 +26,244 @@ plugins: {
|
|
|
47
26
|
OtaKit: {
|
|
48
27
|
appId: "YOUR_OTAKIT_APP_ID",
|
|
49
28
|
appReadyTimeout: 10000,
|
|
29
|
+
|
|
50
30
|
// Optional:
|
|
51
31
|
// channel: "staging",
|
|
52
32
|
// runtimeVersion: "2026.04",
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
33
|
+
// launchPolicy: "apply-staged",
|
|
34
|
+
// resumePolicy: "shadow",
|
|
35
|
+
// runtimePolicy: "immediate",
|
|
36
|
+
// checkInterval: 600000,
|
|
56
37
|
}
|
|
57
38
|
}
|
|
58
39
|
```
|
|
59
40
|
|
|
60
41
|
Advanced overrides for self-hosting or custom trust only:
|
|
61
42
|
|
|
62
|
-
- `cdnUrl`
|
|
63
|
-
- `ingestUrl`
|
|
64
|
-
- `serverUrl`
|
|
43
|
+
- `cdnUrl`
|
|
44
|
+
- `ingestUrl`
|
|
45
|
+
- `serverUrl`
|
|
65
46
|
- `manifestKeys`
|
|
66
47
|
- `allowInsecureUrls`
|
|
67
48
|
|
|
68
49
|
Hosted OtaKit already points at the managed ingest service and CDN and already
|
|
69
50
|
trusts the managed manifest signing keys.
|
|
70
51
|
|
|
71
|
-
##
|
|
52
|
+
## Policies
|
|
72
53
|
|
|
73
|
-
|
|
74
|
-
- `runtimeVersion` answers "which native app shell can safely run this bundle?"
|
|
54
|
+
The plugin has three automatic events:
|
|
75
55
|
|
|
76
|
-
|
|
56
|
+
- `runtime`
|
|
57
|
+
Cold start where the current `runtimeVersion` lane has not been resolved yet.
|
|
58
|
+
- `launch`
|
|
59
|
+
Normal cold start after runtime is already resolved.
|
|
60
|
+
- `resume`
|
|
61
|
+
App returning from background.
|
|
77
62
|
|
|
78
|
-
|
|
79
|
-
want devices on that new native shell to keep receiving older OTA bundles.
|
|
63
|
+
Each event uses the same policy names:
|
|
80
64
|
|
|
81
|
-
|
|
65
|
+
```ts
|
|
66
|
+
type OtaKitPolicy = 'off' | 'shadow' | 'apply-staged' | 'immediate';
|
|
67
|
+
```
|
|
82
68
|
|
|
83
|
-
|
|
84
|
-
- bundle uploads inherit the same runtime value automatically through the CLI
|
|
85
|
-
- releases stay simple: publish the bundle, and it naturally stays inside its own runtime lane
|
|
69
|
+
Semantics:
|
|
86
70
|
|
|
87
|
-
|
|
71
|
+
- `shadow`
|
|
72
|
+
check + download, never apply on that event
|
|
73
|
+
- `apply-staged`
|
|
74
|
+
apply a staged bundle if one already exists, otherwise behave like `shadow`
|
|
75
|
+
- `immediate`
|
|
76
|
+
check + download + apply
|
|
88
77
|
|
|
89
|
-
|
|
78
|
+
Recommended defaults:
|
|
90
79
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
6. it stages and activates the bundle
|
|
80
|
+
```ts
|
|
81
|
+
launchPolicy = 'apply-staged';
|
|
82
|
+
resumePolicy = 'shadow';
|
|
83
|
+
runtimePolicy = 'immediate';
|
|
84
|
+
```
|
|
97
85
|
|
|
98
|
-
|
|
86
|
+
That means:
|
|
99
87
|
|
|
100
|
-
|
|
88
|
+
- fresh install or new `runtimeVersion`: catch up immediately
|
|
89
|
+
- later cold starts: apply already staged content if present, otherwise download
|
|
90
|
+
the next update in the background
|
|
91
|
+
- resumes: periodically check and stage in the background
|
|
101
92
|
|
|
102
|
-
|
|
103
|
-
resume**.
|
|
93
|
+
If an app wants full JS control:
|
|
104
94
|
|
|
105
|
-
|
|
95
|
+
```ts
|
|
96
|
+
launchPolicy = 'off';
|
|
97
|
+
resumePolicy = 'off';
|
|
98
|
+
runtimePolicy = 'off';
|
|
99
|
+
```
|
|
106
100
|
|
|
107
|
-
|
|
108
|
-
check and download in the background on cold start and resume.
|
|
109
|
-
activate the staged bundle only on the next cold start.
|
|
110
|
-
zero disruption during a session — the user never sees a surprise reload.
|
|
101
|
+
## Check interval
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
activate the staged bundle on the next resume or cold start.
|
|
103
|
+
`checkInterval` defaults to 10 minutes and only applies to background resume
|
|
104
|
+
checks:
|
|
115
105
|
|
|
116
|
-
|
|
106
|
+
- `resumePolicy: "shadow"`
|
|
107
|
+
- `resumePolicy: "apply-staged"` when there is no staged bundle to apply
|
|
117
108
|
|
|
118
|
-
|
|
119
|
-
no automatic checks, no automatic staged activation.
|
|
120
|
-
the app integration drives everything via `check()`, `download()`, `apply()`, or `update()`.
|
|
109
|
+
Set `checkInterval` to `0` or a negative value to disable resume throttling.
|
|
121
110
|
|
|
122
|
-
|
|
111
|
+
It does not throttle:
|
|
123
112
|
|
|
113
|
+
- launch handling
|
|
114
|
+
- runtime handling
|
|
124
115
|
- `immediate`
|
|
125
|
-
|
|
126
|
-
primarily for development and testing — not recommended for production.
|
|
127
|
-
|
|
116
|
+
- manual JS APIs
|
|
128
117
|
|
|
129
118
|
## Runtime model
|
|
130
119
|
|
|
131
|
-
The plugin keeps three important
|
|
120
|
+
The plugin keeps three important pointers:
|
|
132
121
|
|
|
133
122
|
- `current`
|
|
134
|
-
the bundle the WebView is
|
|
123
|
+
the bundle the WebView is serving now
|
|
135
124
|
- `fallback`
|
|
136
125
|
the last known-good bundle used for rollback
|
|
137
126
|
- `staged`
|
|
138
127
|
a downloaded bundle waiting to be activated
|
|
139
128
|
|
|
140
|
-
Bundle lifecycle
|
|
129
|
+
Bundle lifecycle:
|
|
141
130
|
|
|
142
131
|
```text
|
|
143
132
|
download -> pending -> trial -> success
|
|
144
133
|
|
|
|
145
|
-
+-> error -> rollback
|
|
134
|
+
+-> error -> rollback
|
|
146
135
|
```
|
|
147
136
|
|
|
148
|
-
|
|
137
|
+
If a bundle is applied and never calls `notifyAppReady()`:
|
|
149
138
|
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
|
|
153
|
-
- timeout or restart during `trial` causes rollback
|
|
139
|
+
- timeout triggers rollback while the app is running
|
|
140
|
+
- or the next cold start detects the still-trial bundle and rolls back before
|
|
141
|
+
boot continues
|
|
154
142
|
|
|
155
|
-
|
|
143
|
+
The last failed applied bundle is persisted so the plugin does not immediately
|
|
144
|
+
download and apply the same broken release again.
|
|
156
145
|
|
|
157
|
-
|
|
158
|
-
- `fallback` is the last known-good bundle
|
|
159
|
-
- `staged` is what can be activated next
|
|
160
|
-
- rollback always goes back to `fallback`, not to an arbitrary older bundle
|
|
146
|
+
## Automatic flow
|
|
161
147
|
|
|
162
|
-
|
|
148
|
+
For the normal hosted path, most apps only need:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await OtaKit.notifyAppReady();
|
|
152
|
+
```
|
|
163
153
|
|
|
164
|
-
|
|
154
|
+
The plugin handles checking, staging, applying, rollback, and runtime-lane
|
|
155
|
+
catch-up based on the configured policies.
|
|
165
156
|
|
|
166
|
-
|
|
157
|
+
## Loading screen recommendation
|
|
158
|
+
|
|
159
|
+
OtaKit does not manage a splash screen or loading overlay for you.
|
|
160
|
+
|
|
161
|
+
Recommended startup order:
|
|
162
|
+
|
|
163
|
+
1. keep a native splash screen or fullscreen loading view visible
|
|
164
|
+
2. finish your normal app bootstrap
|
|
165
|
+
3. call `notifyAppReady()`
|
|
166
|
+
4. hide the splash screen or loading view
|
|
167
|
+
|
|
168
|
+
For React and Next.js apps, treat this as part of the default setup, not an
|
|
169
|
+
optional polish step.
|
|
170
|
+
|
|
171
|
+
## Manual APIs
|
|
172
|
+
|
|
173
|
+
The manual surface maps to the same internal engine:
|
|
167
174
|
|
|
168
175
|
```ts
|
|
176
|
+
const state = await OtaKit.getState();
|
|
177
|
+
const check = await OtaKit.check();
|
|
178
|
+
const download = await OtaKit.download();
|
|
169
179
|
await OtaKit.notifyAppReady();
|
|
170
180
|
```
|
|
171
181
|
|
|
172
|
-
|
|
173
|
-
`updateMode`. In `next-launch` and `next-resume`, it checks on cold start and
|
|
174
|
-
every time the app comes back from the background, throttled by `checkInterval`.
|
|
175
|
-
`immediate` bypasses that throttle.
|
|
182
|
+
`check()` returns:
|
|
176
183
|
|
|
177
|
-
|
|
184
|
+
```ts
|
|
185
|
+
type CheckResult =
|
|
186
|
+
| { kind: 'no_update' }
|
|
187
|
+
| { kind: 'already_staged'; latest: LatestVersion }
|
|
188
|
+
| { kind: 'update_available'; latest: LatestVersion };
|
|
189
|
+
```
|
|
178
190
|
|
|
179
|
-
|
|
191
|
+
`download()` returns:
|
|
180
192
|
|
|
181
|
-
|
|
193
|
+
```ts
|
|
194
|
+
type DownloadResult = { kind: 'no_update' } | { kind: 'staged'; bundle: BundleInfo };
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`update()` uses the same native immediate-flow operation as automatic
|
|
198
|
+
`"immediate"` policies:
|
|
182
199
|
|
|
183
200
|
```ts
|
|
184
|
-
|
|
185
|
-
|
|
201
|
+
await OtaKit.update();
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
That keeps the manual convenience path atomic inside the native plugin instead
|
|
205
|
+
of splitting it into separate `download()` and `apply()` calls.
|
|
206
|
+
|
|
207
|
+
`apply()` and successful `update()` are terminal operations:
|
|
186
208
|
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
- on success they reload the WebView
|
|
210
|
+
- they do not resolve back into the old JS context
|
|
211
|
+
- call `notifyAppReady()` from normal startup after the reloaded app boots
|
|
212
|
+
|
|
213
|
+
There is no listener/event API in this refactor. If an app later needs a
|
|
214
|
+
smaller reactive surface, that can be added intentionally.
|
|
215
|
+
|
|
216
|
+
## Example manual flow
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const check = await OtaKit.check();
|
|
220
|
+
|
|
221
|
+
if (check.kind === 'update_available') {
|
|
222
|
+
const result = await OtaKit.download();
|
|
223
|
+
if (result.kind === 'staged') {
|
|
224
|
+
await OtaKit.apply();
|
|
225
|
+
}
|
|
189
226
|
}
|
|
190
227
|
```
|
|
191
228
|
|
|
192
|
-
Or the
|
|
229
|
+
Or use the one-shot helper:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
await OtaKit.update();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
After the app reloads and starts again, call:
|
|
193
236
|
|
|
194
237
|
```ts
|
|
195
|
-
await OtaKit.
|
|
196
|
-
await OtaKit.apply();
|
|
238
|
+
await OtaKit.notifyAppReady();
|
|
197
239
|
```
|
|
198
240
|
|
|
199
|
-
|
|
200
|
-
only after explicit user confirmation.
|
|
241
|
+
## Compatibility lanes
|
|
201
242
|
|
|
202
|
-
|
|
243
|
+
- `channel` answers "who should get this rollout?"
|
|
244
|
+
- `runtimeVersion` answers "which native app shell can safely run this bundle?"
|
|
245
|
+
|
|
246
|
+
Use channels for rollout tracks such as `beta`, `staging`, or `production`.
|
|
203
247
|
|
|
204
|
-
`
|
|
205
|
-
|
|
206
|
-
|
|
248
|
+
Use `runtimeVersion` when a new store build creates a new compatibility
|
|
249
|
+
boundary and you do not want devices on that native shell to keep receiving
|
|
250
|
+
older OTA bundles.
|
|
207
251
|
|
|
208
|
-
##
|
|
252
|
+
## Trust model
|
|
209
253
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
254
|
+
The plugin does not just download arbitrary zips from a URL.
|
|
255
|
+
|
|
256
|
+
1. it fetches the latest manifest for the current app + channel + runtime lane
|
|
257
|
+
2. it verifies the manifest signature when keys are configured
|
|
258
|
+
3. it compares the manifest with current, staged, and last-failed local state
|
|
259
|
+
4. it downloads only when a newer usable bundle exists
|
|
260
|
+
5. it verifies the zip against the manifest `sha256`
|
|
261
|
+
6. it stages and later applies the bundle
|
|
213
262
|
|
|
214
263
|
## Source areas
|
|
215
264
|
|
|
216
|
-
- `src/definitions.ts`: public types and
|
|
265
|
+
- `src/definitions.ts`: public types and config
|
|
217
266
|
- `src/index.ts`: Capacitor registration and JS wrapper
|
|
218
267
|
- `src/web.ts`: web fallback implementation
|
|
219
268
|
- `ios/Sources/UpdaterPlugin/*`: iOS implementation
|
|
220
269
|
- `android/src/main/java/com/otakit/updater/*`: Android implementation
|
|
221
|
-
|
|
222
|
-
## Build locally
|
|
223
|
-
|
|
224
|
-
```bash
|
|
225
|
-
pnpm --filter @otakit/capacitor-updater build
|
|
226
|
-
pnpm --filter @otakit/capacitor-updater typecheck
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Repo-level verification:
|
|
230
|
-
|
|
231
|
-
```bash
|
|
232
|
-
npm run build
|
|
233
|
-
```
|
|
@@ -20,7 +20,8 @@ final class BundleStore {
|
|
|
20
20
|
private static final String KEY_CURRENT = "current_bundle_id";
|
|
21
21
|
private static final String KEY_FALLBACK = "fallback_bundle_id";
|
|
22
22
|
private static final String KEY_STAGED = "staged_bundle_id";
|
|
23
|
-
private static final String
|
|
23
|
+
private static final String KEY_LAST_FAILED_BUNDLE_INFO = "last_failed_bundle_info";
|
|
24
|
+
private static final String KEY_LAST_RESOLVED_RUNTIME_KEY = "last_resolved_runtime_key";
|
|
24
25
|
|
|
25
26
|
private final Context context;
|
|
26
27
|
private final SharedPreferences prefs;
|
|
@@ -29,7 +30,12 @@ final class BundleStore {
|
|
|
29
30
|
private final String nativeBuild;
|
|
30
31
|
private final String appRuntimeVersion;
|
|
31
32
|
|
|
32
|
-
BundleStore(
|
|
33
|
+
BundleStore(
|
|
34
|
+
Context context,
|
|
35
|
+
String builtinVersion,
|
|
36
|
+
String nativeBuild,
|
|
37
|
+
String appRuntimeVersion
|
|
38
|
+
) {
|
|
33
39
|
this.context = context.getApplicationContext();
|
|
34
40
|
this.prefs = this.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
35
41
|
this.builtinVersion = builtinVersion;
|
|
@@ -134,6 +140,10 @@ final class BundleStore {
|
|
|
134
140
|
return current != null ? current : builtinBundle();
|
|
135
141
|
}
|
|
136
142
|
|
|
143
|
+
synchronized String getCurrentBundleId() {
|
|
144
|
+
return prefs.getString(KEY_CURRENT, null);
|
|
145
|
+
}
|
|
146
|
+
|
|
137
147
|
synchronized void setCurrentBundleId(String id) {
|
|
138
148
|
SharedPreferences.Editor editor = prefs.edit();
|
|
139
149
|
if (id == null) {
|
|
@@ -153,6 +163,10 @@ final class BundleStore {
|
|
|
153
163
|
return fallback != null ? fallback : builtinBundle();
|
|
154
164
|
}
|
|
155
165
|
|
|
166
|
+
synchronized String getFallbackBundleId() {
|
|
167
|
+
return prefs.getString(KEY_FALLBACK, null);
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
synchronized void setFallbackBundleId(String id) {
|
|
157
171
|
SharedPreferences.Editor editor = prefs.edit();
|
|
158
172
|
if (id == null) {
|
|
@@ -177,22 +191,22 @@ final class BundleStore {
|
|
|
177
191
|
editor.commit();
|
|
178
192
|
}
|
|
179
193
|
|
|
180
|
-
synchronized void
|
|
194
|
+
synchronized void setLastFailedBundle(BundleInfo bundle) {
|
|
181
195
|
SharedPreferences.Editor editor = prefs.edit();
|
|
182
196
|
if (bundle == null) {
|
|
183
|
-
editor.remove(
|
|
197
|
+
editor.remove(KEY_LAST_FAILED_BUNDLE_INFO);
|
|
184
198
|
} else {
|
|
185
199
|
try {
|
|
186
|
-
editor.putString(
|
|
200
|
+
editor.putString(KEY_LAST_FAILED_BUNDLE_INFO, bundle.toJSONObject().toString());
|
|
187
201
|
} catch (JSONException ignored) {
|
|
188
|
-
editor.remove(
|
|
202
|
+
editor.remove(KEY_LAST_FAILED_BUNDLE_INFO);
|
|
189
203
|
}
|
|
190
204
|
}
|
|
191
205
|
editor.apply();
|
|
192
206
|
}
|
|
193
207
|
|
|
194
|
-
synchronized BundleInfo
|
|
195
|
-
String json = prefs.getString(
|
|
208
|
+
synchronized BundleInfo getLastFailedBundle() {
|
|
209
|
+
String json = prefs.getString(KEY_LAST_FAILED_BUNDLE_INFO, null);
|
|
196
210
|
if (json == null) {
|
|
197
211
|
return null;
|
|
198
212
|
}
|
|
@@ -203,6 +217,21 @@ final class BundleStore {
|
|
|
203
217
|
}
|
|
204
218
|
}
|
|
205
219
|
|
|
220
|
+
synchronized String getLastResolvedRuntimeKey() {
|
|
221
|
+
return prefs.getString(KEY_LAST_RESOLVED_RUNTIME_KEY, null);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
synchronized void setLastResolvedRuntimeKey(String runtimeKey) {
|
|
225
|
+
SharedPreferences.Editor editor = prefs.edit();
|
|
226
|
+
if (runtimeKey == null) {
|
|
227
|
+
// null clears the key so the next cold start is treated as unresolved again
|
|
228
|
+
editor.remove(KEY_LAST_RESOLVED_RUNTIME_KEY);
|
|
229
|
+
} else {
|
|
230
|
+
editor.putString(KEY_LAST_RESOLVED_RUNTIME_KEY, runtimeKey);
|
|
231
|
+
}
|
|
232
|
+
editor.commit();
|
|
233
|
+
}
|
|
234
|
+
|
|
206
235
|
synchronized void markStatus(String bundleId, BundleStatus status) {
|
|
207
236
|
BundleInfo existing = getBundle(bundleId);
|
|
208
237
|
if (existing == null) {
|
|
@@ -8,9 +8,9 @@ import java.text.SimpleDateFormat;
|
|
|
8
8
|
import java.util.Date;
|
|
9
9
|
import java.util.Locale;
|
|
10
10
|
import java.util.TimeZone;
|
|
11
|
+
import java.util.UUID;
|
|
11
12
|
import java.util.concurrent.ExecutorService;
|
|
12
13
|
import java.util.concurrent.Executors;
|
|
13
|
-
import java.util.UUID;
|
|
14
14
|
import org.json.JSONObject;
|
|
15
15
|
|
|
16
16
|
final class DeviceEventClient {
|
|
@@ -52,8 +52,7 @@ final class DeviceEventClient {
|
|
|
52
52
|
payload.put("releaseId", releaseId);
|
|
53
53
|
payload.put("nativeBuild", nativeBuild);
|
|
54
54
|
if (detail != null) {
|
|
55
|
-
String truncated =
|
|
56
|
-
detail.length() > 500 ? detail.substring(0, 500) : detail;
|
|
55
|
+
String truncated = detail.length() > 500 ? detail.substring(0, 500) : detail;
|
|
57
56
|
payload.put("detail", truncated);
|
|
58
57
|
}
|
|
59
58
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package com.otakit.updater;
|
|
2
2
|
|
|
3
|
+
import android.net.Uri;
|
|
3
4
|
import java.io.ByteArrayOutputStream;
|
|
4
5
|
import java.io.InputStream;
|
|
5
6
|
import java.net.HttpURLConnection;
|
|
@@ -74,14 +75,25 @@ final class ManifestClient {
|
|
|
74
75
|
java.util.List<ManifestVerifier.KeyEntry> manifestKeys
|
|
75
76
|
) throws Exception {
|
|
76
77
|
String base = cdnUrl.replaceAll("/+$", "");
|
|
77
|
-
String channelKey =
|
|
78
|
+
String channelKey =
|
|
79
|
+
channel != null && !channel.trim().isEmpty() ? channel.trim() : BASE_CHANNEL_KEY;
|
|
78
80
|
String runtimeKey =
|
|
79
81
|
runtimeVersion != null && !runtimeVersion.trim().isEmpty()
|
|
80
82
|
? runtimeVersion.trim()
|
|
81
83
|
: DEFAULT_RUNTIME_KEY;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
Uri baseUri = Uri.parse(base);
|
|
85
|
+
if (baseUri.getScheme() == null || baseUri.getHost() == null) {
|
|
86
|
+
throw new IllegalStateException("Invalid CDN URL");
|
|
87
|
+
}
|
|
88
|
+
Uri urlUri = baseUri
|
|
89
|
+
.buildUpon()
|
|
90
|
+
.appendPath("manifests")
|
|
91
|
+
.appendPath(appId)
|
|
92
|
+
.appendPath(channelKey)
|
|
93
|
+
.appendPath(runtimeKey)
|
|
94
|
+
.appendPath("manifest.json")
|
|
95
|
+
.build();
|
|
96
|
+
URL url = new URL(urlUri.toString());
|
|
85
97
|
|
|
86
98
|
requireHTTPS(url, allowInsecureUrls);
|
|
87
99
|
|
|
@@ -112,9 +124,10 @@ final class ManifestClient {
|
|
|
112
124
|
String sha256 = json.getString("sha256");
|
|
113
125
|
int size = json.getInt("size");
|
|
114
126
|
|
|
115
|
-
String responseRuntimeVersion =
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
String responseRuntimeVersion =
|
|
128
|
+
json.has("runtimeVersion") && !json.isNull("runtimeVersion")
|
|
129
|
+
? json.getString("runtimeVersion").trim()
|
|
130
|
+
: null;
|
|
118
131
|
if (responseRuntimeVersion != null && responseRuntimeVersion.isEmpty()) {
|
|
119
132
|
responseRuntimeVersion = null;
|
|
120
133
|
}
|