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();