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.
- package/android/src/main/java/com/systembar/SystemBarModule.kt +334 -247
- package/android/src/main/java/com/systembar/SystemBarPackage.kt +2 -1
- package/index.ts +9 -0
- package/ios/SystemBarModule.m +32 -12
- package/ios/SystemBarModule.swift +241 -83
- package/lib/index.d.ts +4 -0
- package/lib/index.js +29 -0
- package/lib/specs/NativeSystemBar.d.ts +21 -0
- package/lib/specs/NativeSystemBar.js +8 -0
- package/lib/src/SystemBar.d.ts +73 -0
- package/lib/src/SystemBar.js +292 -0
- package/lib/src/types.d.ts +44 -0
- package/lib/src/types.js +5 -0
- package/lib/src/useSystemBar.d.ts +49 -0
- package/lib/src/useSystemBar.js +194 -0
- package/package.json +14 -4
- package/rn-system-bar.podspec +1 -1
- package/src/SystemBar.ts +215 -141
- package/src/types.ts +70 -43
- package/src/useSystemBar.ts +191 -0
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────
|
|
2
|
-
// rn-system-bar · SystemBarModule.kt
|
|
3
|
-
//
|
|
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.
|
|
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
|
|
31
|
-
reactContext
|
|
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
|
|
66
|
+
else -> AudioManager.STREAM_MUSIC
|
|
39
67
|
}
|
|
40
68
|
|
|
41
69
|
// ═══════════════════════════════════════════════
|
|
42
|
-
//
|
|
70
|
+
// BRIGHTNESS
|
|
43
71
|
// ═══════════════════════════════════════════════
|
|
44
72
|
|
|
45
73
|
@ReactMethod
|
|
46
|
-
fun
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
act.runOnUiThread {
|
|
133
|
+
fun immersiveMode(enable: Boolean) {
|
|
134
|
+
activity()?.runOnUiThread {
|
|
61
135
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
62
|
-
val
|
|
63
|
-
if (
|
|
64
|
-
|
|
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
|
-
|
|
141
|
+
c.show(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
|
67
142
|
}
|
|
68
143
|
} else {
|
|
69
144
|
@Suppress("DEPRECATION")
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
260
|
+
// 🆕 BATTERY
|
|
156
261
|
// ═══════════════════════════════════════════════
|
|
157
262
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
//
|
|
320
|
+
// 🆕 HAPTIC FEEDBACK
|
|
222
321
|
// ═══════════════════════════════════════════════
|
|
223
322
|
|
|
224
323
|
@ReactMethod
|
|
225
|
-
fun
|
|
226
|
-
val
|
|
227
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
//
|
|
356
|
+
// 🆕 SCREENCAST
|
|
260
357
|
// ═══════════════════════════════════════════════
|
|
261
358
|
|
|
262
|
-
private var
|
|
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
|
|
266
|
-
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
//
|
|
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
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
}
|