-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: improve wifi state management #220
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
package com.comapeo | ||
|
||
import android.content.Context | ||
import android.net.ConnectivityManager | ||
import android.net.LinkProperties | ||
import android.net.Network | ||
import android.net.NetworkCapabilities | ||
import android.net.NetworkRequest | ||
import android.net.wifi.WifiManager | ||
import android.net.wifi.WifiInfo | ||
import android.os.Build | ||
import com.facebook.react.bridge.Arguments | ||
import com.facebook.react.bridge.ReactApplicationContext | ||
import com.facebook.react.bridge.ReactContextBaseJavaModule | ||
import com.facebook.react.bridge.ReactMethod | ||
import com.facebook.react.bridge.WritableMap | ||
import com.facebook.react.modules.core.DeviceEventManagerModule | ||
|
||
class WifiModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { | ||
private val context = reactContext.applicationContext | ||
private val connectivityManager = | ||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | ||
private val wifiManager = context.getSystemService(Context.WIFI_SERVICE) as? WifiManager | ||
|
||
private val networkCallback = | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { | ||
override fun onAvailable(network: Network) { | ||
emit(network) | ||
} | ||
|
||
override fun onLost(network: Network) { | ||
emit(null) | ||
} | ||
|
||
override fun onCapabilitiesChanged( | ||
network: Network, | ||
networkCapabilities: NetworkCapabilities | ||
) { | ||
emit(network, networkCapabilities = networkCapabilities) | ||
} | ||
|
||
override fun onLinkPropertiesChanged( | ||
network: Network, | ||
linkProperties: LinkProperties | ||
) { | ||
emit(network, linkProperties = linkProperties) | ||
} | ||
} | ||
} else { | ||
object : ConnectivityManager.NetworkCallback() { | ||
override fun onAvailable(network: Network) { | ||
emit(network) | ||
} | ||
|
||
override fun onLost(network: Network) { | ||
emit(null) | ||
} | ||
|
||
override fun onCapabilitiesChanged( | ||
network: Network, | ||
networkCapabilities: NetworkCapabilities | ||
) { | ||
emit(network, networkCapabilities = networkCapabilities) | ||
} | ||
|
||
override fun onLinkPropertiesChanged( | ||
network: Network, | ||
linkProperties: LinkProperties | ||
) { | ||
emit(network, linkProperties = linkProperties) | ||
} | ||
} | ||
} | ||
|
||
private var numberOfListeners = 0 | ||
|
||
override fun getName() = "WifiModule" | ||
|
||
@ReactMethod | ||
fun addListener(eventName: String) { | ||
if (numberOfListeners == 0) { | ||
startListening() | ||
} | ||
numberOfListeners++ | ||
} | ||
|
||
@ReactMethod | ||
fun removeListeners(count: Int) { | ||
numberOfListeners -= count | ||
if (numberOfListeners == 0) { | ||
stopListening() | ||
} | ||
} | ||
Comment on lines
+80
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the relevant React Native docs for sending events from a native module. |
||
|
||
private fun startListening() { | ||
val request = NetworkRequest.Builder() | ||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI) | ||
.build() | ||
connectivityManager.registerNetworkCallback(request, networkCallback) | ||
} | ||
|
||
private fun stopListening() { | ||
connectivityManager.unregisterNetworkCallback(networkCallback) | ||
} | ||
|
||
private fun getState( | ||
network: Network?, | ||
networkCapabilities: NetworkCapabilities?, | ||
linkProperties: LinkProperties? | ||
): WritableMap { | ||
val result = Arguments.createMap() | ||
result.putString("ssid", getSsid(network, networkCapabilities)) | ||
result.putString("ipAddress", getIpAddress(network, linkProperties)) | ||
return result | ||
} | ||
|
||
private fun emit( | ||
network: Network?, | ||
networkCapabilities: NetworkCapabilities? = null, | ||
linkProperties: LinkProperties? = null | ||
) { | ||
reactApplicationContext | ||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) | ||
.emit("change", getState(network, networkCapabilities, linkProperties)) | ||
} | ||
|
||
private fun getSsid(network: Network?, networkCapabilities: NetworkCapabilities?): String? { | ||
val capabilities: NetworkCapabilities? = networkCapabilities | ||
?: try { | ||
connectivityManager.getNetworkCapabilities(network) | ||
} catch (_: SecurityException) { | ||
// Old Android versions can throw errors here. See | ||
// <https://android.googlesource.com/platform/frameworks/base/+/249be21013e389837f5b2beb7d36890b25ecfaaf%5E%21/>. | ||
null | ||
} | ||
val wifiInfoFromCapabilities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||
capabilities?.transportInfo as? WifiInfo | ||
} else { | ||
null | ||
} | ||
// We use the deprecated `WifiManager#connectionInfo` here. Not ideal, but it is required | ||
// on older Android versions. | ||
return getSsid(wifiInfoFromCapabilities) ?: getSsid(wifiManager?.connectionInfo) | ||
} | ||
|
||
private fun getSsid(wifiInfo: WifiInfo?): String? { | ||
var result = wifiInfo?.ssid ?: return null | ||
|
||
// "If the SSID can be decoded as UTF-8, it will be returned surrounded by | ||
// double quotation marks. Otherwise, it is returned as a string of hex | ||
// digits. [...] Prior to `Build.VERSION_CODES.JELLY_BEAN_MR1`, this method | ||
// always returned the SSID with no quotes around it." | ||
// <https://developer.android.com/reference/android/net/wifi/WifiInfo#getSSID()> | ||
if (result.startsWith('"') && result.endsWith('"')) { | ||
result = result.substring(1, result.length - 1) | ||
} | ||
|
||
// "The SSID may be `WifiManager#UNKNOWN_SSID`, if there is no network | ||
// currently connected or if the caller has insufficient permissions to | ||
// access the SSID." | ||
if (result == WifiManager.UNKNOWN_SSID) { | ||
return null | ||
} | ||
|
||
return result | ||
} | ||
|
||
private fun getIpAddress(network: Network?, linkProperties: LinkProperties?): String? { | ||
val properties: LinkProperties? = linkProperties | ||
?: try { | ||
connectivityManager.getLinkProperties(network) | ||
} catch (_: SecurityException) { | ||
// See SecurityException catch above for an explanation. | ||
null | ||
} | ||
|
||
return properties | ||
?.linkAddresses | ||
?.firstOrNull { !it.address.isLoopbackAddress } | ||
?.toString() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.comapeo | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the relevant React Native docs. |
||
|
||
import android.view.View | ||
import com.facebook.react.ReactPackage | ||
import com.facebook.react.bridge.NativeModule | ||
import com.facebook.react.bridge.ReactApplicationContext | ||
import com.facebook.react.uimanager.ReactShadowNode | ||
import com.facebook.react.uimanager.ViewManager | ||
|
||
class WifiPackage : ReactPackage { | ||
override fun createViewManagers(reactApplicationContext: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> = | ||
mutableListOf() | ||
|
||
override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> = | ||
listOf(WifiModule(reactContext)).toMutableList() | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import {useEffect, useState} from 'react'; | ||
import {NativeModules, NativeEventEmitter} from 'react-native'; | ||
|
||
const {WifiModule} = NativeModules; | ||
|
||
type WifiState = Readonly<{ | ||
ssid: null | string; | ||
ipAddress: null | string; | ||
}>; | ||
|
||
const isNullOrString = (value: unknown): value is null | string => | ||
value === null || typeof value === 'string'; | ||
|
||
const parseState = (value: unknown): WifiState => { | ||
if ( | ||
value && | ||
typeof value === 'object' && | ||
'ssid' in value && | ||
'ipAddress' in value && | ||
isNullOrString(value.ssid) && | ||
isNullOrString(value.ipAddress) | ||
) { | ||
return {ssid: value.ssid, ipAddress: value.ipAddress}; | ||
} | ||
throw new Error('Invalid wifi state from native module'); | ||
}; | ||
|
||
export const useWifiState = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs a request for the current state when the hook is first initialized - if there is no |
||
const [result, setResult] = useState<WifiState>({ | ||
ssid: null, | ||
ipAddress: null, | ||
}); | ||
|
||
useEffect(() => { | ||
const emitter = new NativeEventEmitter(WifiModule); | ||
const listener = emitter.addListener('change', (rawState: unknown) => { | ||
setResult(parseState(rawState)); | ||
}); | ||
return () => { | ||
listener.remove(); | ||
}; | ||
}, []); | ||
|
||
return result; | ||
}; |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the relevant React Native docs.