@otakit/capacitor-updater 2.0.1 → 2.1.1
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/{UpdatekitUpdater.podspec → OtaKitUpdater.podspec} +1 -1
- package/README.md +165 -130
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/BundleInfo.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/BundleStatus.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/BundleStore.java +38 -9
- package/android/src/main/java/com/{updatekit → otakit}/updater/DateUtils.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/DeviceEventClient.java +3 -4
- package/android/src/main/java/com/{updatekit → otakit}/updater/HashUtils.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/HostedManifestKeys.java +1 -1
- package/android/src/main/java/com/{updatekit → otakit}/updater/ManifestClient.java +32 -15
- package/android/src/main/java/com/{updatekit → otakit}/updater/ManifestVerifier.java +1 -1
- package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +726 -0
- package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +1191 -0
- package/android/src/main/java/com/{updatekit → otakit}/updater/ZipUtils.java +1 -1
- package/dist/esm/definitions.d.ts +48 -105
- 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 +6 -60
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +7 -11
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +8 -14
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +14 -74
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +14 -74
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +28 -6
- package/ios/Sources/UpdaterPlugin/Downloader.swift +1 -1
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +8 -4
- package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +635 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +3 -5
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +506 -551
- package/package.json +2 -2
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +0 -1191
|
@@ -3,7 +3,7 @@ require 'json'
|
|
|
3
3
|
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
4
|
|
|
5
5
|
Pod::Spec.new do |s|
|
|
6
|
-
s.name = '
|
|
6
|
+
s.name = 'OtaKitUpdater'
|
|
7
7
|
s.version = package['version']
|
|
8
8
|
s.summary = package['description']
|
|
9
9
|
s.license = package['license']
|
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,188 +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
|
-
|
|
163
|
-
|
|
164
|
-
### Automatic flow
|
|
165
|
-
|
|
166
|
-
For the normal hosted path, the app usually just needs to call:
|
|
148
|
+
For the normal hosted path, most apps only need:
|
|
167
149
|
|
|
168
150
|
```ts
|
|
169
151
|
await OtaKit.notifyAppReady();
|
|
170
152
|
```
|
|
171
153
|
|
|
172
|
-
The plugin handles checking,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
154
|
+
The plugin handles checking, staging, applying, rollback, and runtime-lane
|
|
155
|
+
catch-up based on the configured policies.
|
|
156
|
+
|
|
157
|
+
## Loading screen recommendation
|
|
158
|
+
|
|
159
|
+
OtaKit does not manage a splash screen or loading overlay for you.
|
|
176
160
|
|
|
177
|
-
|
|
161
|
+
Recommended startup order:
|
|
178
162
|
|
|
179
|
-
|
|
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
|
|
180
167
|
|
|
181
|
-
For
|
|
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:
|
|
182
174
|
|
|
183
175
|
```ts
|
|
184
176
|
const state = await OtaKit.getState();
|
|
185
|
-
const
|
|
177
|
+
const check = await OtaKit.check();
|
|
178
|
+
const download = await OtaKit.download();
|
|
179
|
+
await OtaKit.notifyAppReady();
|
|
180
|
+
```
|
|
186
181
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
`check()` returns:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
type CheckResult =
|
|
186
|
+
| { kind: 'no_update' }
|
|
187
|
+
| { kind: 'already_staged'; latest: LatestVersion }
|
|
188
|
+
| { kind: 'update_available'; latest: LatestVersion };
|
|
190
189
|
```
|
|
191
190
|
|
|
192
|
-
|
|
191
|
+
`download()` returns:
|
|
193
192
|
|
|
194
193
|
```ts
|
|
195
|
-
|
|
196
|
-
await OtaKit.apply();
|
|
194
|
+
type DownloadResult = { kind: 'no_update' } | { kind: 'staged'; bundle: BundleInfo };
|
|
197
195
|
```
|
|
198
196
|
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
`update()` uses the same native immediate-flow operation as automatic
|
|
198
|
+
`"immediate"` policies:
|
|
201
199
|
|
|
202
|
-
|
|
200
|
+
```ts
|
|
201
|
+
await OtaKit.update();
|
|
202
|
+
```
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
always live, and `immediate` mode ignores the interval entirely.
|
|
204
|
+
That keeps the manual convenience path atomic inside the native plugin instead
|
|
205
|
+
of splitting it into separate `download()` and `apply()` calls.
|
|
207
206
|
|
|
208
|
-
|
|
207
|
+
`apply()` and successful `update()` are terminal operations:
|
|
209
208
|
|
|
210
|
-
-
|
|
211
|
-
-
|
|
212
|
-
-
|
|
213
|
-
- `debug.deleteBundle()` only works for downloaded bundles outside runtime state
|
|
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
|
|
214
212
|
|
|
215
|
-
|
|
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.
|
|
216
215
|
|
|
217
|
-
|
|
218
|
-
- `src/index.ts`: Capacitor registration and JS wrapper
|
|
219
|
-
- `src/web.ts`: web fallback implementation
|
|
220
|
-
- `ios/Sources/UpdaterPlugin/*`: iOS implementation
|
|
221
|
-
- `android/src/main/java/com/updatekit/updater/*`: Android implementation
|
|
216
|
+
## Example manual flow
|
|
222
217
|
|
|
223
|
-
|
|
218
|
+
```ts
|
|
219
|
+
const check = await OtaKit.check();
|
|
224
220
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
221
|
+
if (check.kind === 'update_available') {
|
|
222
|
+
const result = await OtaKit.download();
|
|
223
|
+
if (result.kind === 'staged') {
|
|
224
|
+
await OtaKit.apply();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
228
227
|
```
|
|
229
228
|
|
|
230
|
-
|
|
229
|
+
Or use the one-shot helper:
|
|
231
230
|
|
|
232
|
-
```
|
|
233
|
-
|
|
231
|
+
```ts
|
|
232
|
+
await OtaKit.update();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
After the app reloads and starts again, call:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
await OtaKit.notifyAppReady();
|
|
234
239
|
```
|
|
240
|
+
|
|
241
|
+
## Compatibility lanes
|
|
242
|
+
|
|
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`.
|
|
247
|
+
|
|
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.
|
|
251
|
+
|
|
252
|
+
## Trust model
|
|
253
|
+
|
|
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
|
|
262
|
+
|
|
263
|
+
## Source areas
|
|
264
|
+
|
|
265
|
+
- `src/definitions.ts`: public types and config
|
|
266
|
+
- `src/index.ts`: Capacitor registration and JS wrapper
|
|
267
|
+
- `src/web.ts`: web fallback implementation
|
|
268
|
+
- `ios/Sources/UpdaterPlugin/*`: iOS implementation
|
|
269
|
+
- `android/src/main/java/com/otakit/updater/*`: Android implementation
|
package/android/build.gradle
CHANGED
|
@@ -17,7 +17,7 @@ buildscript {
|
|
|
17
17
|
apply plugin: 'com.android.library'
|
|
18
18
|
|
|
19
19
|
android {
|
|
20
|
-
namespace "com.
|
|
20
|
+
namespace "com.otakit.updater"
|
|
21
21
|
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
|
|
22
22
|
defaultConfig {
|
|
23
23
|
minSdk project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package com.
|
|
1
|
+
package com.otakit.updater;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
4
|
import android.content.SharedPreferences;
|
|
@@ -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) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package com.
|
|
1
|
+
package com.otakit.updater;
|
|
2
2
|
|
|
3
3
|
import java.io.OutputStream;
|
|
4
4
|
import java.net.HttpURLConnection;
|
|
@@ -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
|
-
package com.
|
|
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
|
}
|
|
@@ -136,7 +149,7 @@ final class ManifestClient {
|
|
|
136
149
|
|
|
137
150
|
if (manifestKeys == null || manifestKeys.isEmpty()) {
|
|
138
151
|
android.util.Log.w(
|
|
139
|
-
"
|
|
152
|
+
"OtaKit",
|
|
140
153
|
"No manifest signing keys configured — signature verification is disabled for this request."
|
|
141
154
|
);
|
|
142
155
|
}
|
|
@@ -194,11 +207,15 @@ final class ManifestClient {
|
|
|
194
207
|
if (!sigObj.has("kid") || !sigObj.has("sig") || !sigObj.has("iat") || !sigObj.has("exp")) {
|
|
195
208
|
return null;
|
|
196
209
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
210
|
+
try {
|
|
211
|
+
return new ManifestSignature(
|
|
212
|
+
sigObj.getString("kid"),
|
|
213
|
+
sigObj.getString("sig"),
|
|
214
|
+
sigObj.getInt("iat"),
|
|
215
|
+
sigObj.getInt("exp")
|
|
216
|
+
);
|
|
217
|
+
} catch (org.json.JSONException e) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
203
220
|
}
|
|
204
221
|
}
|