Files
jrshikoku/plugins/with-display-metrics-sync.js

265 lines
7.4 KiB
JavaScript

const { withMainActivity } = require("@expo/config-plugins");
const MARKER = "syncDisplayMetricsForCurrentDisplay";
/**
* Keep React Native's process-wide display metrics aligned with the Activity's
* current display. Samsung DeX/freeform windows can use a different density
* from the phone display that initialized React Native.
*
* On a single-display device the Activity and process densities are the same,
* so this is a no-op for normal Android devices.
*/
const withDisplayMetricsSync = (config) =>
withMainActivity(config, (config) => {
const { language } = config.modResults;
if (config.modResults.contents.includes(MARKER)) {
return config;
}
if (language === "kt") {
config.modResults.contents = patchKotlinActivity(
config.modResults.contents,
);
return config;
}
if (language === "java") {
config.modResults.contents = patchJavaActivity(
config.modResults.contents,
);
return config;
}
throw new Error(
"[withDisplayMetricsSync] Unsupported MainActivity language: " + language,
);
});
function patchKotlinActivity(contents) {
let patched = addImport(contents, "import android.os.Build");
patched = addImport(patched, "import android.view.Display");
patched = addImport(
patched,
"import android.content.res.Configuration",
);
patched = addImport(
patched,
"import com.facebook.react.uimanager.DisplayMetricsHolder",
);
const onCreateCall = "super.onCreate(null)";
if (!patched.includes(onCreateCall)) {
throw new Error(
"[withDisplayMetricsSync] MainActivity.kt does not contain the expected onCreate call",
);
}
patched = patched.replace(
onCreateCall,
[
MARKER + "()",
onCreateCall,
MARKER + "()",
" scheduleDisplayMetricsSync()",
].join("\n "),
);
const method = [
"",
" /**",
" * Re-read density from the display currently hosting this Activity.",
" * DeX can use a different density from the phone's main display.",
" */",
" private fun " + MARKER + "() {",
" try {",
" val currentDisplay = getCurrentDisplay()",
" val displayContext = currentDisplay?.let { createDisplayContext(it) } ?: this",
" DisplayMetricsHolder.initDisplayMetrics(displayContext)",
" } catch (_: Throwable) {",
" // The display can be temporarily unavailable during Activity startup.",
" }",
" }",
"",
" private fun getCurrentDisplay(): Display? {",
" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {",
" display?.let { return it }",
" }",
" return window.decorView.display",
" }",
"",
" private fun scheduleDisplayMetricsSync() {",
" window.decorView.post {",
" " + MARKER + "()",
" window.decorView.requestLayout()",
" }",
" }",
"",
" override fun onStart() {",
" " + MARKER + "()",
" super.onStart()",
" }",
"",
" override fun onResume() {",
" " + MARKER + "()",
" super.onResume()",
" }",
"",
" override fun onConfigurationChanged(newConfig: Configuration) {",
" " + MARKER + "()",
" super.onConfigurationChanged(newConfig)",
" scheduleDisplayMetricsSync()",
" }",
"",
" override fun onWindowFocusChanged(hasFocus: Boolean) {",
" if (hasFocus) {",
" " + MARKER + "()",
" }",
" super.onWindowFocusChanged(hasFocus)",
" if (hasFocus) {",
" scheduleDisplayMetricsSync()",
" }",
" }",
"",
].join("\n");
return insertBeforeClassClosingBrace(patched, method);
}
function patchJavaActivity(contents) {
let patched = addImport(contents, "import android.os.Build;");
patched = addImport(patched, "import android.view.Display;");
patched = addImport(
patched,
"import android.content.res.Configuration;",
);
patched = addImport(patched, "import android.content.Context;");
patched = addImport(
patched,
"import com.facebook.react.uimanager.DisplayMetricsHolder;",
);
const onCreateCall = "super.onCreate(null);";
if (!patched.includes(onCreateCall)) {
throw new Error(
"[withDisplayMetricsSync] MainActivity.java does not contain the expected onCreate call",
);
}
patched = patched.replace(
onCreateCall,
[
MARKER + "();",
onCreateCall,
MARKER + "();",
" scheduleDisplayMetricsSync();",
].join("\n "),
);
const method = [
"",
" /**",
" * Re-read density from the display currently hosting this Activity.",
" * DeX can use a different density from the phone's main display.",
" */",
" private void " + MARKER + "() {",
" try {",
" Display currentDisplay = getCurrentDisplay();",
" Context displayContext = currentDisplay != null",
" ? createDisplayContext(currentDisplay)",
" : this;",
" DisplayMetricsHolder.initDisplayMetrics(displayContext);",
" } catch (Throwable ignored) {",
" // The display can be temporarily unavailable during Activity startup.",
" }",
" }",
"",
" private Display getCurrentDisplay() {",
" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {",
" Display activityDisplay = getDisplay();",
" if (activityDisplay != null) {",
" return activityDisplay;",
" }",
" }",
" return getWindow().getDecorView().getDisplay();",
" }",
"",
" private void scheduleDisplayMetricsSync() {",
" getWindow().getDecorView().post(() -> {",
" " + MARKER + "();",
" getWindow().getDecorView().requestLayout();",
" });",
" }",
"",
" @Override",
" protected void onStart() {",
" " + MARKER + "();",
" super.onStart();",
" }",
"",
" @Override",
" protected void onResume() {",
" " + MARKER + "();",
" super.onResume();",
" }",
"",
" @Override",
" public void onConfigurationChanged(Configuration newConfig) {",
" " + MARKER + "();",
" super.onConfigurationChanged(newConfig);",
" scheduleDisplayMetricsSync();",
" }",
"",
" @Override",
" public void onWindowFocusChanged(boolean hasFocus) {",
" if (hasFocus) {",
" " + MARKER + "();",
" }",
" super.onWindowFocusChanged(hasFocus);",
" if (hasFocus) {",
" scheduleDisplayMetricsSync();",
" }",
" }",
"",
].join("\n");
return insertBeforeClassClosingBrace(patched, method);
}
function addImport(contents, importLine) {
if (contents.includes(importLine)) {
return contents;
}
const packageStart = contents.indexOf("package ");
const packageEnd = contents.indexOf("\n", packageStart);
if (packageStart < 0 || packageEnd < 0) {
throw new Error(
"[withDisplayMetricsSync] Could not find package declaration while adding " +
importLine,
);
}
return (
contents.slice(0, packageEnd + 1) +
"\n" +
importLine +
"\n" +
contents.slice(packageEnd + 1)
);
}
function insertBeforeClassClosingBrace(contents, block) {
const closingBrace = contents.lastIndexOf("\n}");
if (closingBrace < 0) {
throw new Error(
"[withDisplayMetricsSync] Could not find MainActivity class closing brace",
);
}
return contents.slice(0, closingBrace) + block + contents.slice(closingBrace);
}
module.exports = withDisplayMetricsSync;