rn-system-bar 3.2.3 → 3.2.5

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,4 +6,12 @@
6
6
  android:name="android.permission.WRITE_SETTINGS"
7
7
  tools:ignore="ProtectedPermissions" />
8
8
 
9
- </manifest>
9
+ <!-- Required for network state / listener -->
10
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
11
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
12
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
13
+
14
+ <!-- Required for haptic feedback / vibration -->
15
+ <uses-permission android:name="android.permission.VIBRATE" />
16
+
17
+ </manifest>
@@ -10,13 +10,22 @@ import android.content.Context
10
10
  import android.content.Intent
11
11
  import android.content.IntentFilter
12
12
  import android.content.pm.ActivityInfo
13
+ import android.content.res.Configuration
13
14
  import android.graphics.Color
14
15
  import android.hardware.display.DisplayManager
15
16
  import android.media.AudioManager
17
+ import android.net.ConnectivityManager
18
+ import android.net.NetworkCapabilities
19
+ import android.net.NetworkRequest
20
+ import android.os.BatteryManager
16
21
  import android.os.Build
17
22
  import android.os.Handler
18
23
  import android.os.Looper
24
+ import android.os.VibrationEffect
25
+ import android.os.Vibrator
26
+ import android.os.VibratorManager
19
27
  import android.provider.Settings
28
+ import android.util.DisplayMetrics
20
29
  import android.view.View
21
30
  import android.view.WindowInsets
22
31
  import android.view.WindowInsetsController
@@ -503,225 +512,215 @@ class SystemBarModule(
503
512
  displayListener = null
504
513
  }
505
514
 
515
+
506
516
  // ═══════════════════════════════════════════════
507
- // APP-ONLY CAST (MediaRouter — Chromecast / TV)
517
+ // NETWORK
508
518
  // ═══════════════════════════════════════════════
509
519
 
510
- private val mediaRouter: android.media.MediaRouter by lazy {
511
- reactContext.getSystemService(Context.MEDIA_ROUTER_SERVICE) as android.media.MediaRouter
512
- }
520
+ private fun connectivityManager() =
521
+ reactContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
522
+
523
+ private fun buildNetworkMap(): WritableMap {
524
+ val cm = connectivityManager()
525
+ val map = Arguments.createMap()
513
526
 
514
- private var appCastState = "idle"
515
- private var connectedRoute: android.media.MediaRouter.RouteInfo? = null
516
- private var appCastCallback: android.media.MediaRouter.Callback? = null
517
- private val discoveredRoutes = mutableMapOf<String, android.media.MediaRouter.RouteInfo>()
527
+ val net = cm.activeNetwork
528
+ val caps = if (net != null) cm.getNetworkCapabilities(net) else null
518
529
 
519
- // ── Helpers ───────────────────────────────────
530
+ val isConnected = caps != null &&
531
+ (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ||
532
+ caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
520
533
 
521
- private fun routeId(r: android.media.MediaRouter.RouteInfo): String =
522
- r.name?.toString()?.replace(" ", "_")?.plus("_${r.hashCode()}") ?: r.hashCode().toString()
534
+ val type = when {
535
+ caps == null -> "none"
536
+ caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
537
+ caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
538
+ caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet"
539
+ else -> "unknown"
540
+ }
523
541
 
524
- /**
525
- * Replaces the removed RouteInfo.isDefault property (removed in API 34+).
526
- * The default route is the built-in phone speaker / earpiece.
527
- * We identify it by checking that it only supports audio (not video/live-video)
528
- * and that it carries no presentation display.
529
- */
530
- private fun android.media.MediaRouter.RouteInfo.isDefaultRoute(): Boolean {
531
- val videoMask = android.media.MediaRouter.ROUTE_TYPE_LIVE_VIDEO or
532
- android.media.MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY
533
- return (supportedTypes and videoMask) == 0
542
+ map.putString("type", type)
543
+ map.putBoolean("isConnected", isConnected)
544
+ val airplaneMode = Settings.Global.getInt(
545
+ reactContext.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0) == 1
546
+ map.putBoolean("isAirplaneMode", airplaneMode)
547
+ map.putNull("ssid")
548
+ map.putNull("cellularGeneration")
549
+ return map
534
550
  }
535
551
 
536
- private fun routeToMap(r: android.media.MediaRouter.RouteInfo): WritableMap {
537
- val m = Arguments.createMap()
538
- m.putString("id", routeId(r))
539
- m.putString("name", r.name?.toString() ?: "Unknown")
540
- val desc = r.description?.toString()
541
- if (desc != null) m.putString("description", desc) else m.putNull("description")
542
- m.putNull("signalStrength")
543
- m.putBoolean("requiresPairing", false)
544
- return m
552
+ @ReactMethod
553
+ fun getNetworkInfo(promise: Promise) {
554
+ try { promise.resolve(buildNetworkMap()) }
555
+ catch (e: Exception) { promise.reject("NETWORK_ERROR", e.message, e) }
545
556
  }
546
557
 
547
- private fun buildAppCastMap(): WritableMap {
548
- val map = Arguments.createMap()
549
- map.putString("state", appCastState)
550
- map.putNull("error")
558
+ private var networkCallback: ConnectivityManager.NetworkCallback? = null
551
559
 
552
- val devArr = Arguments.createArray()
553
- for (r in discoveredRoutes.values) devArr.pushMap(routeToMap(r))
554
- map.putArray("devices", devArr)
560
+ @ReactMethod
561
+ fun startNetworkListener() {
562
+ if (networkCallback != null) return
563
+ val cb = object : ConnectivityManager.NetworkCallback() {
564
+ override fun onAvailable(network: android.net.Network) { emit("SystemBar_NetworkChange", buildNetworkMap()) }
565
+ override fun onLost(network: android.net.Network) { emit("SystemBar_NetworkChange", buildNetworkMap()) }
566
+ override fun onCapabilitiesChanged(
567
+ network: android.net.Network,
568
+ caps: NetworkCapabilities
569
+ ) { emit("SystemBar_NetworkChange", buildNetworkMap()) }
570
+ }
571
+ networkCallback = cb
572
+ val request = NetworkRequest.Builder().build()
573
+ connectivityManager().registerNetworkCallback(request, cb)
574
+ }
555
575
 
556
- val connected = connectedRoute
557
- if (connected != null) map.putMap("connectedDevice", routeToMap(connected))
558
- else map.putNull("connectedDevice")
576
+ @ReactMethod
577
+ fun stopNetworkListener() {
578
+ networkCallback?.let {
579
+ try { connectivityManager().unregisterNetworkCallback(it) } catch (_: Exception) {}
580
+ }
581
+ networkCallback = null
582
+ }
559
583
 
584
+ // ═══════════════════════════════════════════════
585
+ // BATTERY
586
+ // ═══════════════════════════════════════════════
587
+
588
+ private fun buildBatteryMap(): WritableMap {
589
+ val map = Arguments.createMap()
590
+ val intent = reactContext.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
591
+
592
+ val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
593
+ val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, 100) ?: 100
594
+ val pct = if (scale > 0) (level * 100 / scale) else -1
595
+
596
+ val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN)
597
+ val state = when (status) {
598
+ BatteryManager.BATTERY_STATUS_CHARGING -> "charging"
599
+ BatteryManager.BATTERY_STATUS_FULL -> "full"
600
+ BatteryManager.BATTERY_STATUS_DISCHARGING,
601
+ BatteryManager.BATTERY_STATUS_NOT_CHARGING -> "discharging"
602
+ else -> "unknown"
603
+ }
604
+ val isCharging = state == "charging" || state == "full"
605
+
606
+ map.putInt("level", pct)
607
+ map.putString("state", state)
608
+ map.putBoolean("isCharging", isCharging)
609
+ map.putBoolean("isLow", pct in 0..20 && !isCharging)
560
610
  return map
561
611
  }
562
612
 
563
- private fun emitAppCastChange(error: String? = null) {
564
- val map = buildAppCastMap()
565
- if (error != null) map.putString("error", error)
566
- emit("SystemBar_AppCastChange", map)
613
+ @ReactMethod
614
+ fun getBatteryInfo(promise: Promise) {
615
+ try { promise.resolve(buildBatteryMap()) }
616
+ catch (e: Exception) { promise.reject("BATTERY_ERROR", e.message, e) }
567
617
  }
568
618
 
569
- // ── Scan ──────────────────────────────────────
619
+ private var batteryReceiver: BroadcastReceiver? = null
570
620
 
571
621
  @ReactMethod
572
- fun startAppCastScan() {
573
- if (appCastCallback != null) return
574
- appCastState = "scanning"
575
- discoveredRoutes.clear()
576
-
577
- val cb = object : android.media.MediaRouter.Callback() {
578
-
579
- override fun onRouteAdded(
580
- router: android.media.MediaRouter,
581
- route: android.media.MediaRouter.RouteInfo
582
- ) {
583
- if (route.isDefaultRoute()) return
584
- val id = routeId(route)
585
- discoveredRoutes[id] = route
586
- emitAppCastChange()
622
+ fun startBatteryListener() {
623
+ if (batteryReceiver != null) return
624
+ val receiver = object : BroadcastReceiver() {
625
+ override fun onReceive(ctx: Context?, intent: Intent?) {
626
+ emit("SystemBar_BatteryChange", buildBatteryMap())
587
627
  }
628
+ }
629
+ batteryReceiver = receiver
630
+ val filter = IntentFilter().apply {
631
+ addAction(Intent.ACTION_BATTERY_CHANGED)
632
+ addAction(Intent.ACTION_BATTERY_LOW)
633
+ addAction(Intent.ACTION_BATTERY_OKAY)
634
+ addAction(Intent.ACTION_POWER_CONNECTED)
635
+ addAction(Intent.ACTION_POWER_DISCONNECTED)
636
+ }
637
+ reactContext.registerReceiver(receiver, filter)
638
+ }
588
639
 
589
- override fun onRouteRemoved(
590
- router: android.media.MediaRouter,
591
- route: android.media.MediaRouter.RouteInfo
592
- ) {
593
- discoveredRoutes.remove(routeId(route))
594
- if (connectedRoute?.let { routeId(it) } == routeId(route)) {
595
- connectedRoute = null
596
- appCastState = "idle"
597
- }
598
- emitAppCastChange()
599
- }
640
+ @ReactMethod
641
+ fun stopBatteryListener() {
642
+ batteryReceiver?.let { try { reactContext.unregisterReceiver(it) } catch (_: Exception) {} }
643
+ batteryReceiver = null
644
+ }
600
645
 
601
- override fun onRouteChanged(
602
- router: android.media.MediaRouter,
603
- route: android.media.MediaRouter.RouteInfo
604
- ) {
605
- val id = routeId(route)
606
- if (!route.isDefaultRoute()) discoveredRoutes[id] = route
607
- emitAppCastChange()
608
- }
646
+ // ═══════════════════════════════════════════════
647
+ // HAPTICS
648
+ // ═══════════════════════════════════════════════
609
649
 
610
- override fun onRouteSelected(
611
- router: android.media.MediaRouter,
612
- type: Int,
613
- route: android.media.MediaRouter.RouteInfo
614
- ) {
615
- if (route.isDefaultRoute()) return
616
- connectedRoute = route
617
- appCastState = "connected"
618
- emitAppCastChange()
619
- }
650
+ private fun vibrator(): Vibrator =
651
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
652
+ (reactContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager)
653
+ .defaultVibrator
654
+ } else {
655
+ @Suppress("DEPRECATION")
656
+ reactContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
657
+ }
620
658
 
621
- override fun onRouteUnselected(
622
- router: android.media.MediaRouter,
623
- type: Int,
624
- route: android.media.MediaRouter.RouteInfo
625
- ) {
626
- if (appCastState != "idle") {
627
- connectedRoute = null
628
- appCastState = "idle"
629
- emitAppCastChange()
659
+ @ReactMethod
660
+ fun haptic(pattern: String) {
661
+ try {
662
+ val vib = vibrator()
663
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
664
+ val effect = when (pattern) {
665
+ "light" -> VibrationEffect.createOneShot(30, 80)
666
+ "medium" -> VibrationEffect.createOneShot(50, 150)
667
+ "heavy" -> VibrationEffect.createOneShot(80, 255)
668
+ "success" -> VibrationEffect.createWaveform(longArrayOf(0, 40, 60, 40), intArrayOf(0, 180, 0, 255), -1)
669
+ "warning" -> VibrationEffect.createWaveform(longArrayOf(0, 60, 40, 60), intArrayOf(0, 200, 0, 200), -1)
670
+ "error" -> VibrationEffect.createWaveform(longArrayOf(0, 80, 40, 80, 40, 80), intArrayOf(0, 255, 0, 255, 0, 255), -1)
671
+ "selection" -> VibrationEffect.createOneShot(20, 60)
672
+ else -> VibrationEffect.createOneShot(50, 150)
630
673
  }
674
+ vib.vibrate(effect)
675
+ } else {
676
+ @Suppress("DEPRECATION")
677
+ vib.vibrate(when (pattern) {
678
+ "light" -> 30L
679
+ "heavy" -> 80L
680
+ "selection" -> 20L
681
+ else -> 50L
682
+ })
631
683
  }
684
+ } catch (_: Exception) {}
685
+ }
632
686
 
633
- // ── Required abstract stubs (no-op) ───────
634
- override fun onRouteGrouped(
635
- router: android.media.MediaRouter,
636
- route: android.media.MediaRouter.RouteInfo,
637
- group: android.media.MediaRouter.RouteGroup,
638
- index: Int
639
- ) {}
640
-
641
- override fun onRouteUngrouped(
642
- router: android.media.MediaRouter,
643
- route: android.media.MediaRouter.RouteInfo,
644
- group: android.media.MediaRouter.RouteGroup
645
- ) {}
646
-
647
- override fun onRouteVolumeChanged(
648
- router: android.media.MediaRouter,
649
- route: android.media.MediaRouter.RouteInfo
650
- ) {}
651
- }
687
+ // ═══════════════════════════════════════════════
688
+ // FONT SCALE
689
+ // ═══════════════════════════════════════════════
652
690
 
653
- appCastCallback = cb
654
- mainHandler.post {
655
- mediaRouter.addCallback(
656
- android.media.MediaRouter.ROUTE_TYPE_LIVE_VIDEO,
657
- cb,
658
- // CALLBACK_FLAG_REQUEST_DISCOVERY (0x4) and CALLBACK_FLAG_PERFORM_ACTIVE_SCAN (0x2)
659
- // were removed from the Android SDK — use their raw int values directly.
660
- 0x4 or 0x2
661
- )
662
- }
663
- emitAppCastChange()
691
+ private fun buildFontScaleMap(): WritableMap {
692
+ val map = Arguments.createMap()
693
+ val config = reactContext.resources.configuration
694
+ val metrics = reactContext.resources.displayMetrics
695
+ map.putDouble("fontScale", config.fontScale.toDouble())
696
+ map.putDouble("density", metrics.density.toDouble())
697
+ return map
664
698
  }
665
699
 
666
700
  @ReactMethod
667
- fun stopAppCastScan() {
668
- appCastCallback?.let { mainHandler.post { mediaRouter.removeCallback(it) } }
669
- appCastCallback = null
670
- if (appCastState == "scanning") {
671
- appCastState = "idle"
672
- emitAppCastChange()
673
- }
701
+ fun getFontScaleInfo(promise: Promise) {
702
+ try { promise.resolve(buildFontScaleMap()) }
703
+ catch (e: Exception) { promise.reject("FONTSCALE_ERROR", e.message, e) }
674
704
  }
675
705
 
676
- // ── Connect / Disconnect ──────────────────────
706
+ private var fontScaleReceiver: BroadcastReceiver? = null
677
707
 
678
708
  @ReactMethod
679
- fun connectAppCast(deviceId: String, pairingPin: String?) {
680
- val route = discoveredRoutes[deviceId]
681
- if (route == null) {
682
- val err = Arguments.createMap()
683
- err.putString("state", appCastState)
684
- err.putString("error", "Device not found: $deviceId")
685
- err.putNull("connectedDevice")
686
- err.putArray("devices", Arguments.createArray())
687
- emit("SystemBar_AppCastChange", err)
688
- return
689
- }
690
- appCastState = "connecting"
691
- emitAppCastChange()
692
- mainHandler.post {
693
- try {
694
- mediaRouter.selectRoute(android.media.MediaRouter.ROUTE_TYPE_LIVE_VIDEO, route)
695
- } catch (e: Exception) {
696
- appCastState = "idle"
697
- emitAppCastChange(error = e.message ?: "Connection failed")
698
- }
699
- }
700
- }
701
-
702
- @ReactMethod
703
- fun disconnectAppCast() {
704
- if (connectedRoute == null) return
705
- appCastState = "disconnecting"
706
- emitAppCastChange()
707
- mainHandler.post {
708
- try {
709
- val defaultRoute = mediaRouter.getDefaultRoute()
710
- mediaRouter.selectRoute(android.media.MediaRouter.ROUTE_TYPE_LIVE_VIDEO, defaultRoute)
711
- connectedRoute = null
712
- appCastState = "idle"
713
- emitAppCastChange()
714
- } catch (e: Exception) {
715
- appCastState = "idle"
716
- emitAppCastChange(error = e.message ?: "Disconnect failed")
709
+ fun startFontScaleListener() {
710
+ if (fontScaleReceiver != null) return
711
+ val receiver = object : BroadcastReceiver() {
712
+ override fun onReceive(ctx: Context?, intent: Intent?) {
713
+ emit("SystemBar_FontScaleChange", buildFontScaleMap())
717
714
  }
718
715
  }
716
+ fontScaleReceiver = receiver
717
+ reactContext.registerReceiver(receiver, IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED))
719
718
  }
720
719
 
721
720
  @ReactMethod
722
- fun getAppCastInfo(promise: Promise) {
723
- try { promise.resolve(buildAppCastMap()) }
724
- catch (e: Exception) { promise.reject("APP_CAST_ERROR", e.message, e) }
721
+ fun stopFontScaleListener() {
722
+ fontScaleReceiver?.let { try { reactContext.unregisterReceiver(it) } catch (_: Exception) {} }
723
+ fontScaleReceiver = null
725
724
  }
726
725
 
727
726
  // ── Cleanup ───────────────────────────────────
@@ -729,6 +728,8 @@ class SystemBarModule(
729
728
  stopBrightnessListener()
730
729
  stopVolumeListener()
731
730
  stopSystemScreencastListener()
732
- stopAppCastScan()
731
+ stopNetworkListener()
732
+ stopBatteryListener()
733
+ stopFontScaleListener()
733
734
  }
734
735
  }
package/index.ts CHANGED
@@ -13,7 +13,9 @@ export { setGlobalThemeMode, useTheme } from "./src/useTheme";
13
13
 
14
14
  // React hooks
15
15
  export {
16
- useAppCast,
16
+ useBattery,
17
+ useFontScale,
18
+ useNetwork,
17
19
  useScreencast,
18
20
  useSystemBar,
19
21
  useSystemScreencast,
@@ -25,5 +27,4 @@ export {
25
27
  export type {
26
28
  SystemBarConfig,
27
29
  ThemedSystemBarConfig,
28
- UseAppCastReturn,
29
30
  } from "./src/useSystemBar";
@@ -1,5 +1,5 @@
1
1
  // ─────────────────────────────────────────────
2
- // rn-system-bar · SystemBarModule.m v6
2
+ // rn-system-bar · SystemBarModule.m
3
3
  // Objective-C bridge — exposes Swift to React Native
4
4
  // ─────────────────────────────────────────────
5
5
 
@@ -41,13 +41,6 @@ RCT_EXTERN_METHOD(getSystemScreencastInfo:(RCTPromiseResolveBlock)resolve reject
41
41
  RCT_EXTERN_METHOD(startSystemScreencastListener)
42
42
  RCT_EXTERN_METHOD(stopSystemScreencastListener)
43
43
 
44
- // App-only Cast (Android MediaRouter — stubs on iOS)
45
- RCT_EXTERN_METHOD(startAppCastScan)
46
- RCT_EXTERN_METHOD(stopAppCastScan)
47
- RCT_EXTERN_METHOD(connectAppCast:(NSString *)deviceId pairingPin:(NSString *)pairingPin)
48
- RCT_EXTERN_METHOD(disconnectAppCast)
49
- RCT_EXTERN_METHOD(getAppCastInfo:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
50
-
51
44
  // Font Scale
52
45
  RCT_EXTERN_METHOD(getFontScaleInfo:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
53
46
  RCT_EXTERN_METHOD(startFontScaleListener)