capacitor-plugin-status-bar 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,691 @@
1
+ package com.cap.plugins.statusbar;
2
+
3
+ import android.app.Activity;
4
+ import android.graphics.Color;
5
+ import android.os.Build;
6
+ import android.util.Log;
7
+ import android.view.View;
8
+ import android.view.ViewGroup;
9
+ import android.view.Window;
10
+ import android.view.WindowInsets;
11
+ import android.view.WindowInsetsController;
12
+ import android.widget.FrameLayout;
13
+ import android.view.Gravity;
14
+
15
+ import androidx.annotation.ColorInt;
16
+ import androidx.annotation.Nullable;
17
+ import androidx.core.graphics.ColorUtils;
18
+ import androidx.core.view.ViewCompat;
19
+ import androidx.core.view.WindowCompat;
20
+ import androidx.core.view.WindowInsetsCompat;
21
+
22
+ import com.getcapacitor.Plugin;
23
+
24
+ import java.util.Objects;
25
+
26
+ /**
27
+ * Android Status Bar utilities with Android 10-15+ (API 29-35+) support.
28
+ * Supports:
29
+ * - API 29 (Android 10): Uses deprecated SYSTEM_UI_FLAG for backward
30
+ * compatibility
31
+ * - API 30+ (Android 11+): Uses modern WindowInsetsController API
32
+ * - API 35+ (Android 15+): Fully compatible with edge-to-edge display
33
+ * enforcement
34
+ */
35
+ public class StatusBar extends Plugin {
36
+ private static final String TAG = "StatusBar";
37
+ private static final String STATUS_BAR_OVERLAY_TAG = "capacitor_status_bar_overlay";
38
+ private static final String NAV_BAR_OVERLAY_TAG = "capacitor_navigation_bar_overlay";
39
+
40
+ // Store current state to preserve colors when hiding/showing
41
+ private String currentStyle = "LIGHT";
42
+ private String currentColorHex = null;
43
+ private int currentStatusBarColor = Color.BLACK;
44
+ private int currentNavBarColor = Color.BLACK;
45
+
46
+ @Override
47
+ public void load() {
48
+ super.load();
49
+ setupEdgeToEdgeBehavior();
50
+ }
51
+
52
+ private void setupEdgeToEdgeBehavior() {
53
+ Activity activity = getActivity();
54
+ if (activity == null)
55
+ return;
56
+
57
+ Window window = activity.getWindow();
58
+ View decorView = window.getDecorView();
59
+
60
+ WindowCompat.setDecorFitsSystemWindows(window, false);
61
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 14+
62
+ ViewCompat.setOnApplyWindowInsetsListener(decorView, (v, insets) -> {
63
+ ViewCompat.onApplyWindowInsets(v, insets);
64
+ return insets;
65
+ });
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Ensures edge-to-edge is properly configured for Android 15+.
71
+ * This fixes the keyboard extra space issue by properly handling IME insets
72
+ * using the modern WindowInsets API instead of deprecated soft input modes.
73
+ *
74
+ * @param activity The activity to configure
75
+ */
76
+ public void ensureEdgeToEdgeConfigured(Activity activity) {
77
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { // Android 15 (API 35)
78
+ Window window = activity.getWindow();
79
+ View decorView = window.getDecorView();
80
+
81
+ // Enable edge-to-edge mode for Android 15+
82
+ WindowCompat.setDecorFitsSystemWindows(window, false);
83
+
84
+ ViewCompat.setOnApplyWindowInsetsListener(decorView, (v, insets) -> {
85
+ androidx.core.graphics.Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());
86
+ androidx.core.graphics.Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars());
87
+
88
+ boolean isKeyboardVisible = imeInsets.bottom > 0;
89
+ Log.d(TAG, "ensureEdgeToEdgeConfigured: IME visible=" + isKeyboardVisible
90
+ + ", IME bottom=" + imeInsets.bottom
91
+ + ", system bars bottom=" + systemBarsInsets.bottom);
92
+
93
+ ViewCompat.onApplyWindowInsets(v, insets);
94
+ return insets;
95
+ });
96
+
97
+ Log.d(TAG,
98
+ "ensureEdgeToEdgeConfigured: Edge-to-edge enabled with WindowInsets API for Android 15+ (API 35+)");
99
+ } else {
100
+ Log.d(TAG, "ensureEdgeToEdgeConfigured: Android < 15, no action needed");
101
+ }
102
+ }
103
+
104
+ public void setOverlaysWebView(Activity activity, boolean overlay) {
105
+ // No-op on Android. Exposed for API parity with iOS.
106
+ Log.d(TAG, "setOverlaysWebView: no-op on Android, overlay=" + overlay);
107
+ }
108
+
109
+ public void showStatusBar(Activity activity, boolean animated) {
110
+ Log.d(TAG, "showStatusBar: animated=" + animated + ", currentStyle=" + currentStyle + ", API="
111
+ + Build.VERSION.SDK_INT);
112
+ Window window = activity.getWindow();
113
+ View decorView = window.getDecorView();
114
+
115
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
116
+ // API 30+ (Android 11+) - Use WindowInsetsController
117
+ WindowInsetsController controller = window.getInsetsController();
118
+ if (controller != null) {
119
+ Log.d(TAG, "showStatusBar: showing system bars (API 30+)");
120
+ // Show both status and navigation bars together
121
+ controller.show(WindowInsets.Type.systemBars());
122
+ // Set behavior for transient bars (user can swipe to reveal)
123
+ controller.setSystemBarsBehavior(
124
+ WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
125
+ } else {
126
+ Log.w(TAG, "showStatusBar: WindowInsetsController is null");
127
+ }
128
+ } else {
129
+ // API 29 (Android 10) - Use system UI visibility flags (deprecated but
130
+ // necessary)
131
+ Log.d(TAG, "showStatusBar: showing using system UI flags (API 29)");
132
+ // Set to visible state - clear all immersive flags
133
+ decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
134
+ }
135
+
136
+ // Reapply the stored colors and style instead of removing them
137
+ reapplyCurrentStyle(activity);
138
+
139
+ // Restore the overlay backgrounds to their original colors
140
+ restoreStatusBarBackground(activity);
141
+ }
142
+
143
+ public void hideStatusBar(Activity activity, String animation) {
144
+ Log.d(TAG, "hideStatusBar: animation=" + animation + ", API=" + Build.VERSION.SDK_INT);
145
+ Window window = activity.getWindow();
146
+ View decorView = window.getDecorView();
147
+
148
+ String animationType = animation != null ? animation.toLowerCase() : "slide";
149
+
150
+ if ("fade".equals(animationType)) {
151
+ // Fade mode: Make background transparent without removing status bar and
152
+ // navigation bar
153
+ Log.d(TAG, "hideStatusBar: fade mode - making backgrounds transparent");
154
+ makeStatusBarBackgroundTransparent(activity);
155
+ } else if ("slide".equals(animationType)) {
156
+ // Slide mode: Hide status bar and navigation bar completely (current behavior)
157
+ Log.d(TAG, "hideStatusBar: slide mode - hiding bars completely");
158
+
159
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
160
+ // API 30+ (Android 11+) - Use WindowInsetsController
161
+ WindowInsetsController controller = window.getInsetsController();
162
+ if (controller != null) {
163
+ Log.d(TAG, "hideStatusBar: hiding system bars (API 30+)");
164
+ // Hide both status and navigation bars together
165
+ controller.hide(WindowInsets.Type.systemBars());
166
+ // Set behavior for immersive mode (user can swipe to reveal temporarily)
167
+ controller.setSystemBarsBehavior(
168
+ WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
169
+ } else {
170
+ Log.w(TAG, "hideStatusBar: WindowInsetsController is null");
171
+ }
172
+ } else {
173
+ // API 29 (Android 10) - Use system UI visibility flags (deprecated but
174
+ // necessary)
175
+ Log.d(TAG, "hideStatusBar: hiding using system UI flags (API 29)");
176
+ // Use immersive sticky mode with proper layout flags for Android 10
177
+ decorView.setSystemUiVisibility(
178
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
179
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
180
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
181
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
182
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
183
+ | View.SYSTEM_UI_FLAG_FULLSCREEN);
184
+ }
185
+
186
+ // Make the overlay backgrounds transparent so content shows through
187
+ makeStatusBarBackgroundTransparent(activity);
188
+ } else {
189
+ // Unknown animation type, default to slide
190
+ Log.w(TAG, "hideStatusBar: unknown animation type '" + animationType + "', defaulting to slide");
191
+ hideStatusBar(activity, "slide");
192
+ }
193
+ }
194
+
195
+ public void setStyle(Activity activity, String style, @Nullable String colorHex) {
196
+ Log.d(TAG, "setStyle: style=" + style + ", colorHex=" + colorHex);
197
+ Window window = activity.getWindow();
198
+
199
+ // Enable drawing of system bar backgrounds (required for color changes)
200
+ window.addFlags(android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
201
+ window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
202
+ window.clearFlags(android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
203
+
204
+ // Store the current style and color for later reapplication
205
+ currentStyle = style;
206
+ currentColorHex = colorHex;
207
+
208
+ // Set icon appearance (light/dark) regardless of background approach
209
+ boolean lightBackground;
210
+ if ("LIGHT".equalsIgnoreCase(style)) {
211
+ // Light background -> dark icons
212
+ setLightStatusBarIcons(window, true);
213
+ lightBackground = true;
214
+ } else if ("DARK".equalsIgnoreCase(style)) {
215
+ // Dark background -> light icons
216
+ setLightStatusBarIcons(window, false);
217
+ lightBackground = false;
218
+ } else if ("CUSTOM".equalsIgnoreCase(style)) {
219
+ // CUSTOM: Derive icon color from provided custom color
220
+ int parsed = parseColorOrDefault(colorHex, Color.BLACK);
221
+ boolean isLight = isEffectiveLightColor(parsed);
222
+ // If background is light, request dark icons
223
+ setLightStatusBarIcons(window, isLight);
224
+ lightBackground = isLight;
225
+ } else {
226
+ // Default: Auto-detect based on system theme (follow device theme)
227
+ boolean isSystemDarkMode = isSystemInDarkMode(activity);
228
+ setLightStatusBarIcons(window, !isSystemDarkMode);
229
+ lightBackground = !isSystemDarkMode;
230
+ }
231
+
232
+ if ("CUSTOM".equalsIgnoreCase(style) && colorHex != null) {
233
+ int color = parseColorOrDefault(colorHex, lightBackground ? Color.WHITE : Color.BLACK);
234
+ currentStatusBarColor = color;
235
+ currentNavBarColor = color;
236
+ applyStatusBarBackground(activity, color);
237
+ applyNavigationBarBackground(activity, color);
238
+ } else if ("LIGHT".equalsIgnoreCase(style)) {
239
+ currentStatusBarColor = Color.WHITE;
240
+ currentNavBarColor = Color.WHITE;
241
+ applyStatusBarBackground(activity, Color.WHITE);
242
+ applyNavigationBarBackground(activity, Color.WHITE);
243
+ } else if ("DARK".equalsIgnoreCase(style)) {
244
+ currentStatusBarColor = Color.BLACK;
245
+ currentNavBarColor = Color.BLACK;
246
+ applyStatusBarBackground(activity, Color.BLACK);
247
+ applyNavigationBarBackground(activity, Color.BLACK);
248
+ } else {
249
+ // Default: Auto-detect based on system theme
250
+ boolean isSystemDarkMode = isSystemInDarkMode(activity);
251
+ int themeColor = isSystemDarkMode ? Color.BLACK : Color.WHITE;
252
+ currentStatusBarColor = themeColor;
253
+ currentNavBarColor = themeColor;
254
+ applyStatusBarBackground(activity, themeColor);
255
+ applyNavigationBarBackground(activity, themeColor);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Set the window background color.
261
+ *
262
+ * @param activity The activity to apply the background color to
263
+ * @param colorHex The hex color string (e.g., "#FFFFFF" or "#FF5733")
264
+ */
265
+ public void setBackground(Activity activity, @Nullable String colorHex) {
266
+ Log.d(TAG, "setBackground: colorHex=" + colorHex);
267
+
268
+ if (colorHex == null) {
269
+ Log.w(TAG, "setBackground: colorHex is null");
270
+ return;
271
+ }
272
+
273
+ int color = parseColorOrDefault(colorHex, Color.WHITE);
274
+ applyWindowBackground(activity, color);
275
+ }
276
+
277
+ /**
278
+ * Get the safe area insets.
279
+ * Returns the insets for status bar, navigation bar, and notch areas.
280
+ *
281
+ * @param activity The activity to get the insets from
282
+ * @return A map containing top, bottom, left, and right inset values in pixels
283
+ */
284
+ public java.util.Map<String, Integer> getSafeAreaInsets(Activity activity) {
285
+ Log.d(TAG, "getSafeAreaInsets");
286
+ java.util.Map<String, Integer> insets = new java.util.HashMap<>();
287
+
288
+ Window window = activity.getWindow();
289
+ View decorView = window.getDecorView();
290
+
291
+ WindowInsets windowInsets = decorView.getRootWindowInsets();
292
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
293
+ // API 30+ (Android 11+) - Use WindowInsets API
294
+ if (windowInsets != null) {
295
+ android.graphics.Insets systemBarsInsets = windowInsets.getInsets(WindowInsets.Type.systemBars());
296
+ android.graphics.Insets displayCutoutInsets = windowInsets.getInsets(WindowInsets.Type.displayCutout());
297
+
298
+ // Combine system bars and display cutout insets (use maximum of both)
299
+ insets.put("top", Math.max(systemBarsInsets.top, displayCutoutInsets.top));
300
+ insets.put("bottom", Math.max(systemBarsInsets.bottom, displayCutoutInsets.bottom));
301
+ insets.put("left", Math.max(systemBarsInsets.left, displayCutoutInsets.left));
302
+ insets.put("right", Math.max(systemBarsInsets.right, displayCutoutInsets.right));
303
+
304
+ Log.d(TAG, "getSafeAreaInsets (API 30+): top=" + insets.get("top")
305
+ + ", bottom=" + insets.get("bottom")
306
+ + ", left=" + insets.get("left")
307
+ + ", right=" + insets.get("right"));
308
+ } else {
309
+ // Fallback to zero insets
310
+ insets.put("top", 0);
311
+ insets.put("bottom", 0);
312
+ insets.put("left", 0);
313
+ insets.put("right", 0);
314
+ Log.w(TAG, "getSafeAreaInsets: windowInsets is null");
315
+ }
316
+ } else {
317
+ // API 29 (Android 10) - Use deprecated system window insets
318
+ if (windowInsets != null) {
319
+ insets.put("top", windowInsets.getSystemWindowInsetTop());
320
+ insets.put("bottom", windowInsets.getSystemWindowInsetBottom());
321
+ insets.put("left", windowInsets.getSystemWindowInsetLeft());
322
+ insets.put("right", windowInsets.getSystemWindowInsetRight());
323
+
324
+ Log.d(TAG, "getSafeAreaInsets (API 29): top=" + insets.get("top")
325
+ + ", bottom=" + insets.get("bottom")
326
+ + ", left=" + insets.get("left")
327
+ + ", right=" + insets.get("right"));
328
+ } else {
329
+ // Fallback to zero insets
330
+ insets.put("top", 0);
331
+ insets.put("bottom", 0);
332
+ insets.put("left", 0);
333
+ insets.put("right", 0);
334
+ Log.w(TAG, "getSafeAreaInsets: windowInsets is null");
335
+ }
336
+ }
337
+
338
+ return insets;
339
+ }
340
+
341
+ /**
342
+ * Reapply the current style and colors after showing bars.
343
+ * This ensures colors are preserved when hiding and then showing.
344
+ */
345
+ private void reapplyCurrentStyle(Activity activity) {
346
+ Log.d(TAG, "reapplyCurrentStyle: style=" + currentStyle + ", colorHex=" + currentColorHex);
347
+ Window window = activity.getWindow();
348
+
349
+ // Reapply icon appearance
350
+ if ("LIGHT".equalsIgnoreCase(currentStyle)) {
351
+ setLightStatusBarIcons(window, true);
352
+ } else if ("DARK".equalsIgnoreCase(currentStyle)) {
353
+ setLightStatusBarIcons(window, false);
354
+ } else if ("CUSTOM".equalsIgnoreCase(currentStyle)) {
355
+ int parsed = parseColorOrDefault(currentColorHex, Color.BLACK);
356
+ boolean isLight = isEffectiveLightColor(parsed);
357
+ setLightStatusBarIcons(window, isLight);
358
+ } else {
359
+ // Default: Auto-detect based on system theme
360
+ boolean isSystemDarkMode = isSystemInDarkMode(activity);
361
+ setLightStatusBarIcons(window, !isSystemDarkMode);
362
+ }
363
+
364
+ // Reapply colors
365
+ applyStatusBarBackground(activity, currentStatusBarColor);
366
+ applyNavigationBarBackground(activity, currentNavBarColor);
367
+ }
368
+
369
+ private void setLightStatusBarIcons(Window window, boolean light) {
370
+ Log.d(TAG, "setLightStatusBarIcons: light=" + light + ", API=" + Build.VERSION.SDK_INT);
371
+ View decorView = window.getDecorView();
372
+
373
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
374
+ // API 30+ - Use WindowInsetsController
375
+ WindowInsetsController controller = window.getInsetsController();
376
+ if (controller == null) {
377
+ Log.w(TAG, "setLightStatusBarIcons: WindowInsetsController is null");
378
+ return;
379
+ }
380
+ int mask = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
381
+ | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
382
+ controller.setSystemBarsAppearance(light ? mask : 0, mask);
383
+ Log.d(TAG, "setLightStatusBarIcons: applied using WindowInsetsController (API 30+)");
384
+ } else {
385
+ int flags = decorView.getSystemUiVisibility();
386
+ if (light) {
387
+ // Light background -> dark icons
388
+ flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
389
+ flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
390
+ Log.d(TAG, "setLightStatusBarIcons: set light icons (dark text) (API 29)");
391
+ } else {
392
+ // Dark background -> light icons
393
+ flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
394
+ flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
395
+ Log.d(TAG, "setLightStatusBarIcons: set dark icons (light text) (API 29)");
396
+ }
397
+ decorView.setSystemUiVisibility(flags);
398
+ }
399
+ }
400
+
401
+ private void applyStatusBarBackground(Activity activity, @ColorInt int color) {
402
+ Log.d(TAG, "applyStatusBarBackground: color=#" + Integer.toHexString(color) + ", API=" + Build.VERSION.SDK_INT);
403
+ Window window = activity.getWindow();
404
+ if (Build.VERSION.SDK_INT >= 35) {
405
+ ensureStatusBarOverlay(activity, color);
406
+ } else {
407
+ removeStatusBarOverlayIfPresent(activity);
408
+ window.setStatusBarColor(color);
409
+ }
410
+ }
411
+
412
+ private void applyNavigationBarBackground(Activity activity, @ColorInt int color) {
413
+ Log.d(TAG, "applyNavigationBarBackground: color=#" + Integer.toHexString(color) + ", API="
414
+ + Build.VERSION.SDK_INT);
415
+ Window window = activity.getWindow();
416
+ if (Build.VERSION.SDK_INT >= 35) {
417
+ ensureNavBarOverlay(activity, color);
418
+ } else {
419
+ removeNavBarOverlayIfPresent(activity);
420
+ window.setNavigationBarColor(color);
421
+ }
422
+ }
423
+
424
+ private void ensureStatusBarOverlay(Activity activity, @ColorInt int color) {
425
+ Log.d(TAG, "ensureStatusBarOverlay: color=#" + Integer.toHexString(color));
426
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
427
+ View existing = decorView.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
428
+ if (existing == null) {
429
+ Log.d(TAG, "ensureStatusBarOverlay: creating new overlay");
430
+ View overlay = new View(activity);
431
+ overlay.setTag(STATUS_BAR_OVERLAY_TAG);
432
+ overlay.setBackgroundColor(color);
433
+
434
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
435
+ ViewGroup.LayoutParams.MATCH_PARENT,
436
+ 0);
437
+ lp.topMargin = 0;
438
+ overlay.setLayoutParams(lp);
439
+
440
+ // Add to the top of the decor view
441
+ decorView.addView(overlay);
442
+
443
+ // Apply correct height from insets
444
+ ViewCompat.setOnApplyWindowInsetsListener(overlay, (v, windowInsets) -> {
445
+ int top;
446
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
447
+ top = Objects.requireNonNull(windowInsets.toWindowInsets())
448
+ .getInsets(WindowInsets.Type.statusBars()).top;
449
+ } else {
450
+ top = Objects.requireNonNull(windowInsets.toWindowInsets()).getSystemWindowInsetTop();
451
+ }
452
+ ViewGroup.LayoutParams params = v.getLayoutParams();
453
+ params.height = top;
454
+ v.setLayoutParams(params);
455
+ // Don't set color here - it's set before listener and should not be overridden
456
+ return windowInsets;
457
+ });
458
+ overlay.requestApplyInsets();
459
+ } else {
460
+ Log.d(TAG, "ensureStatusBarOverlay: updating existing overlay");
461
+ existing.setBackgroundColor(color);
462
+ existing.requestApplyInsets();
463
+ }
464
+ }
465
+
466
+ private void removeStatusBarOverlayIfPresent(Activity activity) {
467
+ Log.d(TAG, "removeStatusBarOverlayIfPresent");
468
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
469
+ View existing = decorView.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
470
+ if (existing != null) {
471
+ Log.d(TAG, "removeStatusBarOverlayIfPresent: removing overlay");
472
+ decorView.removeView(existing);
473
+ }
474
+ }
475
+
476
+ private void ensureNavBarOverlay(Activity activity, @ColorInt int color) {
477
+ Log.d(TAG, "ensureNavBarOverlay: color=#" + Integer.toHexString(color));
478
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
479
+ View existing = decorView.findViewWithTag(NAV_BAR_OVERLAY_TAG);
480
+ if (existing == null) {
481
+ Log.d(TAG, "ensureNavBarOverlay: creating new overlay");
482
+ View overlay = new View(activity);
483
+ overlay.setTag(NAV_BAR_OVERLAY_TAG);
484
+ overlay.setBackgroundColor(color);
485
+
486
+ FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
487
+ ViewGroup.LayoutParams.MATCH_PARENT,
488
+ 0);
489
+ lp.gravity = Gravity.BOTTOM;
490
+ overlay.setLayoutParams(lp);
491
+
492
+ decorView.addView(overlay);
493
+
494
+ ViewCompat.setOnApplyWindowInsetsListener(overlay, (v, windowInsets) -> {
495
+ int bottom;
496
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
497
+ bottom = Objects.requireNonNull(windowInsets.toWindowInsets())
498
+ .getInsets(WindowInsets.Type.navigationBars()).bottom;
499
+ } else {
500
+ bottom = Objects.requireNonNull(windowInsets.toWindowInsets()).getSystemWindowInsetBottom();
501
+ }
502
+ ViewGroup.LayoutParams params = v.getLayoutParams();
503
+ params.height = bottom;
504
+ v.setLayoutParams(params);
505
+ // Don't set color here - it's set before listener and should not be overridden
506
+ return windowInsets;
507
+ });
508
+ overlay.requestApplyInsets();
509
+ } else {
510
+ Log.d(TAG, "ensureNavBarOverlay: updating existing overlay");
511
+ existing.setBackgroundColor(color);
512
+ existing.requestApplyInsets();
513
+ }
514
+ }
515
+
516
+ private void removeNavBarOverlayIfPresent(Activity activity) {
517
+ Log.d(TAG, "removeNavBarOverlayIfPresent");
518
+ ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
519
+ View existing = decorView.findViewWithTag(NAV_BAR_OVERLAY_TAG);
520
+ if (existing != null) {
521
+ Log.d(TAG, "removeNavBarOverlayIfPresent: removing overlay");
522
+ decorView.removeView(existing);
523
+ }
524
+ }
525
+
526
+ private void applyWindowBackground(Activity activity, @ColorInt int color) {
527
+ Log.d(TAG, "applyWindowBackground: color=#" + Integer.toHexString(color));
528
+ View decorView = activity.getWindow().getDecorView();
529
+ decorView.setBackgroundColor(color);
530
+ }
531
+
532
+ /**
533
+ * Makes the status bar and navigation bar backgrounds transparent.
534
+ * This allows content to show through when the bars are hidden.
535
+ */
536
+ private void makeStatusBarBackgroundTransparent(Activity activity) {
537
+ Log.d(TAG, "makeStatusBarBackgroundTransparent: API=" + Build.VERSION.SDK_INT);
538
+ Window window = activity.getWindow();
539
+
540
+ if (Build.VERSION.SDK_INT >= 35) {
541
+ // API 35+ (Android 15+) - Make overlay views transparent
542
+ ViewGroup decorView = (ViewGroup) window.getDecorView();
543
+ View statusBarOverlay = decorView.findViewWithTag(STATUS_BAR_OVERLAY_TAG);
544
+ View navBarOverlay = decorView.findViewWithTag(NAV_BAR_OVERLAY_TAG);
545
+
546
+ if (statusBarOverlay != null) {
547
+ statusBarOverlay.setBackgroundColor(Color.TRANSPARENT);
548
+ Log.d(TAG, "makeStatusBarBackgroundTransparent: status bar overlay made transparent");
549
+ }
550
+
551
+ if (navBarOverlay != null) {
552
+ navBarOverlay.setBackgroundColor(Color.TRANSPARENT);
553
+ Log.d(TAG, "makeStatusBarBackgroundTransparent: navigation bar overlay made transparent");
554
+ }
555
+ } else {
556
+ // API 29-34 - Make window bars transparent
557
+ window.setStatusBarColor(Color.TRANSPARENT);
558
+ window.setNavigationBarColor(Color.TRANSPARENT);
559
+ Log.d(TAG, "makeStatusBarBackgroundTransparent: window bars made transparent");
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Restores the status bar and navigation bar backgrounds to their stored
565
+ * colors.
566
+ * Called when showing the bars after they were hidden.
567
+ */
568
+ private void restoreStatusBarBackground(Activity activity) {
569
+ Log.d(TAG, "restoreStatusBarBackground: API=" + Build.VERSION.SDK_INT
570
+ + ", currentStatusBarColor=#" + Integer.toHexString(currentStatusBarColor)
571
+ + ", currentNavBarColor=#" + Integer.toHexString(currentNavBarColor));
572
+
573
+ // Restore all backgrounds to their stored colors
574
+ applyStatusBarBackground(activity, currentStatusBarColor);
575
+ applyNavigationBarBackground(activity, currentNavBarColor);
576
+
577
+ Log.d(TAG, "restoreStatusBarBackground: backgrounds restored");
578
+ }
579
+
580
+ @ColorInt
581
+ private int parseColorOrDefault(@Nullable String color, @ColorInt int def) {
582
+ if (color == null) {
583
+ Log.d(TAG, "parseColorOrDefault: color is null, using default");
584
+ return def;
585
+ }
586
+ try {
587
+ int parsed = parseHexColor(color);
588
+ Log.d(TAG, "parseColorOrDefault: parsed color=" + color + " -> #" + Integer.toHexString(parsed));
589
+ return parsed;
590
+ } catch (IllegalArgumentException ex) {
591
+ Log.w(TAG, "parseColorOrDefault: invalid color=" + color + ", using default");
592
+ return def;
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Parse hex color string similar to iOS implementation.
598
+ * Handles both 6-digit (#RRGGBB) and 8-digit (#RRGGBBAA) formats.
599
+ *
600
+ * @param hex The hex color string (with or without # prefix)
601
+ * @return The parsed color as an integer
602
+ * @throws IllegalArgumentException if the color format is invalid
603
+ */
604
+ private int parseHexColor(String hex) throws IllegalArgumentException {
605
+ String hexSanitized = hex.trim().replaceFirst("^#", "");
606
+
607
+ if (hexSanitized.length() != 6 && hexSanitized.length() != 8) {
608
+ throw new IllegalArgumentException("Invalid hex color length: " + hexSanitized.length());
609
+ }
610
+
611
+ try {
612
+ long rgb = Long.parseLong(hexSanitized, 16);
613
+
614
+ if (hexSanitized.length() == 6) {
615
+ // 6-digit format: #RRGGBB (opaque)
616
+ return (int) (0xFF000000L | rgb);
617
+ } else {
618
+ // 8-digit format: #RRGGBBAA
619
+ int r = (int) ((rgb & 0xFF000000L) >> 24);
620
+ int g = (int) ((rgb & 0x00FF0000L) >> 16);
621
+ int b = (int) ((rgb & 0x0000FF00L) >> 8);
622
+ int a = (int) (rgb & 0x000000FFL);
623
+
624
+ return Color.argb(a, r, g, b);
625
+ }
626
+ } catch (NumberFormatException e) {
627
+ throw new IllegalArgumentException("Invalid hex color format: " + hex, e);
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Calculate effective brightness considering alpha channel.
633
+ * For transparent colors, we assume they will be blended over a white
634
+ * background.
635
+ *
636
+ * @param color The color to analyze
637
+ * @return true if the effective color appears light, false if dark
638
+ */
639
+ private boolean isEffectiveLightColor(@ColorInt int color) {
640
+ int alpha = Color.alpha(color);
641
+
642
+ if (alpha == 255) {
643
+ // Fully opaque - use standard luminance calculation
644
+ return ColorUtils.calculateLuminance(color) > 0.5;
645
+ }
646
+
647
+ // For transparent colors, calculate effective color when blended over white
648
+ // background
649
+ float alphaRatio = alpha / 255.0f;
650
+ int r = Color.red(color);
651
+ int g = Color.green(color);
652
+ int b = Color.blue(color);
653
+
654
+ // Blend with white background (255, 255, 255)
655
+ int effectiveR = (int) (r * alphaRatio + 255 * (1 - alphaRatio));
656
+ int effectiveG = (int) (g * alphaRatio + 255 * (1 - alphaRatio));
657
+ int effectiveB = (int) (b * alphaRatio + 255 * (1 - alphaRatio));
658
+
659
+ int effectiveColor = Color.rgb(effectiveR, effectiveG, effectiveB);
660
+ return ColorUtils.calculateLuminance(effectiveColor) > 0.5;
661
+ }
662
+
663
+ /**
664
+ * Apply default status bar style based on system theme.
665
+ * Automatically detects if the device is in light or dark mode and applies the
666
+ * appropriate style.
667
+ *
668
+ * @param activity The activity to apply the style to
669
+ */
670
+ public void applyDefaultStyle(Activity activity) {
671
+ boolean isDarkMode = isSystemInDarkMode(activity);
672
+ String style = isDarkMode ? "DARK" : "LIGHT";
673
+ Log.d(TAG, "applyDefaultStyle: detected system theme=" + (isDarkMode ? "dark" : "light") + ", applying style="
674
+ + style);
675
+ setStyle(activity, style, null);
676
+ }
677
+
678
+ /**
679
+ * Check if the system is currently in dark mode.
680
+ *
681
+ * @param activity The activity to check the configuration from
682
+ * @return true if system is in dark mode, false otherwise
683
+ */
684
+ private boolean isSystemInDarkMode(Activity activity) {
685
+ int nightModeFlags = activity.getResources().getConfiguration().uiMode
686
+ & android.content.res.Configuration.UI_MODE_NIGHT_MASK;
687
+ boolean isDarkMode = nightModeFlags == android.content.res.Configuration.UI_MODE_NIGHT_YES;
688
+ Log.d(TAG, "isSystemInDarkMode: " + isDarkMode);
689
+ return isDarkMode;
690
+ }
691
+ }