@onekeyfe/react-native-device-utils 3.0.93 → 3.0.95

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.
@@ -6,10 +6,17 @@ import android.content.pm.PackageManager
6
6
  import android.graphics.Color
7
7
  import android.graphics.Rect
8
8
  import android.os.Build
9
+ import android.view.Gravity
10
+ import android.view.View
11
+ import android.view.ViewGroup
12
+ import android.widget.FrameLayout
9
13
  import com.margelo.nitro.nativelogger.OneKeyLog
10
14
  import androidx.preference.PreferenceManager
11
15
  import androidx.core.content.ContextCompat
12
16
  import androidx.core.util.Consumer
17
+ import androidx.core.view.ViewCompat
18
+ import androidx.core.view.WindowCompat
19
+ import androidx.core.view.WindowInsetsCompat
13
20
  import androidx.window.layout.FoldingFeature
14
21
  import androidx.window.layout.WindowInfoTracker
15
22
  import androidx.window.layout.WindowLayoutInfo
@@ -28,6 +35,11 @@ data class Listener(
28
35
  val callback: (Boolean) -> Unit
29
36
  )
30
37
 
38
+ private data class NavigationBarAppearance(
39
+ val color: Int,
40
+ val useDarkIcons: Boolean,
41
+ )
42
+
31
43
  @DoNotStrip
32
44
  class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEventListener {
33
45
 
@@ -42,6 +54,8 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven
42
54
  private const val RECOVERY_PREFS_NAME = "onekey_recovery"
43
55
  private const val BOOT_FAIL_COUNT_KEY = "consecutive_boot_fail_count"
44
56
  private const val RECOVERY_ACTION_KEY = "recovery_action"
57
+ private const val NAVIGATION_BAR_PROTECTION_VIEW_TAG =
58
+ "com.onekeyfe.reactnativedeviceutils.navigationBarProtection"
45
59
 
46
60
  @JvmStatic
47
61
  var staticStartupTime: Long? = null
@@ -237,6 +251,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven
237
251
  private var isObservingLayoutChanges = false
238
252
  private var nextListenerId = 0.0
239
253
  private var isDualScreenDeviceDetected: Boolean? = null
254
+ @Volatile private var navigationBarAppearance: NavigationBarAppearance? = null
240
255
 
241
256
  init {
242
257
  NitroModules.applicationContext?.let { ctx ->
@@ -850,8 +865,161 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven
850
865
  }
851
866
  }
852
867
 
868
+ // MARK: - System Navigation Bar
869
+
870
+ override fun setNavigationBarAppearance(
871
+ r: Double,
872
+ g: Double,
873
+ b: Double,
874
+ a: Double,
875
+ useDarkIcons: Boolean,
876
+ ) {
877
+ val requestedColor = Color.argb(
878
+ a.toInt().coerceIn(0, 255),
879
+ r.toInt().coerceIn(0, 255),
880
+ g.toInt().coerceIn(0, 255),
881
+ b.toInt().coerceIn(0, 255),
882
+ )
883
+ val appearance = NavigationBarAppearance(
884
+ color = if (
885
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.O && useDarkIcons
886
+ ) Color.BLACK else requestedColor,
887
+ useDarkIcons = useDarkIcons,
888
+ )
889
+ navigationBarAppearance = appearance
890
+ getCurrentActivity()?.let { applyNavigationBarAppearance(it, appearance) }
891
+ }
892
+
893
+ @Suppress("DEPRECATION")
894
+ private fun applyNavigationBarAppearance(
895
+ activity: Activity,
896
+ appearance: NavigationBarAppearance,
897
+ ) {
898
+ activity.runOnUiThread {
899
+ try {
900
+ val window = activity.window
901
+ val decorView = window.decorView
902
+
903
+ WindowCompat.getInsetsController(window, decorView)
904
+ .isAppearanceLightNavigationBars = appearance.useDarkIcons
905
+
906
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
907
+ window.isNavigationBarContrastEnforced = false
908
+ }
909
+
910
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
911
+ // Android 15 enforces edge-to-edge. Keep the platform bar transparent
912
+ // and draw the requested color only behind three-button navigation.
913
+ window.navigationBarColor = Color.TRANSPARENT
914
+ installNavigationBarProtection(decorView, appearance.color)
915
+ } else {
916
+ removeNavigationBarProtection(decorView)
917
+ window.navigationBarColor = appearance.color
918
+ }
919
+ } catch (e: Exception) {
920
+ OneKeyLog.error(
921
+ "DeviceUtils",
922
+ "Failed to update navigation bar appearance: ${e.message}",
923
+ )
924
+ }
925
+ }
926
+ }
927
+
928
+ private fun installNavigationBarProtection(decorView: View, color: Int) {
929
+ val decorContainer = decorView as? ViewGroup ?: return
930
+ val protectionView = findNavigationBarProtection(decorContainer) ?: View(
931
+ decorContainer.context
932
+ ).also { view ->
933
+ view.tag = NAVIGATION_BAR_PROTECTION_VIEW_TAG
934
+ view.isClickable = false
935
+ view.isFocusable = false
936
+ view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
937
+ decorContainer.addView(
938
+ view,
939
+ FrameLayout.LayoutParams(
940
+ ViewGroup.LayoutParams.MATCH_PARENT,
941
+ 0,
942
+ Gravity.BOTTOM,
943
+ ),
944
+ )
945
+ }
946
+
947
+ protectionView.setBackgroundColor(color)
948
+ protectionView.visibility = View.VISIBLE
949
+ protectionView.bringToFront()
950
+ ViewCompat.setOnApplyWindowInsetsListener(protectionView) { view, windowInsets ->
951
+ val navigationBarInsets = windowInsets.getInsets(
952
+ WindowInsetsCompat.Type.navigationBars()
953
+ )
954
+ val tappableElementInsets = windowInsets.getInsets(
955
+ WindowInsetsCompat.Type.tappableElement()
956
+ )
957
+ val leftInset = minOf(navigationBarInsets.left, tappableElementInsets.left)
958
+ val rightInset = minOf(navigationBarInsets.right, tappableElementInsets.right)
959
+ val bottomInset = minOf(navigationBarInsets.bottom, tappableElementInsets.bottom)
960
+ val layoutParams = view.layoutParams as FrameLayout.LayoutParams
961
+ val (width, height, gravity) = when {
962
+ leftInset > 0 -> Triple(
963
+ leftInset,
964
+ ViewGroup.LayoutParams.MATCH_PARENT,
965
+ Gravity.LEFT,
966
+ )
967
+ rightInset > 0 -> Triple(
968
+ rightInset,
969
+ ViewGroup.LayoutParams.MATCH_PARENT,
970
+ Gravity.RIGHT,
971
+ )
972
+ bottomInset > 0 -> Triple(
973
+ ViewGroup.LayoutParams.MATCH_PARENT,
974
+ bottomInset,
975
+ Gravity.BOTTOM,
976
+ )
977
+ else -> Triple(0, 0, Gravity.BOTTOM)
978
+ }
979
+ if (
980
+ layoutParams.width != width ||
981
+ layoutParams.height != height ||
982
+ layoutParams.gravity != gravity
983
+ ) {
984
+ layoutParams.width = width
985
+ layoutParams.height = height
986
+ layoutParams.gravity = gravity
987
+ view.layoutParams = layoutParams
988
+ }
989
+ view.visibility = if (
990
+ leftInset > 0 || rightInset > 0 || bottomInset > 0
991
+ ) View.VISIBLE else View.INVISIBLE
992
+
993
+ // This view is only a visual protection; descendants still need the insets.
994
+ windowInsets
995
+ }
996
+ ViewCompat.requestApplyInsets(protectionView)
997
+ }
998
+
999
+ private fun findNavigationBarProtection(decorContainer: ViewGroup): View? {
1000
+ for (index in 0 until decorContainer.childCount) {
1001
+ val child = decorContainer.getChildAt(index)
1002
+ if (child.tag == NAVIGATION_BAR_PROTECTION_VIEW_TAG) {
1003
+ return child
1004
+ }
1005
+ }
1006
+ return null
1007
+ }
1008
+
1009
+ private fun removeNavigationBarProtection(decorView: View) {
1010
+ val decorContainer = decorView as? ViewGroup ?: return
1011
+ val protectionView = findNavigationBarProtection(decorContainer) ?: return
1012
+ ViewCompat.setOnApplyWindowInsetsListener(protectionView, null)
1013
+ decorContainer.removeView(protectionView)
1014
+ }
1015
+
853
1016
  override fun onHostResume() {
854
1017
  startObservingLayoutChanges()
1018
+ val activity = getCurrentActivity()
1019
+ val appearance = navigationBarAppearance
1020
+ if (activity != null && appearance != null) {
1021
+ applyNavigationBarAppearance(activity, appearance)
1022
+ }
855
1023
  }
856
1024
 
857
1025
  override fun onHostPause() {
@@ -163,6 +163,16 @@ class ReactNativeDeviceUtils: HybridReactNativeDeviceUtilsSpec {
163
163
  }
164
164
  }
165
165
 
166
+ public func setNavigationBarAppearance(
167
+ r: Double,
168
+ g: Double,
169
+ b: Double,
170
+ a: Double,
171
+ useDarkIcons: Bool
172
+ ) throws -> Void {
173
+ // Android-only system UI.
174
+ }
175
+
166
176
  // MARK: - LaunchOptionsManager
167
177
 
168
178
  func getLaunchOptions() throws -> Promise<LaunchOptions> {
@@ -31,6 +31,14 @@ export interface ReactNativeDeviceUtils extends HybridObject<{
31
31
  getWindowRects(): Promise<DualScreenInfoRect[]>;
32
32
  getHingeBounds(): Promise<DualScreenInfoRect>;
33
33
  changeBackgroundColor(r: number, g: number, b: number, a: number): void;
34
+ /**
35
+ * Updates the Android system navigation bar from the UI runtime.
36
+ * Color channels use the [0, 255] range. `useDarkIcons` should be true for
37
+ * light backgrounds. Android API 24-25 cannot render dark navigation icons,
38
+ * so those versions use an opaque black background fallback when requested.
39
+ * This is a no-op on iOS.
40
+ */
41
+ setNavigationBarAppearance(r: number, g: number, b: number, a: number, useDarkIcons: boolean): void;
34
42
  addSpanningChangedListener(callback: (isSpanning: boolean) => void): number;
35
43
  removeSpanningChangedListener(id: number): void;
36
44
  setUserInterfaceStyle(style: UserInterfaceStyle): void;
@@ -138,6 +138,10 @@ namespace margelo::nitro::reactnativedeviceutils {
138
138
  static const auto method = _javaPart->javaClassStatic()->getMethod<void(double /* r */, double /* g */, double /* b */, double /* a */)>("changeBackgroundColor");
139
139
  method(_javaPart, r, g, b, a);
140
140
  }
141
+ void JHybridReactNativeDeviceUtilsSpec::setNavigationBarAppearance(double r, double g, double b, double a, bool useDarkIcons) {
142
+ static const auto method = _javaPart->javaClassStatic()->getMethod<void(double /* r */, double /* g */, double /* b */, double /* a */, jboolean /* useDarkIcons */)>("setNavigationBarAppearance");
143
+ method(_javaPart, r, g, b, a, useDarkIcons);
144
+ }
141
145
  double JHybridReactNativeDeviceUtilsSpec::addSpanningChangedListener(const std::function<void(bool /* isSpanning */)>& callback) {
142
146
  static const auto method = _javaPart->javaClassStatic()->getMethod<double(jni::alias_ref<JFunc_void_bool::javaobject> /* callback */)>("addSpanningChangedListener_cxx");
143
147
  auto __result = method(_javaPart, JFunc_void_bool_cxx::fromCpp(callback));
@@ -60,6 +60,7 @@ namespace margelo::nitro::reactnativedeviceutils {
60
60
  std::shared_ptr<Promise<std::vector<DualScreenInfoRect>>> getWindowRects() override;
61
61
  std::shared_ptr<Promise<DualScreenInfoRect>> getHingeBounds() override;
62
62
  void changeBackgroundColor(double r, double g, double b, double a) override;
63
+ void setNavigationBarAppearance(double r, double g, double b, double a, bool useDarkIcons) override;
63
64
  double addSpanningChangedListener(const std::function<void(bool /* isSpanning */)>& callback) override;
64
65
  void removeSpanningChangedListener(double id) override;
65
66
  void setUserInterfaceStyle(UserInterfaceStyle style) override;
@@ -54,6 +54,10 @@ abstract class HybridReactNativeDeviceUtilsSpec: HybridObject() {
54
54
  @Keep
55
55
  abstract fun changeBackgroundColor(r: Double, g: Double, b: Double, a: Double): Unit
56
56
 
57
+ @DoNotStrip
58
+ @Keep
59
+ abstract fun setNavigationBarAppearance(r: Double, g: Double, b: Double, a: Double, useDarkIcons: Boolean): Unit
60
+
57
61
  abstract fun addSpanningChangedListener(callback: (isSpanning: Boolean) -> Unit): Double
58
62
 
59
63
  @DoNotStrip
@@ -134,6 +134,12 @@ namespace margelo::nitro::reactnativedeviceutils {
134
134
  std::rethrow_exception(__result.error());
135
135
  }
136
136
  }
137
+ inline void setNavigationBarAppearance(double r, double g, double b, double a, bool useDarkIcons) override {
138
+ auto __result = _swiftPart.setNavigationBarAppearance(std::forward<decltype(r)>(r), std::forward<decltype(g)>(g), std::forward<decltype(b)>(b), std::forward<decltype(a)>(a), std::forward<decltype(useDarkIcons)>(useDarkIcons));
139
+ if (__result.hasError()) [[unlikely]] {
140
+ std::rethrow_exception(__result.error());
141
+ }
142
+ }
137
143
  inline double addSpanningChangedListener(const std::function<void(bool /* isSpanning */)>& callback) override {
138
144
  auto __result = _swiftPart.addSpanningChangedListener(callback);
139
145
  if (__result.hasError()) [[unlikely]] {
@@ -19,6 +19,7 @@ public protocol HybridReactNativeDeviceUtilsSpec_protocol: HybridObject {
19
19
  func getWindowRects() throws -> Promise<[DualScreenInfoRect]>
20
20
  func getHingeBounds() throws -> Promise<DualScreenInfoRect>
21
21
  func changeBackgroundColor(r: Double, g: Double, b: Double, a: Double) throws -> Void
22
+ func setNavigationBarAppearance(r: Double, g: Double, b: Double, a: Double, useDarkIcons: Bool) throws -> Void
22
23
  func addSpanningChangedListener(callback: @escaping (_ isSpanning: Bool) -> Void) throws -> Double
23
24
  func removeSpanningChangedListener(id: Double) throws -> Void
24
25
  func setUserInterfaceStyle(style: UserInterfaceStyle) throws -> Void
@@ -214,6 +214,17 @@ open class HybridReactNativeDeviceUtilsSpec_cxx {
214
214
  }
215
215
  }
216
216
 
217
+ @inline(__always)
218
+ public final func setNavigationBarAppearance(r: Double, g: Double, b: Double, a: Double, useDarkIcons: Bool) -> bridge.Result_void_ {
219
+ do {
220
+ try self.__implementation.setNavigationBarAppearance(r: r, g: g, b: b, a: a, useDarkIcons: useDarkIcons)
221
+ return bridge.create_Result_void_()
222
+ } catch (let __error) {
223
+ let __exceptionPtr = __error.toCpp()
224
+ return bridge.create_Result_void_(__exceptionPtr)
225
+ }
226
+ }
227
+
217
228
  @inline(__always)
218
229
  public final func addSpanningChangedListener(callback: bridge.Func_void_bool) -> bridge.Result_double_ {
219
230
  do {
@@ -20,6 +20,7 @@ namespace margelo::nitro::reactnativedeviceutils {
20
20
  prototype.registerHybridMethod("getWindowRects", &HybridReactNativeDeviceUtilsSpec::getWindowRects);
21
21
  prototype.registerHybridMethod("getHingeBounds", &HybridReactNativeDeviceUtilsSpec::getHingeBounds);
22
22
  prototype.registerHybridMethod("changeBackgroundColor", &HybridReactNativeDeviceUtilsSpec::changeBackgroundColor);
23
+ prototype.registerHybridMethod("setNavigationBarAppearance", &HybridReactNativeDeviceUtilsSpec::setNavigationBarAppearance);
23
24
  prototype.registerHybridMethod("addSpanningChangedListener", &HybridReactNativeDeviceUtilsSpec::addSpanningChangedListener);
24
25
  prototype.registerHybridMethod("removeSpanningChangedListener", &HybridReactNativeDeviceUtilsSpec::removeSpanningChangedListener);
25
26
  prototype.registerHybridMethod("setUserInterfaceStyle", &HybridReactNativeDeviceUtilsSpec::setUserInterfaceStyle);
@@ -77,6 +77,7 @@ namespace margelo::nitro::reactnativedeviceutils {
77
77
  virtual std::shared_ptr<Promise<std::vector<DualScreenInfoRect>>> getWindowRects() = 0;
78
78
  virtual std::shared_ptr<Promise<DualScreenInfoRect>> getHingeBounds() = 0;
79
79
  virtual void changeBackgroundColor(double r, double g, double b, double a) = 0;
80
+ virtual void setNavigationBarAppearance(double r, double g, double b, double a, bool useDarkIcons) = 0;
80
81
  virtual double addSpanningChangedListener(const std::function<void(bool /* isSpanning */)>& callback) = 0;
81
82
  virtual void removeSpanningChangedListener(double id) = 0;
82
83
  virtual void setUserInterfaceStyle(UserInterfaceStyle style) = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-device-utils",
3
- "version": "3.0.93",
3
+ "version": "3.0.95",
4
4
  "description": "react-native-device-utils",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -48,6 +48,20 @@ export interface ReactNativeDeviceUtils
48
48
  getWindowRects(): Promise<DualScreenInfoRect[]>;
49
49
  getHingeBounds(): Promise<DualScreenInfoRect>;
50
50
  changeBackgroundColor(r: number, g: number, b: number, a: number): void;
51
+ /**
52
+ * Updates the Android system navigation bar from the UI runtime.
53
+ * Color channels use the [0, 255] range. `useDarkIcons` should be true for
54
+ * light backgrounds. Android API 24-25 cannot render dark navigation icons,
55
+ * so those versions use an opaque black background fallback when requested.
56
+ * This is a no-op on iOS.
57
+ */
58
+ setNavigationBarAppearance(
59
+ r: number,
60
+ g: number,
61
+ b: number,
62
+ a: number,
63
+ useDarkIcons: boolean
64
+ ): void;
51
65
  addSpanningChangedListener(callback: (isSpanning: boolean) => void): number;
52
66
  removeSpanningChangedListener(id: number): void;
53
67
  setUserInterfaceStyle(style: UserInterfaceStyle): void;