rn-system-bar 3.0.2 → 3.1.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.
@@ -1,22 +1,36 @@
1
1
  // ─────────────────────────────────────────────
2
- // rn-system-bar · SystemBarModule.kt
3
- // Android Native Module Old + New Architecture
2
+ // rn-system-bar · SystemBarModule.kt v5
3
+ // New: Network, Battery, Haptics, Screencast, FontScale
4
4
  // ─────────────────────────────────────────────
5
5
 
6
6
  package com.systembar
7
7
 
8
8
  import android.app.Activity
9
+ import android.content.BroadcastReceiver
9
10
  import android.content.Context
11
+ import android.content.Intent
12
+ import android.content.IntentFilter
10
13
  import android.content.pm.ActivityInfo
11
- import android.graphics.Color
14
+ import android.hardware.display.DisplayManager
12
15
  import android.media.AudioManager
16
+ import android.net.ConnectivityManager
17
+ import android.net.Network
18
+ import android.net.NetworkCapabilities
19
+ import android.net.NetworkRequest
20
+ import android.os.BatteryManager
13
21
  import android.os.Build
22
+ import android.os.VibrationEffect
23
+ import android.os.Vibrator
24
+ import android.os.VibratorManager
14
25
  import android.provider.Settings
26
+ import android.view.Display
15
27
  import android.view.View
28
+ import android.view.WindowInsets
16
29
  import android.view.WindowInsetsController
17
30
  import android.view.WindowManager
18
31
 
19
32
  import com.facebook.react.bridge.*
33
+ import com.facebook.react.modules.core.DeviceEventManagerModule
20
34
 
21
35
  class SystemBarModule(
22
36
  private val reactContext: ReactApplicationContext
@@ -24,340 +38,413 @@ class SystemBarModule(
24
38
 
25
39
  override fun getName(): String = "SystemBar"
26
40
 
27
- // ── Helper ─────────────────────────────────────
28
41
  private fun activity(): Activity? = reactContext.currentActivity
29
42
 
30
- private fun audioManager(): AudioManager =
31
- reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
43
+ private fun emit(event: String, data: WritableMap) {
44
+ reactContext
45
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
46
+ .emit(event, data)
47
+ }
48
+
49
+ private fun audioManager() = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
50
+ private fun connectivityManager() = reactContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
51
+ private fun batteryManager() = reactContext.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
52
+ private fun displayManager() = reactContext.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
53
+
54
+ @Suppress("DEPRECATION")
55
+ private fun vibrator(): Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
56
+ (reactContext.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager).defaultVibrator
57
+ } else {
58
+ reactContext.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
59
+ }
32
60
 
33
61
  private fun streamType(stream: String): Int = when (stream) {
34
62
  "ring" -> AudioManager.STREAM_RING
35
63
  "notification" -> AudioManager.STREAM_NOTIFICATION
36
64
  "alarm" -> AudioManager.STREAM_ALARM
37
65
  "system" -> AudioManager.STREAM_SYSTEM
38
- else -> AudioManager.STREAM_MUSIC // default: "music"
66
+ else -> AudioManager.STREAM_MUSIC
39
67
  }
40
68
 
41
69
  // ═══════════════════════════════════════════════
42
- // NAVIGATION BAR
70
+ // BRIGHTNESS
43
71
  // ═══════════════════════════════════════════════
44
72
 
45
73
  @ReactMethod
46
- fun setNavigationBarColor(color: String) {
47
- val act = activity() ?: return
48
- act.runOnUiThread {
49
- try {
50
- act.window.navigationBarColor = Color.parseColor(color)
51
- } catch (e: Exception) {
52
- // ignore invalid color
74
+ fun setBrightness(level: Float) {
75
+ activity()?.runOnUiThread {
76
+ val lp = activity()!!.window.attributes
77
+ lp.screenBrightness = level.coerceIn(0f, 1f)
78
+ activity()!!.window.attributes = lp
79
+ }
80
+ }
81
+
82
+ @ReactMethod
83
+ fun getBrightness(promise: Promise) {
84
+ try {
85
+ val lp = activity()?.window?.attributes
86
+ if (lp != null && lp.screenBrightness >= 0f) {
87
+ promise.resolve(lp.screenBrightness.toDouble()); return
53
88
  }
89
+ val sys = Settings.System.getInt(reactContext.contentResolver, Settings.System.SCREEN_BRIGHTNESS, 128)
90
+ promise.resolve(sys / 255.0)
91
+ } catch (e: Exception) { promise.reject("BRIGHTNESS_ERROR", e.message, e) }
92
+ }
93
+
94
+ // ═══════════════════════════════════════════════
95
+ // VOLUME
96
+ // ═══════════════════════════════════════════════
97
+
98
+ private var suppressVolumeHUD = false
99
+
100
+ @ReactMethod fun setVolumeHUDVisible(visible: Boolean) { suppressVolumeHUD = !visible }
101
+
102
+ @ReactMethod
103
+ fun setVolume(level: Float, stream: String) {
104
+ try {
105
+ val am = audioManager(); val type = streamType(stream)
106
+ val vol = (level.coerceIn(0f, 1f) * am.getStreamMaxVolume(type)).toInt()
107
+ am.setStreamVolume(type, vol, if (suppressVolumeHUD) 0 else AudioManager.FLAG_SHOW_UI)
108
+ } catch (_: Exception) {}
109
+ }
110
+
111
+ @ReactMethod
112
+ fun getVolume(stream: String, promise: Promise) {
113
+ try {
114
+ val am = audioManager(); val type = streamType(stream)
115
+ val max = am.getStreamMaxVolume(type)
116
+ promise.resolve(if (max > 0) am.getStreamVolume(type).toDouble() / max else 0.0)
117
+ } catch (e: Exception) { promise.reject("VOLUME_ERROR", e.message, e) }
118
+ }
119
+
120
+ // ═══════════════════════════════════════════════
121
+ // SCREEN
122
+ // ═══════════════════════════════════════════════
123
+
124
+ @ReactMethod
125
+ fun keepScreenOn(enable: Boolean) {
126
+ activity()?.runOnUiThread {
127
+ if (enable) activity()!!.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
128
+ else activity()!!.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
54
129
  }
55
130
  }
56
131
 
57
132
  @ReactMethod
58
- fun setNavigationBarVisibility(mode: String) {
59
- val act = activity() ?: return
60
- act.runOnUiThread {
133
+ fun immersiveMode(enable: Boolean) {
134
+ activity()?.runOnUiThread {
61
135
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
62
- val controller = act.window.insetsController ?: return@runOnUiThread
63
- if (mode == "hidden") {
64
- controller.hide(android.view.WindowInsets.Type.navigationBars())
136
+ val c = activity()!!.window.insetsController ?: return@runOnUiThread
137
+ if (enable) {
138
+ c.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
139
+ c.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
65
140
  } else {
66
- controller.show(android.view.WindowInsets.Type.navigationBars())
141
+ c.show(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
67
142
  }
68
143
  } else {
69
144
  @Suppress("DEPRECATION")
70
- if (mode == "hidden") {
71
- act.window.decorView.systemUiVisibility =
72
- act.window.decorView.systemUiVisibility or
73
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
74
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
75
- } else {
76
- act.window.decorView.systemUiVisibility =
77
- act.window.decorView.systemUiVisibility and
78
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION.inv()
79
- }
145
+ activity()!!.window.decorView.systemUiVisibility = if (enable) {
146
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
147
+ View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
148
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
149
+ } else { View.SYSTEM_UI_FLAG_VISIBLE }
80
150
  }
81
151
  }
82
152
  }
83
153
 
154
+ // ═══════════════════════════════════════════════
155
+ // ORIENTATION
156
+ // ═══════════════════════════════════════════════
157
+
84
158
  @ReactMethod
85
- fun setNavigationBarButtonStyle(style: String) {
86
- val act = activity() ?: return
87
- act.runOnUiThread {
88
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
89
- val controller = act.window.insetsController ?: return@runOnUiThread
90
- controller.setSystemBarsAppearance(
91
- if (style == "dark")
92
- WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
93
- else 0,
94
- WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
95
- )
96
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
97
- @Suppress("DEPRECATION")
98
- val flags = act.window.decorView.systemUiVisibility
99
- act.window.decorView.systemUiVisibility = if (style == "dark") {
100
- flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
101
- } else {
102
- flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
103
- }
104
- }
159
+ fun setOrientation(mode: String) {
160
+ activity()?.requestedOrientation = when (mode) {
161
+ "portrait" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
162
+ "landscape" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
163
+ "landscape-left" -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
164
+ "landscape-right" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
165
+ else -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR
105
166
  }
106
167
  }
107
168
 
108
- @ReactMethod
109
- fun setNavigationBarStyle(style: String) {
110
- // "style" maps to button style + optional color hinting
111
- val act = activity() ?: return
112
- act.runOnUiThread {
113
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
114
- val controller = act.window.insetsController ?: return@runOnUiThread
115
- val useLightIcons = style == "light"
116
- controller.setSystemBarsAppearance(
117
- if (useLightIcons) 0
118
- else WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS,
119
- WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS
120
- )
121
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
122
- @Suppress("DEPRECATION")
123
- val flags = act.window.decorView.systemUiVisibility
124
- act.window.decorView.systemUiVisibility = when (style) {
125
- "dark" -> flags or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
126
- else -> flags and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv()
127
- }
169
+ // ═══════════════════════════════════════════════
170
+ // 🆕 NETWORK INFO
171
+ // ═══════════════════════════════════════════════
172
+
173
+ private var networkCallback: ConnectivityManager.NetworkCallback? = null
174
+
175
+ private fun buildNetworkMap(): WritableMap {
176
+ val cm = connectivityManager()
177
+ val map = Arguments.createMap()
178
+
179
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
180
+ val net = cm.activeNetwork
181
+ val caps = cm.getNetworkCapabilities(net)
182
+ val connected = caps != null
183
+
184
+ map.putBoolean("isConnected", connected)
185
+
186
+ val type = when {
187
+ caps == null -> "none"
188
+ caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
189
+ caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
190
+ caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet"
191
+ else -> "unknown"
128
192
  }
193
+ map.putString("type", type)
194
+
195
+ // Cellular generation
196
+ val cellGen: String? = if (type == "cellular") {
197
+ val am = audioManager() // not audio — reuse context trick; use TelephonyManager instead
198
+ null // placeholder; TelephonyManager requires READ_PHONE_STATE permission
199
+ } else null
200
+ if (cellGen != null) map.putString("cellularGeneration", cellGen)
201
+ else map.putNull("cellularGeneration")
202
+
203
+ // SSID (requires ACCESS_FINE_LOCATION on Android 10+)
204
+ map.putNull("ssid")
205
+
206
+ } else {
207
+ @Suppress("DEPRECATION")
208
+ val info = cm.activeNetworkInfo
209
+ map.putBoolean("isConnected", info?.isConnected == true)
210
+ map.putString("type", when (info?.type) {
211
+ ConnectivityManager.TYPE_WIFI -> "wifi"
212
+ ConnectivityManager.TYPE_MOBILE -> "cellular"
213
+ ConnectivityManager.TYPE_ETHERNET -> "ethernet"
214
+ else -> if (info?.isConnected == true) "unknown" else "none"
215
+ })
216
+ map.putNull("cellularGeneration")
217
+ map.putNull("ssid")
129
218
  }
219
+
220
+ // Airplane mode
221
+ val airplaneMode = Settings.Global.getInt(
222
+ reactContext.contentResolver, Settings.Global.AIRPLANE_MODE_ON, 0
223
+ ) != 0
224
+ map.putBoolean("isAirplaneMode", airplaneMode)
225
+
226
+ return map
130
227
  }
131
228
 
132
229
  @ReactMethod
133
- fun setNavigationBarBehavior(behavior: String) {
134
- val act = activity() ?: return
135
- act.runOnUiThread {
136
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
137
- val controller = act.window.insetsController ?: return@runOnUiThread
138
- // BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE is the only non-deprecated
139
- // constant on API 30+. "inset-swipe" and "inset-touch" both map to
140
- // the default persistent behavior (bars are always visible after show).
141
- controller.systemBarsBehavior = when (behavior) {
142
- "overlay-swipe" ->
143
- WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
144
- else ->
145
- // "inset-swipe" / "inset-touch": bars stay visible after swipe
146
- @Suppress("DEPRECATION")
147
- WindowInsetsController.BEHAVIOR_SHOW_BARS_BY_SWIPE
148
- }
230
+ fun getNetworkInfo(promise: Promise) {
231
+ try { promise.resolve(buildNetworkMap()) }
232
+ catch (e: Exception) { promise.reject("NETWORK_ERROR", e.message, e) }
233
+ }
234
+
235
+ @ReactMethod
236
+ fun startNetworkListener() {
237
+ if (networkCallback != null) return
238
+ val cb = object : ConnectivityManager.NetworkCallback() {
239
+ override fun onAvailable(network: Network) { emit("SystemBar_NetworkChange", buildNetworkMap()) }
240
+ override fun onLost(network: Network) { emit("SystemBar_NetworkChange", buildNetworkMap()) }
241
+ override fun onCapabilitiesChanged(n: Network, c: NetworkCapabilities) {
242
+ emit("SystemBar_NetworkChange", buildNetworkMap())
149
243
  }
150
- // API < 30: behavior is controlled via IMMERSIVE_STICKY flag
244
+ }
245
+ networkCallback = cb
246
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
247
+ connectivityManager().registerDefaultNetworkCallback(cb)
248
+ } else {
249
+ connectivityManager().registerNetworkCallback(NetworkRequest.Builder().build(), cb)
151
250
  }
152
251
  }
153
252
 
253
+ @ReactMethod
254
+ fun stopNetworkListener() {
255
+ networkCallback?.let { connectivityManager().unregisterNetworkCallback(it) }
256
+ networkCallback = null
257
+ }
258
+
154
259
  // ═══════════════════════════════════════════════
155
- // STATUS BAR
260
+ // 🆕 BATTERY
156
261
  // ═══════════════════════════════════════════════
157
262
 
158
- @ReactMethod
159
- fun setStatusBarColor(color: String) {
160
- val act = activity() ?: return
161
- act.runOnUiThread {
162
- try {
163
- act.window.statusBarColor = Color.parseColor(color)
164
- } catch (e: Exception) {
165
- // ignore invalid color
166
- }
263
+ private var batteryReceiver: BroadcastReceiver? = null
264
+
265
+ private fun buildBatteryMap(): WritableMap {
266
+ val bm = batteryManager()
267
+ val map = Arguments.createMap()
268
+ val level = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
269
+ val status = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_STATUS)
270
+
271
+ val state = when (status) {
272
+ BatteryManager.BATTERY_STATUS_CHARGING -> "charging"
273
+ BatteryManager.BATTERY_STATUS_FULL -> "full"
274
+ BatteryManager.BATTERY_STATUS_DISCHARGING -> "discharging"
275
+ BatteryManager.BATTERY_STATUS_NOT_CHARGING -> "discharging"
276
+ else -> "unknown"
167
277
  }
278
+
279
+ val isCharging = state == "charging" || state == "full"
280
+
281
+ map.putInt("level", level)
282
+ map.putString("state", state)
283
+ map.putBoolean("isCharging", isCharging)
284
+ map.putBoolean("isLow", level <= 20 && !isCharging)
285
+ return map
168
286
  }
169
287
 
170
288
  @ReactMethod
171
- fun setStatusBarStyle(style: String) {
172
- val act = activity() ?: return
173
- act.runOnUiThread {
174
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
175
- val controller = act.window.insetsController ?: return@runOnUiThread
176
- controller.setSystemBarsAppearance(
177
- if (style == "dark")
178
- WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
179
- else 0,
180
- WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
181
- )
182
- } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
183
- @Suppress("DEPRECATION")
184
- val flags = act.window.decorView.systemUiVisibility
185
- act.window.decorView.systemUiVisibility = if (style == "dark") {
186
- flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
187
- } else {
188
- flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
189
- }
190
- }
191
- }
289
+ fun getBatteryInfo(promise: Promise) {
290
+ try { promise.resolve(buildBatteryMap()) }
291
+ catch (e: Exception) { promise.reject("BATTERY_ERROR", e.message, e) }
192
292
  }
193
293
 
194
294
  @ReactMethod
195
- fun setStatusBarVisibility(visible: Boolean) {
196
- val act = activity() ?: return
197
- act.runOnUiThread {
198
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
199
- val controller = act.window.insetsController ?: return@runOnUiThread
200
- if (visible) {
201
- controller.show(android.view.WindowInsets.Type.statusBars())
202
- } else {
203
- controller.hide(android.view.WindowInsets.Type.statusBars())
204
- }
205
- } else {
206
- @Suppress("DEPRECATION")
207
- if (visible) {
208
- act.window.decorView.systemUiVisibility =
209
- act.window.decorView.systemUiVisibility and
210
- View.SYSTEM_UI_FLAG_FULLSCREEN.inv()
211
- } else {
212
- act.window.decorView.systemUiVisibility =
213
- act.window.decorView.systemUiVisibility or
214
- View.SYSTEM_UI_FLAG_FULLSCREEN
215
- }
295
+ fun startBatteryListener() {
296
+ if (batteryReceiver != null) return
297
+ val receiver = object : BroadcastReceiver() {
298
+ override fun onReceive(ctx: Context?, intent: Intent?) {
299
+ emit("SystemBar_BatteryChange", buildBatteryMap())
216
300
  }
217
301
  }
302
+ batteryReceiver = receiver
303
+ val filter = IntentFilter().apply {
304
+ addAction(Intent.ACTION_BATTERY_CHANGED)
305
+ addAction(Intent.ACTION_BATTERY_LOW)
306
+ addAction(Intent.ACTION_BATTERY_OKAY)
307
+ addAction(Intent.ACTION_POWER_CONNECTED)
308
+ addAction(Intent.ACTION_POWER_DISCONNECTED)
309
+ }
310
+ reactContext.registerReceiver(receiver, filter)
311
+ }
312
+
313
+ @ReactMethod
314
+ fun stopBatteryListener() {
315
+ batteryReceiver?.let { reactContext.unregisterReceiver(it) }
316
+ batteryReceiver = null
218
317
  }
219
318
 
220
319
  // ═══════════════════════════════════════════════
221
- // BRIGHTNESS
320
+ // 🆕 HAPTIC FEEDBACK
222
321
  // ═══════════════════════════════════════════════
223
322
 
224
323
  @ReactMethod
225
- fun setBrightness(level: Float) {
226
- val act = activity() ?: return
227
- act.runOnUiThread {
228
- val lp = act.window.attributes
229
- lp.screenBrightness = level.coerceIn(0f, 1f)
230
- act.window.attributes = lp
231
- }
232
- }
324
+ fun haptic(pattern: String) {
325
+ val vib = vibrator()
326
+ if (!vib.hasVibrator()) return
233
327
 
234
- @ReactMethod
235
- fun getBrightness(promise: Promise) {
236
- try {
237
- val act = activity()
238
- if (act != null) {
239
- val lp = act.window.attributes
240
- // screenBrightness == -1 means "use system default"
241
- if (lp.screenBrightness >= 0f) {
242
- promise.resolve(lp.screenBrightness.toDouble())
243
- return
244
- }
328
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
329
+ val effect = when (pattern) {
330
+ "light" -> VibrationEffect.createOneShot(20, 80)
331
+ "medium" -> VibrationEffect.createOneShot(40, 128)
332
+ "heavy" -> VibrationEffect.createOneShot(60, 255)
333
+ "success" -> VibrationEffect.createWaveform(longArrayOf(0, 30, 60, 30), intArrayOf(0, 180, 0, 255), -1)
334
+ "warning" -> VibrationEffect.createWaveform(longArrayOf(0, 50, 80, 50), intArrayOf(0, 200, 0, 200), -1)
335
+ "error" -> VibrationEffect.createWaveform(longArrayOf(0, 40, 40, 40, 40, 40), intArrayOf(0, 255, 0, 255, 0, 255), -1)
336
+ "selection" -> VibrationEffect.createOneShot(10, 60)
337
+ else -> VibrationEffect.createOneShot(30, 128)
338
+ }
339
+ vib.vibrate(effect)
340
+ } else {
341
+ @Suppress("DEPRECATION")
342
+ when (pattern) {
343
+ "light" -> vib.vibrate(20)
344
+ "medium" -> vib.vibrate(40)
345
+ "heavy" -> vib.vibrate(60)
346
+ "success" -> vib.vibrate(longArrayOf(0, 30, 60, 30), -1)
347
+ "warning" -> vib.vibrate(longArrayOf(0, 50, 80, 50), -1)
348
+ "error" -> vib.vibrate(longArrayOf(0, 40, 40, 40, 40, 40), -1)
349
+ "selection" -> vib.vibrate(10)
350
+ else -> vib.vibrate(30)
245
351
  }
246
- // Fall back to system brightness setting (0–255 → 0.0–1.0)
247
- val sysBrightness = Settings.System.getInt(
248
- reactContext.contentResolver,
249
- Settings.System.SCREEN_BRIGHTNESS,
250
- 128
251
- )
252
- promise.resolve(sysBrightness / 255.0)
253
- } catch (e: Exception) {
254
- promise.reject("BRIGHTNESS_ERROR", e.message, e)
255
352
  }
256
353
  }
257
354
 
258
355
  // ═══════════════════════════════════════════════
259
- // VOLUME
356
+ // 🆕 SCREENCAST
260
357
  // ═══════════════════════════════════════════════
261
358
 
262
- private var suppressVolumeHUD = false
359
+ private var displayListener: DisplayManager.DisplayListener? = null
360
+
361
+ private fun buildScreencastMap(): WritableMap {
362
+ val dm = displayManager()
363
+ val map = Arguments.createMap()
364
+ val presentations = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION)
365
+ val isCasting = presentations.isNotEmpty()
366
+ map.putBoolean("isCasting", isCasting)
367
+ if (isCasting) map.putString("displayName", presentations[0].name)
368
+ else map.putNull("displayName")
369
+ return map
370
+ }
263
371
 
264
372
  @ReactMethod
265
- fun setVolumeHUDVisible(visible: Boolean) {
266
- suppressVolumeHUD = !visible
373
+ fun getScreencastInfo(promise: Promise) {
374
+ try { promise.resolve(buildScreencastMap()) }
375
+ catch (e: Exception) { promise.reject("SCREENCAST_ERROR", e.message, e) }
267
376
  }
268
377
 
269
378
  @ReactMethod
270
- fun setVolume(level: Float, stream: String) {
271
- try {
272
- val am = audioManager()
273
- val streamType = streamType(stream)
274
- val max = am.getStreamMaxVolume(streamType)
275
- val vol = (level.coerceIn(0f, 1f) * max).toInt()
276
- val flags = if (suppressVolumeHUD) 0 else AudioManager.FLAG_SHOW_UI
277
- am.setStreamVolume(streamType, vol, flags)
278
- } catch (e: Exception) {
279
- // ignore
379
+ fun startScreencastListener() {
380
+ if (displayListener != null) return
381
+ val listener = object : DisplayManager.DisplayListener {
382
+ override fun onDisplayAdded(id: Int) { emit("SystemBar_ScreencastChange", buildScreencastMap()) }
383
+ override fun onDisplayRemoved(id: Int) { emit("SystemBar_ScreencastChange", buildScreencastMap()) }
384
+ override fun onDisplayChanged(id: Int) { emit("SystemBar_ScreencastChange", buildScreencastMap()) }
280
385
  }
386
+ displayListener = listener
387
+ displayManager().registerDisplayListener(listener, null)
281
388
  }
282
389
 
283
390
  @ReactMethod
284
- fun getVolume(stream: String, promise: Promise) {
285
- try {
286
- val am = audioManager()
287
- val streamType = streamType(stream)
288
- val current = am.getStreamVolume(streamType)
289
- val max = am.getStreamMaxVolume(streamType)
290
- promise.resolve(if (max > 0) current.toDouble() / max else 0.0)
291
- } catch (e: Exception) {
292
- promise.reject("VOLUME_ERROR", e.message, e)
391
+ fun stopScreencastListener() {
392
+ displayListener?.let { displayManager().unregisterDisplayListener(it) }
393
+ displayListener = null
394
+ }
395
+
396
+ @ReactMethod
397
+ fun setSecureScreen(enable: Boolean) {
398
+ activity()?.runOnUiThread {
399
+ if (enable) activity()!!.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
400
+ else activity()!!.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
293
401
  }
294
402
  }
295
403
 
296
404
  // ═══════════════════════════════════════════════
297
- // SCREEN
405
+ // 🆕 FONT SCALE
298
406
  // ═══════════════════════════════════════════════
299
407
 
408
+ private var fontScaleReceiver: BroadcastReceiver? = null
409
+
410
+ private fun buildFontScaleMap(): WritableMap {
411
+ val map = Arguments.createMap()
412
+ val cfg = reactContext.resources.configuration
413
+ val dm = reactContext.resources.displayMetrics
414
+ map.putDouble("fontScale", cfg.fontScale.toDouble())
415
+ map.putDouble("density", dm.density.toDouble())
416
+ return map
417
+ }
418
+
300
419
  @ReactMethod
301
- fun keepScreenOn(enable: Boolean) {
302
- val act = activity() ?: return
303
- act.runOnUiThread {
304
- if (enable) {
305
- act.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
306
- } else {
307
- act.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
308
- }
309
- }
420
+ fun getFontScaleInfo(promise: Promise) {
421
+ try { promise.resolve(buildFontScaleMap()) }
422
+ catch (e: Exception) { promise.reject("FONT_SCALE_ERROR", e.message, e) }
310
423
  }
311
424
 
312
425
  @ReactMethod
313
- fun immersiveMode(enable: Boolean) {
314
- val act = activity() ?: return
315
- act.runOnUiThread {
316
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
317
- val controller = act.window.insetsController ?: return@runOnUiThread
318
- if (enable) {
319
- controller.hide(
320
- android.view.WindowInsets.Type.statusBars() or
321
- android.view.WindowInsets.Type.navigationBars()
322
- )
323
- controller.systemBarsBehavior =
324
- WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
325
- } else {
326
- controller.show(
327
- android.view.WindowInsets.Type.statusBars() or
328
- android.view.WindowInsets.Type.navigationBars()
329
- )
330
- }
331
- } else {
332
- @Suppress("DEPRECATION")
333
- if (enable) {
334
- act.window.decorView.systemUiVisibility =
335
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
336
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
337
- View.SYSTEM_UI_FLAG_FULLSCREEN or
338
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
339
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
340
- } else {
341
- act.window.decorView.systemUiVisibility =
342
- View.SYSTEM_UI_FLAG_VISIBLE
343
- }
426
+ fun startFontScaleListener() {
427
+ if (fontScaleReceiver != null) return
428
+ val receiver = object : BroadcastReceiver() {
429
+ override fun onReceive(ctx: Context?, intent: Intent?) {
430
+ emit("SystemBar_FontScaleChange", buildFontScaleMap())
344
431
  }
345
432
  }
433
+ fontScaleReceiver = receiver
434
+ reactContext.registerReceiver(receiver, IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED))
346
435
  }
347
436
 
348
- // ═══════════════════════════════════════════════
349
- // ORIENTATION
350
- // ═══════════════════════════════════════════════
351
-
352
437
  @ReactMethod
353
- fun setOrientation(mode: String) {
354
- val act = activity() ?: return
355
- act.requestedOrientation = when (mode) {
356
- "portrait" -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
357
- "landscape" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
358
- "landscape-left" -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
359
- "landscape-right" -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
360
- else -> ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR // "auto"
361
- }
438
+ fun stopFontScaleListener() {
439
+ fontScaleReceiver?.let { reactContext.unregisterReceiver(it) }
440
+ fontScaleReceiver = null
441
+ }
442
+
443
+ // ── Cleanup on host destroy ──────────────────
444
+ override fun onCatalystInstanceDestroy() {
445
+ stopNetworkListener()
446
+ stopBatteryListener()
447
+ stopScreencastListener()
448
+ stopFontScaleListener()
362
449
  }
363
450
  }