@@ -14,66 +14,179 @@ const mainActivityPath = join(
1414 "MainActivity.java" ,
1515) ;
1616
17- const stableImmersiveMainActivity = `package app.mha.controlhub;
17+ const edgeToEdgeMainActivity = `package app.mha.controlhub;
1818
19- import android.os.Build;
19+ import android.content.Intent;
20+ import android.content.res.Configuration;
21+ import android.graphics.Color;
2022import android.os.Bundle;
2123import android.view.View;
2224import android.view.Window;
23- import android.view.WindowInsets;
24- import android.view.WindowInsetsController;
25+ import android.view.WindowManager;
26+ import android.webkit.WebView;
27+
28+ import androidx.core.graphics.Insets;
29+ import androidx.core.view.ViewCompat;
30+ import androidx.core.view.WindowCompat;
31+ import androidx.core.view.WindowInsetsCompat;
32+ import androidx.core.view.WindowInsetsControllerCompat;
2533
2634import com.getcapacitor.BridgeActivity;
2735
2836public class MainActivity extends BridgeActivity {
37+ private Insets lastPublishedInsets = Insets.NONE;
38+
2939 @Override
3040 protected void onCreate(Bundle savedInstanceState) {
41+ applyEdgeToEdgeWindowFlags();
3142 super.onCreate(savedInstanceState);
32- enableStableImmersiveMode();
43+ hideNativeChrome();
44+ applyEdgeToEdgeWindowFlags();
45+ installInsetsListener();
46+ schedulePostResumeEdgeToEdgeSync();
47+ republishSafeAreaInsets(true);
3348 }
3449
3550 @Override
3651 public void onWindowFocusChanged(boolean hasFocus) {
3752 super.onWindowFocusChanged(hasFocus);
3853 if (hasFocus) {
39- enableStableImmersiveMode();
54+ hideNativeChrome();
55+ applyEdgeToEdgeWindowFlags();
56+ schedulePostResumeEdgeToEdgeSync();
57+ republishSafeAreaInsets(true);
58+ }
59+ }
60+
61+ @Override
62+ public void onResume() {
63+ super.onResume();
64+ hideNativeChrome();
65+ applyEdgeToEdgeWindowFlags();
66+ schedulePostResumeEdgeToEdgeSync();
67+ republishSafeAreaInsets(true);
68+ }
69+
70+ @Override
71+ protected void onNewIntent(Intent intent) {
72+ super.onNewIntent(intent);
73+ hideNativeChrome();
74+ applyEdgeToEdgeWindowFlags();
75+ schedulePostResumeEdgeToEdgeSync();
76+ republishSafeAreaInsets(true);
77+ }
78+
79+ private void hideNativeChrome() {
80+ if (getSupportActionBar() != null) {
81+ getSupportActionBar().hide();
4082 }
4183 }
4284
43- private void enableStableImmersiveMode () {
85+ private void applyEdgeToEdgeWindowFlags () {
4486 Window window = getWindow();
87+ WindowCompat.setDecorFitsSystemWindows(window, false);
88+ window.setStatusBarColor(Color.TRANSPARENT);
89+ window.setNavigationBarColor(Color.TRANSPARENT);
90+
91+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
92+ WindowManager.LayoutParams attributes = window.getAttributes();
93+ attributes.layoutInDisplayCutoutMode =
94+ WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
95+ window.setAttributes(attributes);
96+ }
97+
98+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
99+ window.setStatusBarContrastEnforced(false);
100+ window.setNavigationBarContrastEnforced(false);
101+ }
102+
103+ WebView webView = getBridge() != null ? getBridge().getWebView() : null;
104+ if (webView != null) webView.setBackgroundColor(Color.TRANSPARENT);
105+ syncSystemBarAppearance();
106+ }
107+
108+ private void schedulePostResumeEdgeToEdgeSync() {
109+ View decorView = getWindow().getDecorView();
110+ decorView.post(() -> {
111+ applyEdgeToEdgeWindowFlags();
112+ ViewCompat.requestApplyInsets(decorView);
113+ republishSafeAreaInsets(true);
114+ });
115+ }
45116
46- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
47- // Keep Android's navigation / gesture area stable. Hiding the navigation
48- // bars in WebView immersive mode can make Android recalculate the bottom
49- // viewport while the user scrolls near the end of the page, which can
50- // produce visible flashes on glass-heavy layouts.
51- window.setDecorFitsSystemWindows(true);
52- WindowInsetsController controller = window.getInsetsController();
53-
54- if (controller != null) {
55- controller.hide(WindowInsets.Type.statusBars());
56- controller.setSystemBarsBehavior(
57- WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
117+ private void installInsetsListener() {
118+ ViewCompat.setOnApplyWindowInsetsListener(
119+ getWindow().getDecorView(),
120+ (view, windowInsets) -> {
121+ Insets safeInsets = windowInsets.getInsets(
122+ WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()
58123 );
124+ publishSafeAreaInsets(safeInsets, false);
125+ return windowInsets;
59126 }
60- return;
61- }
127+ );
128+ ViewCompat.requestApplyInsets(getWindow().getDecorView());
129+ }
130+
131+ private void syncSystemBarAppearance() {
132+ Window window = getWindow();
133+ WindowInsetsControllerCompat controller = WindowCompat.getInsetsController(
134+ window,
135+ window.getDecorView()
136+ );
137+ if (controller == null) return;
138+
139+ boolean useDarkSystemBarIcons =
140+ (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)
141+ != Configuration.UI_MODE_NIGHT_YES;
142+ controller.setAppearanceLightStatusBars(useDarkSystemBarIcons);
143+ controller.setAppearanceLightNavigationBars(useDarkSystemBarIcons);
144+ }
62145
63- // Legacy Android fallback: hide only the status bar. Do not request
64- // HIDE_NAVIGATION / LAYOUT_HIDE_NAVIGATION here, because that is the part
65- // most likely to destabilize the bottom gesture/navigation area.
66- window.getDecorView().setSystemUiVisibility(
67- View.SYSTEM_UI_FLAG_FULLSCREEN
68- | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
146+ private void republishSafeAreaInsets(boolean force) {
147+ WindowInsetsCompat rootInsets = ViewCompat.getRootWindowInsets(getWindow().getDecorView());
148+ if (rootInsets == null) return;
149+ Insets safeInsets = rootInsets.getInsets(
150+ WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()
69151 );
152+ publishSafeAreaInsets(safeInsets, force);
153+ }
154+
155+ private void publishSafeAreaInsets(Insets insets, boolean force) {
156+ if (insets == null) return;
157+ if (!force && insets.equals(lastPublishedInsets)) return;
158+ lastPublishedInsets = insets;
159+
160+ WebView webView = getBridge() != null ? getBridge().getWebView() : null;
161+ if (webView == null) return;
162+
163+ final int top = insets.top;
164+ final int right = insets.right;
165+ final int bottom = insets.bottom;
166+ final int left = insets.left;
167+ final String script =
168+ "(function(){" +
169+ "const insets={top:" + top + ",right:" + right + ",bottom:" + bottom + ",left:" + left + "};" +
170+ "window.__MHA_ANDROID_EDGE_TO_EDGE__=true;" +
171+ "window.__MHA_ANDROID_INSETS__=insets;" +
172+ "const root=document.documentElement;" +
173+ "root.dataset.mhaAndroidEdgeToEdge='true';" +
174+ "root.style.setProperty('--mha-safe-top',insets.top+'px');" +
175+ "root.style.setProperty('--mha-safe-right',insets.right+'px');" +
176+ "root.style.setProperty('--mha-safe-bottom',insets.bottom+'px');" +
177+ "root.style.setProperty('--mha-safe-left',insets.left+'px');" +
178+ "document.querySelectorAll('mha-widget-hub').forEach((el)=>el.classList.add('mha-android-edge-to-edge'));" +
179+ "window.dispatchEvent(new CustomEvent('mha:android-insets-changed',{detail:insets}));" +
180+ "})();";
181+
182+ webView.post(() -> webView.evaluateJavascript(script, null));
70183 }
71184}
72185` ;
73186
74187mkdirSync ( dirname ( mainActivityPath ) , { recursive : true } ) ;
75- writeFileSync ( mainActivityPath , stableImmersiveMainActivity , "utf8" ) ;
76- console . log ( "Applied Android stable immersive mode to MainActivity.java" ) ;
188+ writeFileSync ( mainActivityPath , edgeToEdgeMainActivity , "utf8" ) ;
189+ console . log ( "Applied Android edge-to-edge MainActivity.java template " ) ;
77190
78191if ( ! existsSync ( mainActivityPath ) ) {
79192 process . exitCode = 1 ;
0 commit comments