265 lines
7.4 KiB
JavaScript
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;
|