feat(rtos): 安装前补传PNG图标并等待成功回调

这个提交包含在:
sydxky 于 2026-11-14 18:07:35 +08:00 提交
1 父节点 c3649c626f
当前提交 17b51d98ae
18 个文件已更改
+579 -48

没有与搜索条件相匹配的文件

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application><uses-library android:name="android.test.runner" /></application>
</manifest>
@@ -0,0 +1,152 @@
package com.sydxky.rtos.bridge;
import android.test.InstrumentationTestCase;
import android.graphics.*;
import android.os.*;
import org.json.*;
import java.io.*;
import java.nio.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.lang.reflect.Field;
/** Real Android codec/Binder tests with an explicit fake watch endpoint. No watch writes. */
public final class IconInstallTest extends InstrumentationTestCase {
private File inbox;
private WearEngine engine;
private Journal journal;
private FakeWatch watch;
private byte[] png;
@Override protected void setUp() throws Exception {
super.setUp();
inbox = new File(getInstrumentation().getTargetContext().getFilesDir(), "inbox"); inbox.mkdirs();
Bitmap bitmap = Bitmap.createBitmap(600, 300, Bitmap.Config.ARGB_8888);
bitmap.eraseColor(Color.BLUE); ByteArrayOutputStream output = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, output); bitmap.recycle(); png = output.toByteArray();
journal = new Journal(); engine = new WearEngine(getInstrumentation().getTargetContext(), journal); watch = new FakeWatch();
set("remote", watch); set("version", 2006006); set("descriptor", WearEngine.NEW); set("state", "bound"); set("authorized", true);
Files.write(new File(inbox, "icon-test.pkg").toPath(), bundle(png, false));
}
private void set(String name, Object value) throws Exception { Field field = WearEngine.class.getDeclaredField(name); field.setAccessible(true); field.set(engine, value); }
@Override protected void tearDown() throws Exception {
engine.close(); new File(inbox, "icon-test.pkg").delete(); new File(inbox, "icon-test.png").delete(); super.tearDown();
}
private interface Ready { boolean test() throws Exception; }
private void waitFor(Ready ready) throws Exception {
long deadline = System.currentTimeMillis() + 5000;
while (!ready.test()) { if (System.currentTimeMillis() > deadline) fail("Asynchronous callback timed out"); Thread.sleep(20); }
}
private String install() { return engine.execute(Journal.json("command", "install", "file", "icon-test.pkg")); }
public void testPngResizedWithAspectRatio() throws Exception {
byte[] encoded = WatchIconPng.encode(png); assertEquals((byte) 0x89, encoded[0]);
Bitmap image = BitmapFactory.decodeByteArray(encoded, 0, encoded.length);
assertEquals(192, image.getWidth()); assertEquals(96, image.getHeight()); image.recycle();
}
public void testCrAndCompiledManifest() throws Exception {
Files.write(new File(inbox, "icon-test.pkg").toPath(), bundle(png, true));
try (GlyphixPackage pkg = new GlyphixPackage(new File(inbox, "icon-test.pkg"))) {
assertEquals("com.example.icon", pkg.packageName); assertEquals("assets/icon.png", pkg.iconPath); assertEquals(png.length, pkg.icon().length);
}
}
public void testRealVendorBuiltPackageManifestAndPgf() throws Exception {
File source = new File(getInstrumentation().getTargetContext().getFilesDir(), "real-probe.pkg");
assertTrue("Push the locally built deviceprobe bundle as files/real-probe.pkg before running", source.isFile());
try (GlyphixPackage pkg = new GlyphixPackage(source)) {
assertEquals("com.sydxky.rtos.deviceprobe", pkg.packageName);
byte[] icon = pkg.icon(); assertEquals('P', icon[0]); assertEquals('G', icon[1]); assertEquals('F', icon[2]);
}
}
public void testInvalidResourceRangeStopsBeforeBinder() throws Exception {
byte[] invalid = bundle(png, false); ByteBuffer.wrap(invalid).order(ByteOrder.LITTLE_ENDIAN).putInt(36, Integer.MAX_VALUE);
Files.write(new File(inbox, "icon-test.pkg").toPath(), invalid);
String id = install(); waitFor(() -> !journal.pending(id)); assertEquals("failed", journal.job(id).getString("state")); assertEquals(0, watch.sent);
}
public void testInstallWaitsForCode200AndDeduplicates() throws Exception {
String id = install(); waitFor(() -> watch.iconResolver != null);
assertEquals("com.example.icon.png", watch.name); assertEquals("/user/jsapps/com.example.icon.png", watch.path); assertEquals(0, watch.installed);
assertEquals((byte) 0x89, watch.image[0]);
watch.callback(3, "{\"code\":200}"); Thread.sleep(100); assertEquals(0, watch.installed);
watch.callback(1, "{\"code\":200}"); waitFor(() -> watch.installed == 1);
watch.callback(1, "{\"code\":200}"); Thread.sleep(100); assertEquals(1, watch.installed);
waitFor(() -> !journal.pending(id)); assertEquals("success", journal.job(id).getString("state"));
}
public void testNon200StopsPackage() throws Exception { reject(1, "{\"code\":500}"); }
public void testFailureStopsPackage() throws Exception { reject(2, "{\"code\":200}"); }
public void testMalformedStopsPackage() throws Exception { reject(1, "not-json"); }
public void testNoCodeStopsPackage() throws Exception { reject(1, "{}"); }
private void reject(int code, String value) throws Exception {
String id = install(); waitFor(() -> watch.iconResolver != null); watch.callback(code, value);
waitFor(() -> !journal.pending(id)); assertEquals("failed", journal.job(id).getString("state")); assertEquals(0, watch.installed);
}
public void testPgfNeedsExplicitPng() throws Exception {
Files.write(new File(inbox, "icon-test.pkg").toPath(), bundle(new byte[]{'P','G','F',0}, false));
String id = install(); waitFor(() -> !journal.pending(id)); assertEquals("failed", journal.job(id).getString("state")); assertEquals(0, watch.sent);
Files.write(new File(inbox, "icon-test.png").toPath(), png);
String next = engine.execute(Journal.json("command", "install", "file", "icon-test.pkg", "iconFile", "icon-test.png"));
waitFor(() -> watch.iconResolver != null); watch.callback(1, "{\"code\":200}");
waitFor(() -> !journal.pending(next)); assertEquals(1, watch.installed);
}
public void testDisconnectedCallbackCannotInstall() throws Exception {
String id = install(); waitFor(() -> watch.iconResolver != null);
engine.disconnect(); getInstrumentation().waitForIdleSync();
watch.callback(1, "{\"code\":200}"); Thread.sleep(150); assertEquals(0, watch.installed); assertFalse(journal.pending(id));
}
public void testLateCallbackAfterTimeoutCannotInstall() throws Exception {
String id = install(); waitFor(() -> watch.iconResolver != null);
journal.result(id, "timeout", "test deadline"); watch.callback(1, "{\"code\":200}");
Thread.sleep(100); assertEquals(0, watch.installed);
}
public void testAuthorizationFailureStopsAllTransfers() throws Exception {
set("authorized", false); String id = install(); waitFor(() -> !journal.pending(id)); assertEquals(0, watch.sent); assertEquals(0, watch.installed);
}
private static byte[] bundle(byte[] icon, boolean compiled) throws Exception {
byte[] manifest;
if (compiled) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(ByteBuffer.allocate(16).order(ByteOrder.LITTLE_ENDIAN).putInt(0x0168b5ae).putInt(0).putInt(0).putInt(1).array());
for (String text : new String[]{"com.example.icon", "测试", "1.0.0", "/assets/icon.png"}) {
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
out.write(bytes.length & 255); out.write(bytes.length >> 8); out.write(bytes);
}
manifest = out.toByteArray();
} else manifest = "{\"package\":\"com.example.icon\",\"icon\":\"/assets/icon.png\"}".getBytes(StandardCharsets.UTF_8);
byte[] names = "manifest.jsonassets/icon.png".getBytes(StandardCharsets.UTF_8);
int start = 64 + names.length;
ByteBuffer data = ByteBuffer.allocate(start + manifest.length + icon.length).order(ByteOrder.LITTLE_ENDIAN);
data.putInt(0x5243).putInt(2).position(32);
data.putInt(0).putInt(start).putInt(manifest.length).putInt(13);
data.putInt(0).putInt(start + manifest.length).putInt(icon.length).putInt(15);
data.put(names).put(manifest).put(icon); return data.array();
}
private static final class FakeWatch extends Binder {
volatile IBinder iconResolver;
volatile String name, path;
volatile byte[] image;
volatile int sent, installed;
@Override protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
data.enforceInterface(WearEngine.NEW);
try {
if (code == 15) {
data.readInt(); ParcelFileDescriptor fd = ParcelFileDescriptor.CREATOR.createFromParcel(data);
name = data.readString(); path = data.readString(); data.readString();
try (InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(fd)) {
ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; int n;
while ((n = input.read(buffer)) != -1) output.write(buffer, 0, n); image = output.toByteArray();
}
sent++; iconResolver = data.readStrongBinder();
} else if (code == 16) {
data.readString(); data.readInt(); ParcelFileDescriptor fd = ParcelFileDescriptor.CREATOR.createFromParcel(data); fd.close();
String uri = data.readString(); if (!uri.startsWith("remote://mass/bridge_")) throw new IOException("Wrong package URI");
IBinder resolver = data.readStrongBinder(); installed++; resolve(resolver, 1, "{\"code\":200}");
}
} catch (Exception error) { throw new RemoteException(error.toString()); }
reply.writeNoException(); return true;
}
void callback(int code, String raw) throws Exception { resolve(iconResolver, code, raw); }
private static void resolve(IBinder callback, int code, String raw) throws Exception {
Parcel data = Parcel.obtain(), reply = Parcel.obtain();
try { data.writeInterfaceToken("com.glyphix.mas.WearEngineProgressResolver"); data.writeString(raw); callback.transact(code, data, reply, 0); reply.readException(); }
finally { data.recycle(); reply.recycle(); }
}
}
}