diff --git a/README.md b/README.md index 7a679d1..7980df5 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,8 @@ The following methods work only on iOS ### `connectToProtectedSSIDPrefix(SSIDPrefix: string, password: string, isWep: boolean): Promise` +### `connectToProtectedSSIDPrefixOnce(SSIDPrefix: string, password: string, isWep: boolean, joinOnce: boolean): Promise` + Use this function when you want to match a known SSID prefix, but don’t have a full SSID. If the system finds multiple Wi-Fi networks whose SSID string matches the given prefix, it selects the network with the greatest signal strength. #### SSIDPrefix diff --git a/android/src/main/java/com/reactlibrary/rnwifi/RNWifiModule.java b/android/src/main/java/com/reactlibrary/rnwifi/RNWifiModule.java index 4f4402e..68c13f8 100644 --- a/android/src/main/java/com/reactlibrary/rnwifi/RNWifiModule.java +++ b/android/src/main/java/com/reactlibrary/rnwifi/RNWifiModule.java @@ -54,11 +54,13 @@ import java.util.List; public class RNWifiModule extends ReactContextBaseJavaModule { + private Network joinedNetwork; private final WifiManager wifi; private final ReactApplicationContext context; private static String TAG = "RNWifiModule"; private static final int TIMEOUT_MILLIS = 15000; + private static final int TIMEOUT_REMOVE_MILLIS = 10000; RNWifiModule(ReactApplicationContext context) { super(context); @@ -138,6 +140,14 @@ public void forceWifiUsageWithOptions(final boolean useWifi, @Nullable final Rea } if (useWifi) { + // thanks to https://github.com/flutternetwork/WiFiFlutter/pull/309 + // SDK-31 If not previously in a disconnected state, select the joinedNetwork to ensure + // the correct network is used for communications, else fallback to network manager network. + // https://developer.android.com/about/versions/12/behavior-changes-12#concurrent-connections + if (joinedNetwork != null) { + selectNetwork(joinedNetwork, connectivityManager); + promise.resolve(null); + } else { final NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI); @@ -149,17 +159,14 @@ public void forceWifiUsageWithOptions(final boolean useWifi, @Nullable final Rea @Override public void onAvailable(@NonNull final Network network) { super.onAvailable(network); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - connectivityManager.bindProcessToNetwork(network); - } else { - ConnectivityManager.setProcessDefaultNetwork(network); - } + selectNetwork(network, connectivityManager); connectivityManager.unregisterNetworkCallback(this); promise.resolve(null); } }); + } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { connectivityManager.bindProcessToNetwork(null); @@ -237,7 +244,7 @@ public void connectToProtectedSSID(@NonNull final String SSID, @NonNull final St this.removeWifiNetwork(SSID, promise, () -> { connectToWifiDirectly(SSID, password, isHidden, TIMEOUT_MILLIS, promise); - }); + }, TIMEOUT_REMOVE_MILLIS); } @@ -265,11 +272,10 @@ public void connectToProtectedWifiSSID(@NonNull ReadableMap options, final Promi boolean isHidden = options.hasKey("isHidden") && options.getBoolean("isHidden"); int secondsTimeout = options.hasKey("timeout") ? options.getInt("timeout") * 1000 : TIMEOUT_MILLIS; - this.removeWifiNetwork(ssid, promise, () -> { assert ssid != null; connectToWifiDirectly(ssid, password, isHidden, secondsTimeout, promise); - }); + }, TIMEOUT_REMOVE_MILLIS); } @@ -300,14 +306,28 @@ public void connectionStatus(final Promise promise) { */ @ReactMethod public void disconnect(final Promise promise) { + final int timeout = TIMEOUT_REMOVE_MILLIS; + final Handler timeoutHandler = new Handler(Looper.getMainLooper()); + final Runnable timeoutRunnable = () -> { + promise.reject(ConnectErrorCodes.timeoutOccurred.toString(), "Connection timeout"); + if (isAndroidTenOrLater()) { + DisconnectCallbackHolder.getInstance().unbindProcessFromNetwork(); + DisconnectCallbackHolder.getInstance().disconnect(); + } + }; + + timeoutHandler.postDelayed(timeoutRunnable, timeout); + WifiUtils.withContext(this.context).disconnect(new DisconnectionSuccessListener() { @Override public void success() { + timeoutHandler.removeCallbacks(timeoutRunnable); promise.resolve(true); } @Override public void failed(@NonNull DisconnectionErrorCode errorCode) { + timeoutHandler.removeCallbacks(timeoutRunnable); switch (errorCode) { case COULD_NOT_GET_WIFI_MANAGER: { promise.reject(DisconnectErrorCodes.couldNotGetWifiManager.toString(), "Could not get WifiManager."); @@ -391,20 +411,33 @@ public void getIP(final Promise promise) { */ @ReactMethod public void isRemoveWifiNetwork(final String SSID, final Promise promise) { - removeWifiNetwork(SSID, promise, null); + removeWifiNetwork(SSID, promise, null, TIMEOUT_REMOVE_MILLIS); } - private void removeWifiNetwork(final String SSID, final Promise promise, final Runnable onSuccess) { + private void removeWifiNetwork(final String SSID, final Promise promise, final Runnable onSuccess, final int timeout) { final boolean locationPermissionGranted = PermissionUtils.isLocationPermissionGranted(context); if (!locationPermissionGranted) { promise.reject(IsRemoveWifiNetworkErrorCodes.locationPermissionMissing.toString(), "Location permission (ACCESS_FINE_LOCATION) is not granted"); return; } + final Handler timeoutHandler = new Handler(Looper.getMainLooper()); + final Runnable timeoutRunnable = () -> { + promise.reject(ConnectErrorCodes.timeoutOccurred.toString(), "Connection timeout"); + if (isAndroidTenOrLater()) { + DisconnectCallbackHolder.getInstance().unbindProcessFromNetwork(); + DisconnectCallbackHolder.getInstance().disconnect(); + } + }; + + timeoutHandler.postDelayed(timeoutRunnable, timeout); + WifiUtils.withContext(this.context) .remove(SSID, new RemoveSuccessListener() { @Override public void success() { + timeoutHandler.removeCallbacks(timeoutRunnable); + joinedNetwork = null; if (onSuccess != null) { onSuccess.run(); return; @@ -414,6 +447,7 @@ public void success() { @Override public void failed(@NonNull RemoveErrorCode errorCode) { + timeoutHandler.removeCallbacks(timeoutRunnable); switch (errorCode) { case COULD_NOT_GET_WIFI_MANAGER: { promise.reject(IsRemoveWifiNetworkErrorCodes.couldNotGetWifiManager.toString(), "Could not get WifiManager."); @@ -456,7 +490,7 @@ public void reScanAndLoadWifiList(final Promise promise) { private void connectToWifiDirectly(@NonNull final String SSID, @NonNull final String password, final boolean isHidden, final int timeout, final Promise promise) { if (isAndroidTenOrLater()) { - connectAndroidQ(SSID, password, isHidden,timeout, promise); + connectAndroidQ(SSID, password, isHidden, timeout, promise); } else { connectPreAndroidQ(SSID, password, promise); } @@ -492,6 +526,14 @@ private void connectPreAndroidQ(@NonNull final String SSID, @NonNull final Strin promise.resolve("connected"); } + private boolean selectNetwork(final Network network, final ConnectivityManager manager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return manager.bindProcessToNetwork(network); + } else { + return ConnectivityManager.setProcessDefaultNetwork(network); + } + } + @RequiresApi(api = Build.VERSION_CODES.Q) private void connectAndroidQ(@NonNull final String SSID, @NonNull final String password, final boolean isHidden, final int timeout, final Promise promise) { WifiNetworkSpecifier.Builder wifiNetworkSpecifier = new WifiNetworkSpecifier.Builder() @@ -504,12 +546,19 @@ private void connectAndroidQ(@NonNull final String SSID, @NonNull final String p NetworkRequest nr = new NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) - .setNetworkSpecifier(wifiNetworkSpecifier.build()) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + //.addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + .setNetworkSpecifier(wifiNetworkSpecifier.build()) .build(); + // cleanup previous connections just in case + DisconnectCallbackHolder.getInstance().disconnect(); + + joinedNetwork = null; + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - + final Handler timeoutHandler = new Handler(Looper.getMainLooper()); final Runnable timeoutRunnable = () -> { promise.reject(ConnectErrorCodes.timeoutOccurred.toString(), "Connection timeout"); @@ -524,8 +573,9 @@ private void connectAndroidQ(@NonNull final String SSID, @NonNull final String p public void onAvailable(@NonNull Network network) { super.onAvailable(network); timeoutHandler.removeCallbacks(timeoutRunnable); + joinedNetwork = network; DisconnectCallbackHolder.getInstance().bindProcessToNetwork(network); - connectivityManager.setNetworkPreference(ConnectivityManager.DEFAULT_NETWORK_PREFERENCE); + //connectivityManager.setNetworkPreference(ConnectivityManager.DEFAULT_NETWORK_PREFERENCE); if (!pollForValidSSID(3, SSID)) { promise.reject(ConnectErrorCodes.android10ImmediatelyDroppedConnection.toString(), "Firmware bugs on OnePlus prevent it from connecting on some firmware versions."); return; @@ -537,12 +587,15 @@ public void onAvailable(@NonNull Network network) { public void onUnavailable() { super.onUnavailable(); timeoutHandler.removeCallbacks(timeoutRunnable); + joinedNetwork = null; promise.reject(ConnectErrorCodes.didNotFindNetwork.toString(), "Network not found or network request cannot be fulfilled."); } @Override public void onLost(@NonNull Network network) { super.onLost(network); + timeoutHandler.removeCallbacks(timeoutRunnable); + joinedNetwork = null; DisconnectCallbackHolder.getInstance().unbindProcessFromNetwork(); DisconnectCallbackHolder.getInstance().disconnect(); } diff --git a/ios/RNWifi.m b/ios/RNWifi.m index be3b07b..98029c6 100644 --- a/ios/RNWifi.m +++ b/ios/RNWifi.m @@ -87,15 +87,25 @@ + (BOOL)requiresMainQueueSetup } } -RCT_EXPORT_METHOD(connectToProtectedSSIDPrefix:(NSString*)ssid +RCT_EXPORT_METHOD(connectToProtectedSSIDPrefix:(NSString*)ssidPrefix withPassphrase:(NSString*)passphrase isWEP:(BOOL)isWEP resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + [self connectToProtectedSSIDPrefixOnce:ssidPrefix withPassphrase:passphrase isWEP:isWEP joinOnce:false resolver:resolve rejecter:reject]; +} + +RCT_EXPORT_METHOD(connectToProtectedSSIDPrefixOnce:(NSString*)ssidPrefix + withPassphrase:(NSString*)passphrase + isWEP:(BOOL)isWEP + joinOnce:(BOOL)joinOnce + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + if (@available(iOS 13.0, *)) { - NEHotspotConfiguration* configuration = [[NEHotspotConfiguration alloc] initWithSSIDPrefix:ssid passphrase:passphrase isWEP:isWEP]; - configuration.joinOnce = false; + NEHotspotConfiguration* configuration = [[NEHotspotConfiguration alloc] initWithSSIDPrefix:ssidPrefix passphrase:passphrase isWEP:isWEP]; + configuration.joinOnce = joinOnce; [[NEHotspotConfigurationManager sharedManager] applyConfiguration:configuration completionHandler:^(NSError * _Nullable error) { if (error != nil) { @@ -103,10 +113,10 @@ + (BOOL)requiresMainQueueSetup } else { // Verify SSID connection [self getWifiSSID:^(NSString* result) { - if ([result hasPrefix:ssid]){ + if ([result hasPrefix:ssidPrefix]){ resolve(nil); } else { - reject([ConnectError code:UnableToConnect], [NSString stringWithFormat:@"%@/%@", @"Unable to connect to Wi-Fi with prefix ", ssid], nil); + reject([ConnectError code:UnableToConnect], [NSString stringWithFormat:@"%@/%@", @"Unable to connect to Wi-Fi with prefix ", ssidPrefix], nil); } }]; } diff --git a/lib/types/index.d.ts b/lib/types/index.d.ts index 7a99ee3..f2fa89b 100644 --- a/lib/types/index.d.ts +++ b/lib/types/index.d.ts @@ -157,6 +157,20 @@ declare module 'react-native-wifi-reborn' { password: string, isWEP: boolean ): Promise; + /** + * Connects to a WiFi network that start with SSIDPrefix. Rejects with an error if it couldn't connect. + * + * @param SSIDPrefix Wifi name prefix. + * @param password `null` for open networks. + * @param isWep Used on iOS. If `true`, the network is WEP Wi-Fi; otherwise it is a WPA or WPA2 personal Wi-Fi network. + * @param joinOnce Used on iOS. If `true`, restricts the lifetime of a configuration to the operating status of the app that created it. + */ + export function connectToProtectedSSIDPrefixOnce( + SSIDPrefix: string, + password: string | null, + isWEP: boolean, + joinOnce: boolean + ): Promise; //#endregion