189 lines
8.2 KiB
TypeScript
189 lines
8.2 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
const root = path.resolve(import.meta.dir, "..");
|
|
const apk = path.resolve(
|
|
process.argv[2] ?? path.join(root, "inputs", "MoanNote.apk"),
|
|
);
|
|
const output = path.resolve(
|
|
process.argv[3] ?? path.join(root, "extracted", "xiaomi-eink"),
|
|
);
|
|
const decoded = path.join(output, ".decoded");
|
|
const apktool = path.join(root, "java", "apktool_2.4.1.jar");
|
|
|
|
function copyTree(source: string, destination: string): void {
|
|
fs.mkdirSync(destination, { recursive: true });
|
|
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
const from = path.join(source, entry.name);
|
|
const to = path.join(destination, entry.name);
|
|
if (entry.isDirectory()) copyTree(from, to);
|
|
else fs.copyFileSync(from, to);
|
|
}
|
|
}
|
|
|
|
function write(file: string, content: string): void {
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, content.trimStart());
|
|
}
|
|
|
|
function main(): void {
|
|
if (!fs.existsSync(apk)) throw new Error(`APK not found at ${apk}`);
|
|
fs.rmSync(decoded, { recursive: true, force: true });
|
|
fs.mkdirSync(output, { recursive: true });
|
|
const result = spawnSync(
|
|
"java",
|
|
["-jar", apktool, "d", "-f", "-r", apk, "-o", decoded],
|
|
{ stdio: "inherit" },
|
|
);
|
|
if (result.status !== 0)
|
|
throw new Error(`Apktool decode failed with status ${result.status}`);
|
|
|
|
const nativeSource = path.join(decoded, "lib");
|
|
if (!fs.existsSync(nativeSource))
|
|
throw new Error("MoanNote contains no native libraries");
|
|
const jniLibs = path.join(output, "android", "src", "main", "jniLibs");
|
|
for (const abi of fs.readdirSync(nativeSource)) {
|
|
const library = path.join(nativeSource, abi, "libfakeRKEpdUtils.so");
|
|
if (!fs.existsSync(library)) continue;
|
|
fs.mkdirSync(path.join(jniLibs, abi), { recursive: true });
|
|
fs.copyFileSync(library, path.join(jniLibs, abi, path.basename(library)));
|
|
}
|
|
const javaRoot = path.join(output, "android", "src", "main", "java");
|
|
write(
|
|
path.join(javaRoot, "com/moan/fakerkepdlib/EpdUtil.java"),
|
|
nativeBinding,
|
|
);
|
|
write(
|
|
path.join(javaRoot, "rip/crit/ms/eink/XiaomiEinkRenderer.java"),
|
|
facade,
|
|
);
|
|
write(path.join(output, "SYMBOLS.md"), symbolMap);
|
|
write(path.join(output, "README.md"), extractedReadme);
|
|
fs.rmSync(decoded, { recursive: true, force: true });
|
|
console.log(`[ok] Extracted E Ink compatibility library to ${output}`);
|
|
}
|
|
|
|
const nativeBinding = String.raw`
|
|
package com.moan.fakerkepdlib;
|
|
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.PointF;
|
|
import android.graphics.RectF;
|
|
|
|
public final class EpdUtil {
|
|
static { System.loadLibrary("fakeRKEpdUtils"); }
|
|
private EpdUtil() {}
|
|
private static native int JniCall_ClearOverlayBuffer(int id, int color, int sendOsd);
|
|
private static native int JniCall_CommitOverlay(Bitmap bitmap, int id, float left, float top, float right, float bottom, float x, float y);
|
|
private static native int JniCall_ObtainEBC(float[] bounds);
|
|
private static native void JniCall_ReleaseEBC(int id);
|
|
private static native int JniCall_SetOverLayBgControl(int id, int mode);
|
|
private static native int JniCall_SetOverLayCommit(int id, int enabled);
|
|
private static native int JniCall_test(int[] values);
|
|
|
|
public static EBCData sInitEBC() {
|
|
float[] bounds = new float[4];
|
|
int id = JniCall_ObtainEBC(bounds);
|
|
if (id < 0) return null;
|
|
EBCData data = new EBCData();
|
|
data.mDataID = id;
|
|
data.mCommitBufRect.set(bounds[0], bounds[1], bounds[2], bounds[3]);
|
|
return data;
|
|
}
|
|
public static void sRecycleEBC(EBCData data) { if (data != null) JniCall_ReleaseEBC(data.mDataID); }
|
|
public static int sSetFastBgRefresh(EBCData data, boolean enabled) { return JniCall_SetOverLayBgControl(data.mDataID, enabled ? 0 : 1); }
|
|
public static int sSetFastHandWrite(EBCData data, boolean enabled) { return JniCall_SetOverLayCommit(data.mDataID, enabled ? 1 : 0); }
|
|
public static int sCommitColor(EBCData data, EBCCommitRequest request, EBCCommitResult result) {
|
|
return JniCall_ClearOverlayBuffer(data.mDataID, request.mCommitColor, request.mSendOsd);
|
|
}
|
|
public static int sCommitImage(EBCData data, EBCCommitRequest request, EBCCommitResult result) {
|
|
int code = request.mCommitType == EBCCommitRequest.COMMIT_CLEAR_COLOR
|
|
? JniCall_ClearOverlayBuffer(data.mDataID, request.mCommitColor, request.mSendOsd)
|
|
: JniCall_CommitOverlay(request.mCommitImg, data.mDataID, request.mCommitSrcRect.left,
|
|
request.mCommitSrcRect.top, request.mCommitSrcRect.right, request.mCommitSrcRect.bottom,
|
|
request.mCommitPt.x, request.mCommitPt.y);
|
|
if (code > 0) result.mResultCode = 0;
|
|
return code;
|
|
}
|
|
public static final class EBCData {
|
|
public int mDataID;
|
|
public final RectF mCommitBufRect = new RectF();
|
|
public int getEBCRect(RectF destination) { destination.set(mCommitBufRect); return 0; }
|
|
}
|
|
public static final class EBCCommitResult { public int mResultCode; }
|
|
public static final class EBCCommitRequest {
|
|
public static final int COMMIT_NORMAL = 0, COMMIT_WITH_SCALE = 1, COMMIT_WITH_ROTATE = 2, COMMIT_CLEAR_COLOR = 4;
|
|
public int mCommitColor, mCommitType, mSendOsd;
|
|
public final RectF mCommitDstRect = new RectF();
|
|
public Bitmap mCommitImg;
|
|
public final PointF mCommitPt = new PointF();
|
|
public final RectF mCommitSrcRect = new RectF();
|
|
}
|
|
}
|
|
`;
|
|
|
|
const facade = String.raw`
|
|
package rip.crit.ms.eink;
|
|
|
|
import android.graphics.Bitmap;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Rect;
|
|
import android.view.View;
|
|
import com.moan.fakerkepdlib.EpdUtil;
|
|
|
|
public final class XiaomiEinkRenderer implements AutoCloseable {
|
|
private final EpdUtil.EBCData buffer;
|
|
public XiaomiEinkRenderer() {
|
|
buffer = EpdUtil.sInitEBC();
|
|
if (buffer == null) throw new IllegalStateException("The Xiaomi E Ink buffer is unavailable");
|
|
}
|
|
public void setFastHandwriting(boolean enabled) { EpdUtil.sSetFastHandWrite(buffer, enabled); }
|
|
public void setFastBackgroundRefresh(boolean enabled) { EpdUtil.sSetFastBgRefresh(buffer, enabled); }
|
|
public int commit(Bitmap bitmap, Rect dirtyArea) {
|
|
EpdUtil.EBCCommitRequest request = new EpdUtil.EBCCommitRequest();
|
|
request.mCommitImg = bitmap;
|
|
request.mCommitSrcRect.set(dirtyArea);
|
|
request.mCommitPt.set(dirtyArea.left, dirtyArea.top);
|
|
return EpdUtil.sCommitImage(buffer, request, new EpdUtil.EBCCommitResult());
|
|
}
|
|
public int commit(View view, Rect dirtyArea) {
|
|
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
|
|
view.draw(new Canvas(bitmap));
|
|
try { return commit(bitmap, dirtyArea); } finally { bitmap.recycle(); }
|
|
}
|
|
public int clear(int color) {
|
|
EpdUtil.EBCCommitRequest request = new EpdUtil.EBCCommitRequest();
|
|
request.mCommitType = EpdUtil.EBCCommitRequest.COMMIT_CLEAR_COLOR;
|
|
request.mCommitColor = color;
|
|
return EpdUtil.sCommitColor(buffer, request, new EpdUtil.EBCCommitResult());
|
|
}
|
|
@Override public void close() { EpdUtil.sRecycleEBC(buffer); }
|
|
}
|
|
`;
|
|
|
|
const symbolMap = `# Resolved symbol map
|
|
|
|
The compatibility class keeps its original binary names because the JNI exports depend on them. New callers use the renamed facade.
|
|
|
|
| Original symbol | Resolved name |
|
|
| --- | --- |
|
|
| EpdUtil.sInitEBC | XiaomiEinkRenderer constructor |
|
|
| EpdUtil.sRecycleEBC | XiaomiEinkRenderer.close |
|
|
| EpdUtil.sCommitImage | XiaomiEinkRenderer.commit |
|
|
| EpdUtil.sCommitColor | XiaomiEinkRenderer.clear |
|
|
| EpdUtil.sSetFastBgRefresh | XiaomiEinkRenderer.setFastBackgroundRefresh |
|
|
| EpdUtil.sSetFastHandWrite | XiaomiEinkRenderer.setFastHandwriting |
|
|
| EBCData | Native E Ink buffer handle |
|
|
| EBCCommitRequest | Image commit request |
|
|
| EBCCommitResult | Image commit result |
|
|
`;
|
|
|
|
const extractedReadme = `# Xiaomi Mi Reader E Ink compatibility library
|
|
|
|
This artifact is reconstructed from the supplied MoanNote APK. Copy the contents of \`android/src/main\` into an Android app module. The original JNI binding package is retained for binary compatibility. Application code should import \`rip.crit.ms.eink.XiaomiEinkRenderer\`.
|
|
|
|
The native library is device-specific and should only be exercised on the intended Xiaomi Mi Reader firmware.
|
|
`;
|
|
|
|
main();
|