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