feat(exif): write the Xiaomi watermark blob (0x889e) so HyperOS Gallery can frame our captures

HyperOS Gallery reads EXIF 0x889e (a JSON blob) to build its watermark
frame; without it a photo fails with "cannot recognize the parameters".
CameraX/HAL never supplies that vendor tag, so a capture from this app
lacked it while a stock-camera photo carried it.

Read back the same props the stock camera derives the blob from
(ro.product.device / ro.product.marketname / Build.MANUFACTURER) via the
native module, synthesize the blob in exifWrite, and write it only when
the source has none and the device is Xiaomi/Redmi/POCO - a source blob
is never overwritten.
This commit is contained in:
2026-09-11 18:47:01 +07:00
parent 33123d47fd
commit 17ee01b62a
6 changed files with 163 additions and 3 deletions
@@ -58,6 +58,17 @@ private class FramedResult(val bmp: Bitmap, val window: RectF?)
// Font temp files are tiny and reused across exports — cache the Typefaces.
private val typefaceCache = HashMap<String, Typeface>()
// android.os.SystemProperties is hidden API: reflection keeps this compiling
// without stubs and degrades to null if a future SDK moves it.
private fun systemProp(key: String): String? = try {
val get = Class.forName("android.os.SystemProperties")
.getMethod("get", String::class.java, String::class.java)
(get.invoke(null, key, "") as String).ifEmpty { null }
} catch (e: Throwable) {
Log.w(TAG, "systemProp($key) unavailable: ${e.message}")
null
}
class RecipescamExportModule : Module() {
override fun definition() = ModuleDefinition {
Name("RecipesCamExport")
@@ -104,6 +115,22 @@ class RecipescamExportModule : Module() {
"pong"
}
// Raw device identity the stock Xiaomi camera folds into EXIF 0x889e —
// the watermark blob HyperOS Gallery reads before it will open its
// watermark editor. CameraX/HAL never writes that tag, so Gallery reports
// it cannot recognize the parameters on our captures. JS formats the JSON
// (see exifWrite.watermarkBlob); native only reads the props.
Function("deviceInfo") { ->
mapOf(
"device" to (systemProp("ro.product.device") ?: ""),
"marketName" to (systemProp("ro.product.marketname")
?: systemProp("ro.product.odm.marketname")
?: systemProp("ro.product.vendor.marketname") ?: ""),
"manufacturer" to (android.os.Build.MANUFACTURER ?: ""),
"model" to (android.os.Build.MODEL ?: ""),
)
}
// P0 spike harness: copy a bundled drawable asset (e.g. "wallframe", the
// 3117x4000 artwork — representative 12MP decode) into cacheDir and hand the
// real file path back to JS, which then runs decodeEncodeAsync on it. This
+7
View File
@@ -91,6 +91,13 @@ export interface RecipescamExportModule {
processPhotoAsync(srcPath: string, dstPath: string, matrix: number[], crop: CropRect | null, adjust?: PhotoAdjust): Promise<ColorTimings>;
/** P1 alias: matrix + crop, no tone/cinema/enhance passes. */
processColorAsync(srcPath: string, dstPath: string, matrix: number[], crop: CropRect | null, quality: number): Promise<ColorTimings>;
/**
* Raw device identity the stock Xiaomi camera folds into EXIF 0x889e (the
* watermark blob HyperOS Gallery reads) but CameraX/HAL never supplies:
* codename, marketing name, manufacturer, model. Empty strings when absent.
*/
deviceInfo(): { device: string; marketName: string; manufacturer: string; model: string };
/** P0 probe helpers — removed before ship. */
ping(): Promise<string>;
materializeBenchAsset(assetName: string): Promise<string>;