@otakit/capacitor-updater 1.0.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/LICENSE +21 -0
- package/README.md +183 -0
- package/UpdatekitUpdater.podspec +18 -0
- package/android/build.gradle +49 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/updatekit/updater/BundleInfo.java +100 -0
- package/android/src/main/java/com/updatekit/updater/BundleStatus.java +28 -0
- package/android/src/main/java/com/updatekit/updater/BundleStore.java +264 -0
- package/android/src/main/java/com/updatekit/updater/DateUtils.java +17 -0
- package/android/src/main/java/com/updatekit/updater/HashUtils.java +32 -0
- package/android/src/main/java/com/updatekit/updater/HostedManifestKeys.java +37 -0
- package/android/src/main/java/com/updatekit/updater/ManifestClient.java +209 -0
- package/android/src/main/java/com/updatekit/updater/ManifestVerifier.java +146 -0
- package/android/src/main/java/com/updatekit/updater/StatsClient.java +80 -0
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +963 -0
- package/android/src/main/java/com/updatekit/updater/ZipUtils.java +72 -0
- package/dist/esm/definitions.d.ts +229 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +17 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +85 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +25 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +56 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +165 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +168 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/UpdaterPlugin/BundleInfo.swift +39 -0
- package/ios/Sources/UpdaterPlugin/BundleStatus.swift +9 -0
- package/ios/Sources/UpdaterPlugin/BundleStore.swift +233 -0
- package/ios/Sources/UpdaterPlugin/Downloader.swift +120 -0
- package/ios/Sources/UpdaterPlugin/HashUtils.swift +33 -0
- package/ios/Sources/UpdaterPlugin/HostedManifestKeys.swift +23 -0
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +161 -0
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +115 -0
- package/ios/Sources/UpdaterPlugin/StatsClient.swift +66 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +15 -0
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +913 -0
- package/ios/Sources/UpdaterPlugin/ZipUtils.swift +90 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OtaKit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# @otakit/capacitor-updater
|
|
2
|
+
|
|
3
|
+
Capacitor OTA updater plugin for OtaKit.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- checks the manifest endpoint for a newer bundle
|
|
8
|
+
- downloads and verifies OTA bundles
|
|
9
|
+
- stages updates safely
|
|
10
|
+
- activates them on the next launch or immediately during startup
|
|
11
|
+
- also supports fully manual update prompts when the app wants control
|
|
12
|
+
- requires `notifyAppReady()` as the success handshake
|
|
13
|
+
- rolls back automatically if the new bundle does not prove healthy
|
|
14
|
+
|
|
15
|
+
For normal app code, the main public methods are:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const state = await OtaKit.getState();
|
|
19
|
+
const latest = await OtaKit.check();
|
|
20
|
+
|
|
21
|
+
// Low-level manual flow:
|
|
22
|
+
const bundle = await OtaKit.download();
|
|
23
|
+
if (bundle) {
|
|
24
|
+
await OtaKit.apply();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Or the one-shot manual helper:
|
|
28
|
+
await OtaKit.update();
|
|
29
|
+
await OtaKit.notifyAppReady();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For manual mode, `getState()` tells you if something is already staged,
|
|
33
|
+
`check()` tells you whether a newer update exists, `download()` stages it, and
|
|
34
|
+
`update()` is the one-shot helper that downloads and applies the newest update.
|
|
35
|
+
|
|
36
|
+
Manual inspection and support methods like state inspection and reset live under
|
|
37
|
+
`OtaKit.debug`.
|
|
38
|
+
|
|
39
|
+
## Hosted config
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
plugins: {
|
|
43
|
+
OtaKit: {
|
|
44
|
+
appId: "YOUR_OTAKIT_APP_ID",
|
|
45
|
+
appReadyTimeout: 10000,
|
|
46
|
+
// Optional:
|
|
47
|
+
// channel: "staging",
|
|
48
|
+
// updateMode: "manual",
|
|
49
|
+
// updateMode: "immediate",
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Advanced overrides for self-hosting or custom trust only:
|
|
55
|
+
|
|
56
|
+
- `serverUrl`
|
|
57
|
+
- `manifestKeys`
|
|
58
|
+
- `allowInsecureUrls`
|
|
59
|
+
|
|
60
|
+
Hosted OtaKit already points at `https://otakit.app/api/v1` and already trusts
|
|
61
|
+
the managed manifest signing keys.
|
|
62
|
+
|
|
63
|
+
## Trust model
|
|
64
|
+
|
|
65
|
+
The plugin does not just download from a URL and trust the result.
|
|
66
|
+
|
|
67
|
+
1. it fetches a manifest from the server
|
|
68
|
+
2. it verifies the manifest signature when manifest keys are configured
|
|
69
|
+
3. it downloads the bundle zip
|
|
70
|
+
4. it verifies the zip against the manifest `sha256`
|
|
71
|
+
5. it stages and activates the bundle
|
|
72
|
+
|
|
73
|
+
In the hosted path, managed signing keys are already built in.
|
|
74
|
+
|
|
75
|
+
## Update modes
|
|
76
|
+
|
|
77
|
+
- `manual`
|
|
78
|
+
no automatic startup check and no automatic staged activation
|
|
79
|
+
- `next-launch`
|
|
80
|
+
automatic startup check, download in the background, activate on the next cold launch
|
|
81
|
+
- `immediate`
|
|
82
|
+
automatic startup check, then download and activate during startup
|
|
83
|
+
|
|
84
|
+
The default is `next-launch`.
|
|
85
|
+
|
|
86
|
+
## Runtime model
|
|
87
|
+
|
|
88
|
+
The plugin keeps three important runtime pointers:
|
|
89
|
+
|
|
90
|
+
- `current`
|
|
91
|
+
the bundle the WebView is currently serving
|
|
92
|
+
- `fallback`
|
|
93
|
+
the last known-good bundle used for rollback
|
|
94
|
+
- `staged`
|
|
95
|
+
a downloaded bundle waiting to be activated
|
|
96
|
+
|
|
97
|
+
Bundle lifecycle is intentionally small:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
download -> pending -> trial -> success
|
|
101
|
+
|
|
|
102
|
+
+-> error -> rollback -> delete failed files
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
In practice:
|
|
106
|
+
|
|
107
|
+
- downloads start as `pending`
|
|
108
|
+
- an activated bundle becomes `trial`
|
|
109
|
+
- `notifyAppReady()` promotes `trial` to `success`
|
|
110
|
+
- timeout or restart during `trial` causes rollback
|
|
111
|
+
|
|
112
|
+
That gives the plugin a small but important safety model:
|
|
113
|
+
|
|
114
|
+
- `current` is what is serving now
|
|
115
|
+
- `fallback` is the last known-good bundle
|
|
116
|
+
- `staged` is what can be activated next
|
|
117
|
+
- rollback always goes back to `fallback`, not to an arbitrary older bundle
|
|
118
|
+
|
|
119
|
+
## Automatic and manual flows
|
|
120
|
+
|
|
121
|
+
### Automatic flow
|
|
122
|
+
|
|
123
|
+
For the normal hosted path, the app usually just needs to call:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await OtaKit.notifyAppReady();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The plugin handles checking, downloading, activation, and rollback based on
|
|
130
|
+
`updateMode`.
|
|
131
|
+
|
|
132
|
+
For most apps, this is the entire runtime integration.
|
|
133
|
+
|
|
134
|
+
### Manual flow
|
|
135
|
+
|
|
136
|
+
For app-driven prompts:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const state = await OtaKit.getState();
|
|
140
|
+
const latest = await OtaKit.check();
|
|
141
|
+
|
|
142
|
+
if (latest) {
|
|
143
|
+
await OtaKit.update();
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Or the split version:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
await OtaKit.download();
|
|
151
|
+
await OtaKit.apply();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Use the split flow when the app wants to download in the background and switch
|
|
155
|
+
only after explicit user confirmation.
|
|
156
|
+
|
|
157
|
+
## Retention and deletion
|
|
158
|
+
|
|
159
|
+
- the builtin, current, fallback, and staged bundles are protected
|
|
160
|
+
- superseded staged bundles are deleted automatically
|
|
161
|
+
- failed trial bundles are deleted during rollback
|
|
162
|
+
- `debug.deleteBundle()` only works for downloaded bundles outside runtime state
|
|
163
|
+
|
|
164
|
+
## Source areas
|
|
165
|
+
|
|
166
|
+
- `src/definitions.ts`: public types and methods
|
|
167
|
+
- `src/index.ts`: Capacitor registration and JS wrapper
|
|
168
|
+
- `src/web.ts`: web fallback implementation
|
|
169
|
+
- `ios/Sources/UpdaterPlugin/*`: iOS implementation
|
|
170
|
+
- `android/src/main/java/com/updatekit/updater/*`: Android implementation
|
|
171
|
+
|
|
172
|
+
## Build locally
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pnpm --filter @otakit/capacitor-updater build
|
|
176
|
+
pnpm --filter @otakit/capacitor-updater typecheck
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Repo-level verification:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npm run build
|
|
183
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'UpdatekitUpdater'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.homepage = package['repository']['url']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
|
+
s.ios.deployment_target = '15.0'
|
|
15
|
+
s.dependency 'Capacitor'
|
|
16
|
+
s.dependency 'ZIPFoundation', '~> 0.9'
|
|
17
|
+
s.swift_version = '5.9'
|
|
18
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
ext {
|
|
2
|
+
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
|
3
|
+
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5'
|
|
4
|
+
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
buildscript {
|
|
8
|
+
repositories {
|
|
9
|
+
google()
|
|
10
|
+
mavenCentral()
|
|
11
|
+
}
|
|
12
|
+
dependencies {
|
|
13
|
+
classpath 'com.android.tools.build:gradle:8.2.2'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
apply plugin: 'com.android.library'
|
|
18
|
+
|
|
19
|
+
android {
|
|
20
|
+
namespace "com.updatekit.updater"
|
|
21
|
+
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
|
|
22
|
+
defaultConfig {
|
|
23
|
+
minSdk project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
|
|
24
|
+
targetSdk project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
|
|
25
|
+
versionCode 1
|
|
26
|
+
versionName "0.0.1"
|
|
27
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
28
|
+
}
|
|
29
|
+
compileOptions {
|
|
30
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
31
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
32
|
+
}
|
|
33
|
+
lintOptions {
|
|
34
|
+
abortOnError false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
repositories {
|
|
39
|
+
google()
|
|
40
|
+
mavenCentral()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
dependencies {
|
|
44
|
+
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
|
45
|
+
implementation project(':capacitor-android')
|
|
46
|
+
testImplementation "junit:junit:$junitVersion"
|
|
47
|
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
48
|
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
49
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
package com.updatekit.updater;
|
|
2
|
+
|
|
3
|
+
import com.getcapacitor.JSObject;
|
|
4
|
+
import org.json.JSONException;
|
|
5
|
+
import org.json.JSONObject;
|
|
6
|
+
|
|
7
|
+
class BundleInfo {
|
|
8
|
+
|
|
9
|
+
final String id;
|
|
10
|
+
final String version;
|
|
11
|
+
final BundleStatus status;
|
|
12
|
+
final Long downloadedAt;
|
|
13
|
+
final String sha256;
|
|
14
|
+
final String path;
|
|
15
|
+
final String channel;
|
|
16
|
+
final String releaseId;
|
|
17
|
+
|
|
18
|
+
BundleInfo(
|
|
19
|
+
String id,
|
|
20
|
+
String version,
|
|
21
|
+
BundleStatus status,
|
|
22
|
+
Long downloadedAt,
|
|
23
|
+
String sha256,
|
|
24
|
+
String path,
|
|
25
|
+
String channel,
|
|
26
|
+
String releaseId
|
|
27
|
+
) {
|
|
28
|
+
this.id = id;
|
|
29
|
+
this.version = version;
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.downloadedAt = downloadedAt;
|
|
32
|
+
this.sha256 = sha256;
|
|
33
|
+
this.path = path;
|
|
34
|
+
this.channel = channel;
|
|
35
|
+
this.releaseId = releaseId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
boolean isBuiltin() {
|
|
39
|
+
return "builtin".equals(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
JSObject toJSObject() {
|
|
43
|
+
JSObject object = new JSObject();
|
|
44
|
+
object.put("id", id);
|
|
45
|
+
object.put("version", version);
|
|
46
|
+
object.put("status", status.value());
|
|
47
|
+
if (downloadedAt != null) {
|
|
48
|
+
object.put("downloadedAt", DateUtils.toIsoString(downloadedAt));
|
|
49
|
+
}
|
|
50
|
+
if (sha256 != null) {
|
|
51
|
+
object.put("sha256", sha256);
|
|
52
|
+
}
|
|
53
|
+
if (channel != null) {
|
|
54
|
+
object.put("channel", channel);
|
|
55
|
+
}
|
|
56
|
+
if (releaseId != null) {
|
|
57
|
+
object.put("releaseId", releaseId);
|
|
58
|
+
}
|
|
59
|
+
return object;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
JSONObject toJSONObject() throws JSONException {
|
|
63
|
+
JSONObject object = new JSONObject();
|
|
64
|
+
object.put("id", id);
|
|
65
|
+
object.put("version", version);
|
|
66
|
+
object.put("status", status.value());
|
|
67
|
+
if (downloadedAt != null) {
|
|
68
|
+
object.put("downloadedAt", downloadedAt);
|
|
69
|
+
}
|
|
70
|
+
if (sha256 != null) {
|
|
71
|
+
object.put("sha256", sha256);
|
|
72
|
+
}
|
|
73
|
+
if (channel != null) {
|
|
74
|
+
object.put("channel", channel);
|
|
75
|
+
}
|
|
76
|
+
if (releaseId != null) {
|
|
77
|
+
object.put("releaseId", releaseId);
|
|
78
|
+
}
|
|
79
|
+
if (path != null) {
|
|
80
|
+
object.put("path", path);
|
|
81
|
+
}
|
|
82
|
+
return object;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static BundleInfo fromJSONObject(JSONObject object) {
|
|
86
|
+
String id = object.optString("id", "builtin");
|
|
87
|
+
String version = object.optString("version", "0.0.0");
|
|
88
|
+
BundleStatus status = BundleStatus.from(object.optString("status", "pending"));
|
|
89
|
+
Long downloadedAt = object.has("downloadedAt") ? object.optLong("downloadedAt") : null;
|
|
90
|
+
String sha256 = object.has("sha256") ? object.optString("sha256", null) : null;
|
|
91
|
+
String channel = object.has("channel") ? object.optString("channel", null) : null;
|
|
92
|
+
String releaseId = object.has("releaseId") ? object.optString("releaseId", null) : null;
|
|
93
|
+
String path = object.has("path") ? object.optString("path", null) : null;
|
|
94
|
+
return new BundleInfo(id, version, status, downloadedAt, sha256, path, channel, releaseId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
BundleInfo withStatus(BundleStatus nextStatus) {
|
|
98
|
+
return new BundleInfo(id, version, nextStatus, downloadedAt, sha256, path, channel, releaseId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.updatekit.updater;
|
|
2
|
+
|
|
3
|
+
enum BundleStatus {
|
|
4
|
+
BUILTIN("builtin"),
|
|
5
|
+
PENDING("pending"),
|
|
6
|
+
TRIAL("trial"),
|
|
7
|
+
SUCCESS("success"),
|
|
8
|
+
ERROR("error");
|
|
9
|
+
|
|
10
|
+
private final String value;
|
|
11
|
+
|
|
12
|
+
BundleStatus(String value) {
|
|
13
|
+
this.value = value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public String value() {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public static BundleStatus from(String raw) {
|
|
21
|
+
for (BundleStatus status : values()) {
|
|
22
|
+
if (status.value.equals(raw)) {
|
|
23
|
+
return status;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return PENDING;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
package com.updatekit.updater;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.SharedPreferences;
|
|
5
|
+
import com.getcapacitor.JSArray;
|
|
6
|
+
import java.io.File;
|
|
7
|
+
import java.io.FileInputStream;
|
|
8
|
+
import java.io.FileOutputStream;
|
|
9
|
+
import java.nio.charset.StandardCharsets;
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Arrays;
|
|
12
|
+
import java.util.Comparator;
|
|
13
|
+
import java.util.List;
|
|
14
|
+
import org.json.JSONException;
|
|
15
|
+
import org.json.JSONObject;
|
|
16
|
+
|
|
17
|
+
final class BundleStore {
|
|
18
|
+
|
|
19
|
+
private static final String PREFS_NAME = "otakit_updater_state";
|
|
20
|
+
private static final String KEY_CURRENT = "current_bundle_id";
|
|
21
|
+
private static final String KEY_FALLBACK = "fallback_bundle_id";
|
|
22
|
+
private static final String KEY_STAGED = "staged_bundle_id";
|
|
23
|
+
private static final String KEY_FAILED_INFO = "failed_bundle_info";
|
|
24
|
+
|
|
25
|
+
private final Context context;
|
|
26
|
+
private final SharedPreferences prefs;
|
|
27
|
+
private final File bundlesDirectory;
|
|
28
|
+
private final String builtinVersion;
|
|
29
|
+
private final String nativeBuild;
|
|
30
|
+
|
|
31
|
+
BundleStore(Context context, String builtinVersion, String nativeBuild) {
|
|
32
|
+
this.context = context.getApplicationContext();
|
|
33
|
+
this.prefs = this.context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
34
|
+
this.builtinVersion = builtinVersion;
|
|
35
|
+
this.nativeBuild = nativeBuild;
|
|
36
|
+
this.bundlesDirectory = new File(this.context.getFilesDir(), "otakit_bundles");
|
|
37
|
+
if (!bundlesDirectory.exists()) {
|
|
38
|
+
//noinspection ResultOfMethodCallIgnored
|
|
39
|
+
bundlesDirectory.mkdirs();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
String getNativeBuild() {
|
|
44
|
+
return nativeBuild;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
String getBuiltinVersion() {
|
|
48
|
+
return builtinVersion;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
BundleInfo builtinBundle() {
|
|
52
|
+
return new BundleInfo(
|
|
53
|
+
"builtin",
|
|
54
|
+
builtinVersion,
|
|
55
|
+
BundleStatus.BUILTIN,
|
|
56
|
+
null,
|
|
57
|
+
null,
|
|
58
|
+
null,
|
|
59
|
+
null,
|
|
60
|
+
null
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
File bundleDirectory(String id) {
|
|
65
|
+
return new File(bundlesDirectory, id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private File metadataFile(String id) {
|
|
69
|
+
return new File(bundleDirectory(id), "bundle.json");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
synchronized void saveBundle(BundleInfo bundle) throws Exception {
|
|
73
|
+
if (bundle.isBuiltin()) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
File directory = bundleDirectory(bundle.id);
|
|
78
|
+
if (!directory.exists() && !directory.mkdirs()) {
|
|
79
|
+
throw new IllegalStateException("Failed to create bundle directory");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
byte[] bytes = bundle.toJSONObject().toString().getBytes(StandardCharsets.UTF_8);
|
|
83
|
+
try (FileOutputStream output = new FileOutputStream(metadataFile(bundle.id), false)) {
|
|
84
|
+
output.write(bytes);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
synchronized BundleInfo getBundle(String id) {
|
|
89
|
+
if ("builtin".equals(id)) {
|
|
90
|
+
return builtinBundle();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
File metadata = metadataFile(id);
|
|
94
|
+
if (!metadata.exists()) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try (FileInputStream input = new FileInputStream(metadata)) {
|
|
99
|
+
byte[] bytes = readAllBytes(input);
|
|
100
|
+
JSONObject json = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
|
|
101
|
+
return BundleInfo.fromJSONObject(json);
|
|
102
|
+
} catch (Exception ignored) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private static byte[] readAllBytes(FileInputStream input) throws Exception {
|
|
108
|
+
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
|
|
109
|
+
byte[] chunk = new byte[8192];
|
|
110
|
+
int read;
|
|
111
|
+
while ((read = input.read(chunk)) != -1) {
|
|
112
|
+
buffer.write(chunk, 0, read);
|
|
113
|
+
}
|
|
114
|
+
return buffer.toByteArray();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
synchronized boolean bundleExists(String id) {
|
|
118
|
+
return getBundle(id) != null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
synchronized BundleInfo getCurrentBundle() {
|
|
122
|
+
String currentId = prefs.getString(KEY_CURRENT, null);
|
|
123
|
+
if (currentId == null) {
|
|
124
|
+
return builtinBundle();
|
|
125
|
+
}
|
|
126
|
+
BundleInfo current = getBundle(currentId);
|
|
127
|
+
return current != null ? current : builtinBundle();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
synchronized void setCurrentBundleId(String id) {
|
|
131
|
+
SharedPreferences.Editor editor = prefs.edit();
|
|
132
|
+
if (id == null) {
|
|
133
|
+
editor.remove(KEY_CURRENT);
|
|
134
|
+
} else {
|
|
135
|
+
editor.putString(KEY_CURRENT, id);
|
|
136
|
+
}
|
|
137
|
+
editor.commit();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
synchronized BundleInfo getFallbackBundle() {
|
|
141
|
+
String fallbackId = prefs.getString(KEY_FALLBACK, null);
|
|
142
|
+
if (fallbackId == null) {
|
|
143
|
+
return builtinBundle();
|
|
144
|
+
}
|
|
145
|
+
BundleInfo fallback = getBundle(fallbackId);
|
|
146
|
+
return fallback != null ? fallback : builtinBundle();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
synchronized void setFallbackBundleId(String id) {
|
|
150
|
+
SharedPreferences.Editor editor = prefs.edit();
|
|
151
|
+
if (id == null) {
|
|
152
|
+
editor.remove(KEY_FALLBACK);
|
|
153
|
+
} else {
|
|
154
|
+
editor.putString(KEY_FALLBACK, id);
|
|
155
|
+
}
|
|
156
|
+
editor.commit();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
synchronized String getStagedBundleId() {
|
|
160
|
+
return prefs.getString(KEY_STAGED, null);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
synchronized void setStagedBundleId(String id) {
|
|
164
|
+
SharedPreferences.Editor editor = prefs.edit();
|
|
165
|
+
if (id == null) {
|
|
166
|
+
editor.remove(KEY_STAGED);
|
|
167
|
+
} else {
|
|
168
|
+
editor.putString(KEY_STAGED, id);
|
|
169
|
+
}
|
|
170
|
+
editor.commit();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
synchronized void setFailedBundle(BundleInfo bundle) {
|
|
174
|
+
SharedPreferences.Editor editor = prefs.edit();
|
|
175
|
+
if (bundle == null) {
|
|
176
|
+
editor.remove(KEY_FAILED_INFO);
|
|
177
|
+
} else {
|
|
178
|
+
try {
|
|
179
|
+
editor.putString(KEY_FAILED_INFO, bundle.toJSONObject().toString());
|
|
180
|
+
} catch (JSONException ignored) {
|
|
181
|
+
editor.remove(KEY_FAILED_INFO);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
editor.apply();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
synchronized BundleInfo getFailedBundle() {
|
|
188
|
+
String json = prefs.getString(KEY_FAILED_INFO, null);
|
|
189
|
+
if (json == null) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
return BundleInfo.fromJSONObject(new JSONObject(json));
|
|
194
|
+
} catch (Exception e) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
synchronized void markStatus(String bundleId, BundleStatus status) {
|
|
200
|
+
BundleInfo existing = getBundle(bundleId);
|
|
201
|
+
if (existing == null) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
saveBundle(existing.withStatus(status));
|
|
206
|
+
} catch (Exception ignored) {}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
synchronized void deleteBundle(String id) throws Exception {
|
|
210
|
+
if ("builtin".equals(id)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
File directory = bundleDirectory(id);
|
|
215
|
+
deleteRecursively(directory);
|
|
216
|
+
|
|
217
|
+
if (id.equals(getStagedBundleId())) {
|
|
218
|
+
setStagedBundleId(null);
|
|
219
|
+
}
|
|
220
|
+
if (id.equals(prefs.getString(KEY_CURRENT, null))) {
|
|
221
|
+
setCurrentBundleId(null);
|
|
222
|
+
}
|
|
223
|
+
if (id.equals(prefs.getString(KEY_FALLBACK, null))) {
|
|
224
|
+
setFallbackBundleId(null);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
synchronized JSArray listDownloadedBundles() {
|
|
229
|
+
List<BundleInfo> result = new ArrayList<>();
|
|
230
|
+
File[] entries = bundlesDirectory.listFiles();
|
|
231
|
+
if (entries != null) {
|
|
232
|
+
Arrays.sort(entries, Comparator.comparing(File::getName));
|
|
233
|
+
for (File entry : entries) {
|
|
234
|
+
BundleInfo bundle = getBundle(entry.getName());
|
|
235
|
+
if (bundle != null) {
|
|
236
|
+
result.add(bundle);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
JSArray array = new JSArray();
|
|
242
|
+
for (BundleInfo bundle : result) {
|
|
243
|
+
array.put(bundle.toJSObject());
|
|
244
|
+
}
|
|
245
|
+
return array;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private void deleteRecursively(File target) throws Exception {
|
|
249
|
+
if (!target.exists()) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (target.isDirectory()) {
|
|
253
|
+
File[] children = target.listFiles();
|
|
254
|
+
if (children != null) {
|
|
255
|
+
for (File child : children) {
|
|
256
|
+
deleteRecursively(child);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!target.delete()) {
|
|
261
|
+
throw new IllegalStateException("Failed to delete " + target.getAbsolutePath());
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.updatekit.updater;
|
|
2
|
+
|
|
3
|
+
import java.text.SimpleDateFormat;
|
|
4
|
+
import java.util.Date;
|
|
5
|
+
import java.util.Locale;
|
|
6
|
+
import java.util.TimeZone;
|
|
7
|
+
|
|
8
|
+
final class DateUtils {
|
|
9
|
+
|
|
10
|
+
private DateUtils() {}
|
|
11
|
+
|
|
12
|
+
static String toIsoString(long timestampMs) {
|
|
13
|
+
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
|
|
14
|
+
format.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
15
|
+
return format.format(new Date(timestampMs));
|
|
16
|
+
}
|
|
17
|
+
}
|