Release Healer Man 0.1.29
This commit is contained in:
1 parent
8c9809d3e0
commit
544c1bee91
120 files changed
+4363
-1036
No files matched your search
@@ -24,6 +24,8 @@ android {
|
||||
versionName releaseVersionName
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Dungeon objects already carry verified lossless gzip compression.
|
||||
noCompress 'gzip'
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.phenomrom.healerman;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import com.getcapacitor.Bridge;
|
||||
import org.json.JSONObject;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/** Streams shared APK objects at their original logical URLs; no expanded disk copy. */
|
||||
final class BundledContentAssets {
|
||||
private final Bridge bridge;
|
||||
private final AssetManager assets;
|
||||
private JSONObject files;
|
||||
|
||||
BundledContentAssets(Bridge bridge) {
|
||||
this.bridge = bridge;
|
||||
this.assets = bridge.getContext().getAssets();
|
||||
}
|
||||
|
||||
private synchronized JSONObject files() throws Exception {
|
||||
if (files == null) {
|
||||
try (InputStream input = assets.open("public/content/bootstrap-manifest.json")) {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[16384];
|
||||
int count;
|
||||
while ((count = input.read(buffer)) != -1) output.write(buffer, 0, count);
|
||||
files = new JSONObject(output.toString("UTF-8")).getJSONObject("files");
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
WebResourceResponse intercept(WebResourceRequest request) {
|
||||
if (!"GET".equals(request.getMethod()) || !bridge.getHost().equals(request.getUrl().getAuthority())
|
||||
|| !bridge.getScheme().equals(request.getUrl().getScheme())) return null;
|
||||
String path = request.getUrl().getPath();
|
||||
if (path == null || !(path.startsWith("/assets/") || path.startsWith("/basis/") || path.startsWith("/data/"))) return null;
|
||||
try {
|
||||
JSONObject entry = files().optJSONObject(path);
|
||||
if (entry == null) return null;
|
||||
JSONObject download = entry.optJSONObject("download");
|
||||
String objectUrl = (download == null ? entry : download).getString("url");
|
||||
if (!objectUrl.matches("/content/bundled/[a-f0-9]{64}\\.[a-z0-9]+(?:\\.g(?:z|zip))?")) return null;
|
||||
InputStream input = assets.open("public" + objectUrl);
|
||||
if (download != null) {
|
||||
try { input = new GZIPInputStream(input, 128 * 1024); }
|
||||
catch (Exception error) { input.close(); throw error; }
|
||||
}
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("Content-Length", String.valueOf(entry.getLong("bytes")));
|
||||
headers.put("Cache-Control", "public, max-age=3600");
|
||||
return new WebResourceResponse(mimeType(path), null, 200, "OK", headers, input);
|
||||
} catch (Exception error) {
|
||||
return new WebResourceResponse("text/plain", "UTF-8", 500, "Bundled asset unavailable", new HashMap<>(),
|
||||
new ByteArrayInputStream("Bundled asset unavailable.".getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
}
|
||||
|
||||
private static String mimeType(String path) {
|
||||
String extension = path.substring(path.lastIndexOf('.') + 1);
|
||||
switch (extension) {
|
||||
case "glb": return "model/gltf-binary";
|
||||
case "json": return "application/json";
|
||||
case "js": return "text/javascript";
|
||||
case "wasm": return "application/wasm";
|
||||
case "ktx2": return "image/ktx2";
|
||||
case "png": return "image/png";
|
||||
case "webp": return "image/webp";
|
||||
case "avif": return "image/avif";
|
||||
case "svg": return "image/svg+xml";
|
||||
case "mp3": return "audio/mpeg";
|
||||
case "ogg": return "audio/ogg";
|
||||
case "wav": return "audio/wav";
|
||||
case "webm": return "video/webm";
|
||||
default: return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
import com.getcapacitor.BridgeWebViewClient;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
@@ -20,6 +24,7 @@ import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/** Streams verified, resumable content objects into Android app-private storage. */
|
||||
@CapacitorPlugin(name = "ContentStorage")
|
||||
@@ -27,6 +32,18 @@ public class ContentStoragePlugin extends Plugin {
|
||||
private static final int BUFFER_BYTES = 128 * 1024;
|
||||
private static final long PROGRESS_INTERVAL_MS = 200L;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
BundledContentAssets bundled = new BundledContentAssets(bridge);
|
||||
bridge.setWebViewClient(new BridgeWebViewClient(bridge) {
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
WebResourceResponse response = bundled.intercept(request);
|
||||
return response != null ? response : super.shouldInterceptRequest(view, request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void download(PluginCall call) {
|
||||
execute(() -> {
|
||||
@@ -36,6 +53,10 @@ public class ContentStoragePlugin extends Plugin {
|
||||
String sha256 = normalizedSha(requiredString(call, "sha256"));
|
||||
String extension = normalizedExtension(requiredString(call, "extension"));
|
||||
long expectedBytes = numericLong(call, "bytes", -1L);
|
||||
boolean compressed = "gzip".equals(call.getString("encoding"));
|
||||
String transferSha = compressed ? normalizedSha(requiredString(call, "downloadSha256")) : sha256;
|
||||
long transferBytes = compressed ? numericLong(call, "downloadBytes", -1L) : expectedBytes;
|
||||
if (expectedBytes < 0 || transferBytes < 0) throw new IllegalArgumentException("Missing content size.");
|
||||
validateRemoteUrl(url);
|
||||
|
||||
File destination = objectFile(sha256, extension);
|
||||
@@ -47,54 +68,89 @@ public class ContentStoragePlugin extends Plugin {
|
||||
throw new IllegalStateException("Unable to replace invalid cached content.");
|
||||
}
|
||||
|
||||
File partial = new File(destination.getParentFile(), destination.getName() + ".part");
|
||||
long existingBytes = partial.isFile() ? partial.length() : 0L;
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(15_000);
|
||||
connection.setReadTimeout(45_000);
|
||||
connection.setRequestProperty("Accept-Encoding", "identity");
|
||||
if (existingBytes > 0L) connection.setRequestProperty("Range", "bytes=" + existingBytes + "-");
|
||||
int status = connection.getResponseCode();
|
||||
boolean resumed = existingBytes > 0L && status == HttpURLConnection.HTTP_PARTIAL;
|
||||
if (status != HttpURLConnection.HTTP_OK && status != HttpURLConnection.HTTP_PARTIAL) {
|
||||
connection.disconnect();
|
||||
throw new IllegalStateException("Content server returned HTTP " + status + ".");
|
||||
}
|
||||
if (!resumed) existingBytes = 0L;
|
||||
long downloadedBytes = existingBytes;
|
||||
long totalBytes = expectedBytes > 0L
|
||||
? expectedBytes
|
||||
: existingBytes + Math.max(0L, connection.getContentLengthLong());
|
||||
long lastProgressAt = 0L;
|
||||
|
||||
try (
|
||||
InputStream input = new BufferedInputStream(connection.getInputStream(), BUFFER_BYTES);
|
||||
BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(partial, resumed), BUFFER_BYTES)
|
||||
) {
|
||||
byte[] buffer = new byte[BUFFER_BYTES];
|
||||
int count;
|
||||
while ((count = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, count);
|
||||
downloadedBytes += count;
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) {
|
||||
notifyProgress(logicalPath, downloadedBytes, totalBytes);
|
||||
lastProgressAt = now;
|
||||
File partial = new File(destination.getParentFile(), destination.getName() + (compressed ? "." + transferSha + ".gz" : "") + ".part");
|
||||
if (!verifies(partial, transferSha, transferBytes)) {
|
||||
if (partial.isFile() && partial.length() >= transferBytes && !partial.delete()) {
|
||||
throw new IllegalStateException("Unable to reset an invalid partial download.");
|
||||
}
|
||||
long existingBytes = partial.isFile() ? partial.length() : 0L;
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(15_000);
|
||||
connection.setReadTimeout(45_000);
|
||||
connection.setRequestProperty("Accept-Encoding", "identity");
|
||||
if (existingBytes > 0L) connection.setRequestProperty("Range", "bytes=" + existingBytes + "-");
|
||||
int status = connection.getResponseCode();
|
||||
boolean resumed = existingBytes > 0L && status == HttpURLConnection.HTTP_PARTIAL;
|
||||
if (status != HttpURLConnection.HTTP_OK && status != HttpURLConnection.HTTP_PARTIAL) {
|
||||
connection.disconnect();
|
||||
throw new IllegalStateException("Content server returned HTTP " + status + ".");
|
||||
}
|
||||
if (status == HttpURLConnection.HTTP_PARTIAL) {
|
||||
String range = connection.getHeaderField("Content-Range");
|
||||
if (!resumed || range == null || !range.startsWith("bytes " + existingBytes + "-")) {
|
||||
connection.disconnect();
|
||||
partial.delete();
|
||||
throw new IllegalStateException("Content server returned an invalid resume range.");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
notifyProgress(logicalPath, downloadedBytes, totalBytes);
|
||||
if (!resumed) existingBytes = 0L;
|
||||
long downloadedBytes = existingBytes;
|
||||
long totalBytes = transferBytes > 0L
|
||||
? transferBytes
|
||||
: existingBytes + Math.max(0L, connection.getContentLengthLong());
|
||||
long lastProgressAt = 0L;
|
||||
|
||||
if (!verifies(partial, sha256, expectedBytes)) {
|
||||
try (
|
||||
InputStream input = new BufferedInputStream(connection.getInputStream(), BUFFER_BYTES);
|
||||
BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(partial, resumed), BUFFER_BYTES)
|
||||
) {
|
||||
byte[] buffer = new byte[BUFFER_BYTES];
|
||||
int count;
|
||||
while ((count = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, count);
|
||||
downloadedBytes += count;
|
||||
if (downloadedBytes > transferBytes) throw new IllegalStateException("Downloaded content exceeds expected size.");
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) {
|
||||
notifyProgress(logicalPath, downloadedBytes, totalBytes);
|
||||
lastProgressAt = now;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
notifyProgress(logicalPath, downloadedBytes, totalBytes);
|
||||
}
|
||||
|
||||
if (!verifies(partial, transferSha, transferBytes)) {
|
||||
partial.delete();
|
||||
throw new IllegalStateException("Downloaded content failed SHA-256 or size verification.");
|
||||
}
|
||||
File verified = partial;
|
||||
if (compressed) {
|
||||
verified = new File(destination.getParentFile(), destination.getName() + ".decoded");
|
||||
try (InputStream input = new GZIPInputStream(new FileInputStream(partial), BUFFER_BYTES);
|
||||
BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(verified), BUFFER_BYTES)) {
|
||||
byte[] buffer = new byte[BUFFER_BYTES];
|
||||
long decodedBytes = 0;
|
||||
int count;
|
||||
while ((count = input.read(buffer)) != -1) {
|
||||
decodedBytes += count;
|
||||
if (decodedBytes > expectedBytes) throw new IllegalStateException("Expanded content exceeds expected size.");
|
||||
output.write(buffer, 0, count);
|
||||
}
|
||||
} catch (Exception error) { verified.delete(); throw error; }
|
||||
if (!verifies(verified, sha256, expectedBytes)) {
|
||||
verified.delete();
|
||||
partial.delete();
|
||||
throw new IllegalStateException("Expanded content failed SHA-256 or size verification.");
|
||||
}
|
||||
partial.delete();
|
||||
}
|
||||
if (destination.exists() && !destination.delete()) {
|
||||
throw new IllegalStateException("Unable to activate downloaded content.");
|
||||
}
|
||||
if (!partial.renameTo(destination)) {
|
||||
if (!verified.renameTo(destination)) {
|
||||
throw new IllegalStateException("Unable to atomically activate downloaded content.");
|
||||
}
|
||||
call.resolve(fileResult(destination, false));
|
||||
|
||||
@@ -100,6 +100,8 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
|
||||
addAxisTokens(currentTokens, event.getAxisValue(MotionEvent.AXIS_HAT_Y), "Button12", "Button13");
|
||||
addAxisTokens(currentTokens, leftStickX, "Axis0-", "Axis0+");
|
||||
addAxisTokens(currentTokens, leftStickY, "Axis1-", "Axis1+");
|
||||
addAxisTokens(currentTokens, rightStickX, "Axis2-", "Axis2+");
|
||||
addAxisTokens(currentTokens, rightStickY, "Axis3-", "Axis3+");
|
||||
addPositiveAxisToken(currentTokens, positiveControllerAxisValue(event, MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_BRAKE), "Button6");
|
||||
addPositiveAxisToken(currentTokens, positiveControllerAxisValue(event, MotionEvent.AXIS_RTRIGGER, MotionEvent.AXIS_GAS), "Button7");
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ public class ThorDualScreenPlugin extends Plugin implements DisplayManager.Displ
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
return bridge.getLocalServer().shouldInterceptRequest(request);
|
||||
return bridge.getWebViewClient().shouldInterceptRequest(view, request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user