diff --git a/src/android/StatusBar.java b/src/android/StatusBar.java index b350fa4f..0e6ced95 100644 --- a/src/android/StatusBar.java +++ b/src/android/StatusBar.java @@ -19,11 +19,18 @@ */ package org.apache.cordova.statusbar; +import android.content.res.Resources; import android.graphics.Color; import android.os.Build; +import android.util.DisplayMetrics; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.Window; +import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowMetrics; import androidx.appcompat.app.AppCompatActivity; import androidx.core.view.WindowCompat; @@ -41,6 +48,7 @@ public class StatusBar extends CordovaPlugin { private static final String TAG = "StatusBar"; + // Action constants private static final String ACTION_HIDE = "hide"; private static final String ACTION_SHOW = "show"; private static final String ACTION_READY = "_ready"; @@ -48,7 +56,11 @@ public class StatusBar extends CordovaPlugin { private static final String ACTION_OVERLAYS_WEB_VIEW = "overlaysWebView"; private static final String ACTION_STYLE_DEFAULT = "styleDefault"; private static final String ACTION_STYLE_LIGHT_CONTENT = "styleLightContent"; + private static final String ACTION_GET_NAVIGATION_BAR_HEIGHT = "getNavigationBarHeight"; + private static final String ACTION_GET_STATUS_BAR_HEIGHT = "getStatusBarHeight"; + private static final String ACTION_GET_TOTAL_SCREEN_HEIGHT = "getTotalScreenHeight"; + // Style constants private static final String STYLE_DEFAULT = "default"; private static final String STYLE_LIGHT_CONTENT = "lightcontent"; @@ -107,31 +119,11 @@ public boolean execute(final String action, final CordovaArgs args, final Callba return true; case ACTION_SHOW: - activity.runOnUiThread(() -> { - int uiOptions = window.getDecorView().getSystemUiVisibility(); - uiOptions &= ~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; - uiOptions &= ~View.SYSTEM_UI_FLAG_FULLSCREEN; - - window.getDecorView().setSystemUiVisibility(uiOptions); - - // CB-11197 We still need to update LayoutParams to force status bar - // to be hidden when entering e.g. text fields - window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - }); + activity.runOnUiThread(() -> showStatusBar()); return true; case ACTION_HIDE: - activity.runOnUiThread(() -> { - int uiOptions = window.getDecorView().getSystemUiVisibility() - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_FULLSCREEN; - - window.getDecorView().setSystemUiVisibility(uiOptions); - - // CB-11197 We still need to update LayoutParams to force status bar - // to be hidden when entering e.g. text fields - window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - }); + activity.runOnUiThread(() -> hideStatusBar()); return true; case ACTION_BACKGROUND_COLOR_BY_HEX_STRING: @@ -162,11 +154,80 @@ public boolean execute(final String action, final CordovaArgs args, final Callba activity.runOnUiThread(() -> setStatusBarStyle(STYLE_LIGHT_CONTENT)); return true; + case ACTION_GET_NAVIGATION_BAR_HEIGHT: + case ACTION_GET_STATUS_BAR_HEIGHT: + case ACTION_GET_TOTAL_SCREEN_HEIGHT: + handleHeightRequests(action, callbackContext); + return true; + default: return false; } } + /** + * Handle all height-related requests + */ + private void handleHeightRequests(String action, CallbackContext callbackContext) { + activity.runOnUiThread(() -> { + int height; + String logMessage; + + switch (action) { + case ACTION_GET_NAVIGATION_BAR_HEIGHT: + height = getNavigationBarHeight(); + logMessage = "Navigation bar height"; + break; + case ACTION_GET_STATUS_BAR_HEIGHT: + height = getStatusBarHeight(); + logMessage = "Status bar height"; + break; + case ACTION_GET_TOTAL_SCREEN_HEIGHT: + height = getTotalScreenHeight(); + logMessage = "Total screen height"; + break; + default: + return; + } + + LOG.d(TAG, logMessage + ": " + height); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, height)); + }); + } + + /** + * Show the status bar + */ + private void showStatusBar() { + int uiOptions = window.getDecorView().getSystemUiVisibility(); + uiOptions &= ~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + uiOptions &= ~View.SYSTEM_UI_FLAG_FULLSCREEN; + + window.getDecorView().setSystemUiVisibility(uiOptions); + + // CB-11197 We still need to update LayoutParams to force status bar + // to be hidden when entering e.g. text fields + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + /** + * Hide the status bar + */ + private void hideStatusBar() { + int uiOptions = window.getDecorView().getSystemUiVisibility() + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_FULLSCREEN; + + window.getDecorView().setSystemUiVisibility(uiOptions); + + // CB-11197 We still need to update LayoutParams to force status bar + // to be hidden when entering e.g. text fields + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + + /** + * Set the status bar background color + */ private void setStatusBarBackgroundColor(final String colorPref) { if (colorPref.isEmpty()) return; @@ -183,19 +244,44 @@ private void setStatusBarBackgroundColor(final String colorPref) { window.setStatusBarColor(color); } + /** + * Set the status bar transparency + */ private void setStatusBarTransparent(final boolean isTransparent) { - final Window window = cordova.getActivity().getWindow(); - int visibility = isTransparent - ? View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - : View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; + if (!isTransparent) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE); + return; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowCompat.setDecorFitsSystemWindows(window, false); + window.setStatusBarColor(Color.TRANSPARENT); + } + else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { + View decorView = window.getDecorView(); + + int flags = decorView.getSystemUiVisibility(); + flags |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + flags |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + flags |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + decorView.setSystemUiVisibility(flags); - window.getDecorView().setSystemUiVisibility(visibility); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(Color.TRANSPARENT); - if (isTransparent) { + window.setNavigationBarColor(Color.TRANSPARENT); + } + else { + int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + window.getDecorView().setSystemUiVisibility(visibility); window.setStatusBarColor(Color.TRANSPARENT); } } + /** + * Set the status bar style + */ private void setStatusBarStyle(final String style) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !style.isEmpty()) { View decorView = window.getDecorView(); @@ -210,4 +296,118 @@ private void setStatusBarStyle(final String style) { } } } + + private boolean isEdgeToEdge() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + View decorView = window.getDecorView(); + int uiOptions = decorView.getSystemUiVisibility(); + + // Check for flags that indicate edge-to-edge mode + boolean hasLayoutFullscreen = (uiOptions & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0; + boolean hasLayoutHideNavigation = (uiOptions & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0; + boolean hasImmersiveFlag = (uiOptions & View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) != 0 + || (uiOptions & View.SYSTEM_UI_FLAG_IMMERSIVE) != 0; + + // Check window flags + boolean hasTranslucentStatus = (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) != 0; + boolean hasTranslucentNavigation = (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) != 0; + boolean hasLayoutNoLimits = (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) != 0; + + boolean drawSystemBarBackgrounds = (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; + int statusBarColor = window.getStatusBarColor(); + int navBarColor = window.getNavigationBarColor(); + + // Check if status bar or nav bar has transparency + boolean hasTransparentBars = drawSystemBarBackgrounds && + (Color.alpha(statusBarColor) < 255 || Color.alpha(navBarColor) < 255); + + // If any of these flags are set, we're likely in edge-to-edge mode + return hasLayoutFullscreen || hasLayoutHideNavigation || hasImmersiveFlag || + hasTranslucentStatus || hasTranslucentNavigation || hasLayoutNoLimits || + hasTransparentBars; + } + return true; + } + + /** + * Gets window insets for API 30+ devices + */ + private WindowInsets getWindowInsets() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + View decorView = window.getDecorView(); + return decorView.getRootWindowInsets(); + } + return null; + } + + /** + * Helper method to check if the device has a navigation bar. + * + * @return true if the device has a navigation bar, false otherwise + */ + private boolean hasNavigationBar() { + Resources resources = activity.getResources(); + int id = resources.getIdentifier("config_showNavigationBar", "bool", "android"); + if (id > 0) { + return resources.getBoolean(id); + } + + boolean hasMenuKey = ViewConfiguration.get(activity).hasPermanentMenuKey(); + boolean hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK); + + return !hasMenuKey && !hasBackKey; + } + + /** + * Gets the bottom navigation bar height + */ + private int getNavigationBarHeight() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowInsets insets = getWindowInsets(); + if (insets != null) { + return insets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()).bottom; + } + } else if (isEdgeToEdge()) { + Resources resources = activity.getResources(); + int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId > 0 && hasNavigationBar()) { + return resources.getDimensionPixelSize(resourceId); + } + } + return 0; + } + + /** + * Gets the top status bar height + */ + private int getStatusBarHeight() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowInsets insets = getWindowInsets(); + if (insets != null) { + return insets.getInsetsIgnoringVisibility(WindowInsets.Type.statusBars()).top; + } + } else if (isEdgeToEdge()) { + Resources resources = activity.getResources(); + int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + return resources.getDimensionPixelSize(resourceId); + } + } + return 0; + } + + /** + * Gets the total screen height + */ + private int getTotalScreenHeight() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + WindowManager windowManager = activity.getSystemService(WindowManager.class); + WindowMetrics metrics = windowManager.getCurrentWindowMetrics(); + return metrics.getBounds().height(); + } else { + DisplayMetrics displayMetrics = new DisplayMetrics(); + activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + return displayMetrics.heightPixels; + } + } } diff --git a/src/browser/StatusBarProxy.js b/src/browser/StatusBarProxy.js index 32a4f80c..9c532d91 100644 --- a/src/browser/StatusBarProxy.js +++ b/src/browser/StatusBarProxy.js @@ -40,7 +40,10 @@ module.exports = { backgroundColorByHexString: notSupported, hide: notSupported, show: notSupported, - _ready: notSupported + _ready: notSupported, + getNavigationBarHeight: notSupported, + getStatusBarHeight: notSupported, + getTotalScreenHeight: notSupported }; require('cordova/exec/proxy').add('StatusBar', module.exports); diff --git a/src/ios/CDVStatusBar.h b/src/ios/CDVStatusBar.h index 4c23b101..84e7bd04 100644 --- a/src/ios/CDVStatusBar.h +++ b/src/ios/CDVStatusBar.h @@ -6,9 +6,9 @@ to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 - + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -42,7 +42,11 @@ - (void) hide:(CDVInvokedUrlCommand*)command; - (void) show:(CDVInvokedUrlCommand*)command; - + - (void) _ready:(CDVInvokedUrlCommand*)command; +- (void) getStatusBarHeight:(CDVInvokedUrlCommand*)command; +- (void) getNavigationBarHeight:(CDVInvokedUrlCommand*)command; +- (void) getTotalScreenHeight:(CDVInvokedUrlCommand*)command; + @end diff --git a/src/ios/CDVStatusBar.m b/src/ios/CDVStatusBar.m index eb21d155..0c39533a 100644 --- a/src/ios/CDVStatusBar.m +++ b/src/ios/CDVStatusBar.m @@ -148,7 +148,7 @@ - (void)pluginInitialize } else { [self webViewScrollView].scrollsToTop = NO; } - + // blank scroll view to intercept status bar taps UIScrollView *fakeScrollView = [[UIScrollView alloc] initWithFrame:UIScreen.mainScreen.bounds]; fakeScrollView.delegate = self; @@ -454,7 +454,7 @@ -(void)resizeWebView } frame.size.height -= frame.origin.y; self.webView.frame = frame; - + } - (void) dealloc @@ -483,4 +483,55 @@ - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView return NO; } +#pragma mark - Height Measurement Methods + +- (void) getStatusBarHeight:(CDVInvokedUrlCommand*)command +{ + CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height; + + if (@available(iOS 11.0, *)) { + UIWindow *window = UIApplication.sharedApplication.keyWindow; + CGFloat topPadding = window.safeAreaInsets.top; + + if (topPadding > 20) { + statusBarHeight = topPadding; + } + } + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:statusBarHeight]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (void) getNavigationBarHeight:(CDVInvokedUrlCommand*)command +{ + CGFloat navigationBarHeight = 0; + + if ([self.viewController isKindOfClass:[UINavigationController class]]) { + UINavigationController *navController = (UINavigationController *)self.viewController; + navigationBarHeight = navController.navigationBar.frame.size.height; + } else if (self.viewController.navigationController) { + navigationBarHeight = self.viewController.navigationController.navigationBar.frame.size.height; + } else { + navigationBarHeight = 0.0; // Standard navigation bar height + } + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:navigationBarHeight]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + +- (void) getTotalScreenHeight:(CDVInvokedUrlCommand*)command +{ + CGFloat screenHeight = 0; + + CGRect screenBounds = [[UIScreen mainScreen] bounds]; + screenHeight = screenBounds.size.height; + + if (@available(iOS 11.0, *)) { + UIWindow *window = UIApplication.sharedApplication.keyWindow; + } + + CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDouble:screenHeight]; + [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; +} + @end diff --git a/www/statusbar.js b/www/statusbar.js index c191e14a..a5fe6764 100644 --- a/www/statusbar.js +++ b/www/statusbar.js @@ -40,21 +40,57 @@ var namedColors = { brown: '#A52A2A' }; +/** + * Helper function to execute cordova commands + * @param {string} action - The action to execute + * @param {Array} args - Arguments for the action + * @param {Function} successCallback - Optional success callback + * @param {Function} errorCallback - Optional error callback + * @returns {*} - Return value from success callback if provided + */ +function execStatusBarCommand (action, args, successCallback, errorCallback) { + return exec( + function (result) { + if (typeof successCallback === 'function') { + return successCallback(result); + } + return result; + }, + function (error) { + if (typeof errorCallback === 'function') { + errorCallback(error); + } + }, + 'StatusBar', + action, + args || [] + ); +} + +/** + * Simple command execution without callbacks + * @param {string} action - The action to execute + * @param {Array} args - Arguments for the action + */ +function execSimpleCommand (action, args) { + exec(null, null, 'StatusBar', action, args || []); +} + var StatusBar = { isVisible: true, overlaysWebView: function (doOverlay) { - exec(null, null, 'StatusBar', 'overlaysWebView', [doOverlay]); + execSimpleCommand('overlaysWebView', [doOverlay]); }, styleDefault: function () { - // dark text ( to be used on a light background ) - exec(null, null, 'StatusBar', 'styleDefault', []); + // dark text (to be used on a light background) + execSimpleCommand('styleDefault'); }, styleLightContent: function () { - // light text ( to be used on a dark background ) - exec(null, null, 'StatusBar', 'styleLightContent', []); + // light text (to be used on a dark background) + execSimpleCommand('styleLightContent'); }, backgroundColorByName: function (colorname) { @@ -71,17 +107,40 @@ var StatusBar = { hexString = '#' + split[1] + split[1] + split[2] + split[2] + split[3] + split[3]; } - exec(null, null, 'StatusBar', 'backgroundColorByHexString', [hexString]); + execSimpleCommand('backgroundColorByHexString', [hexString]); }, hide: function () { - exec(null, null, 'StatusBar', 'hide', []); + execSimpleCommand('hide'); StatusBar.isVisible = false; }, show: function () { - exec(null, null, 'StatusBar', 'show', []); + execSimpleCommand('show'); StatusBar.isVisible = true; + }, + + /** + * Gets the height of various UI elements + * @param {string} elementType - The element type to get height for + * @param {Function} successCallback - Success callback + * @param {Function} errorCallback - Error callback + * @returns {*} - Return value from success callback + */ + getHeight: function (elementType, successCallback, errorCallback) { + return execStatusBarCommand(elementType, [], successCallback, errorCallback); + }, + + getNavigationBarHeight: function (successCallback, errorCallback) { + return StatusBar.getHeight('getNavigationBarHeight', successCallback, errorCallback); + }, + + getStatusBarHeight: function (successCallback, errorCallback) { + return StatusBar.getHeight('getStatusBarHeight', successCallback, errorCallback); + }, + + getTotalScreenHeight: function (successCallback, errorCallback) { + return StatusBar.getHeight('getTotalScreenHeight', successCallback, errorCallback); } };