package com.ablsolutions.wificonnect.connector import android.app.AppOpsManager import android.content.Context import android.content.SharedPreferences import android.net.wifi.WifiManager import android.net.wifi.WifiNetworkSuggestion import android.os.Build import android.os.Handler import android.os.Looper import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import com.ablwificonnectivity.connector.getSharedPreferences import com.ablwificonnectivity.connector.hasWifiPermission import com.ablwificonnectivity.settings.ConnectionSettings import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.modules.core.DeviceEventManagerModule @RequiresApi(Build.VERSION_CODES.Q) class Android10WifiConnector(private val reactContext: ReactApplicationContext) : WifiConnector { private val appOpsManager = reactContext.getSystemService(AppCompatActivity.APP_OPS_SERVICE) as AppOpsManager private val wifiManager = reactContext.getSystemService(AppCompatActivity.WIFI_SERVICE) as WifiManager companion object { private const val IS_NETWORK_CONFIGURED_KEY: String = "android-is-network-configured" } override fun connect( connectionSettings: ConnectionSettings, successCallback: () -> Unit, failCallback: (errorCode: String, errorMessage: String) -> Unit ) { connect(connectionSettings, successCallback, failCallback, false) } override fun deleteConfiguration(successCallback: () -> Unit, failCallback: (errorCode: String, errorMessage: String) -> Unit) { val sharedPreferences = getSharedPreferences() if (sharedPreferences == null) { failCallback(ErrorCodes.activityDoesNotExist, "Removing a network configuration must be executed in the context of an activity") return } // pass an empty list to remove all suggestions provided by this app val result = wifiManager.removeNetworkSuggestions(emptyList()) when (result) { WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS -> { sharedPreferences.edit().putBoolean(IS_NETWORK_CONFIGURED_KEY, false).apply() successCallback() } WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_APP_DISALLOWED -> { sharedPreferences.edit().putBoolean(IS_NETWORK_CONFIGURED_KEY, false).apply() successCallback() } else -> { failCallback(ErrorCodes.failedToRemoveNetwork, "Failed to remove network configuration from device") } } } override fun isWifiConfigured(): Boolean { val sharedPreferences = getSharedPreferences() val isNetworkConfigured = sharedPreferences?.getBoolean(IS_NETWORK_CONFIGURED_KEY, false) return isNetworkConfigured == true } private fun connect( connectionSettings: ConnectionSettings, successCallback: () -> Unit, failCallback: (errorCode: String, errorMessage: String) -> Unit, isRetry: Boolean ) { val suggestions = ArrayList() suggestions.add( WifiNetworkSuggestion.Builder() .setSsid(connectionSettings.ssid) .setIsHiddenSsid(connectionSettings.isHiddenSSID) .setWpa2EnterpriseConfig(connectionSettings.enterpriseConfig) .setPriority(100) .build() ) val sharedPreferences = getSharedPreferences() if (sharedPreferences == null) { failCallback(ErrorCodes.activityDoesNotExist, "Adding a network configuration must be executed in the context of an activity") return } val result: Int try { result = wifiManager.addNetworkSuggestions(suggestions) } catch (e: SecurityException) { failCallback(ErrorCodes.permissionDenied, "Permission android:change_wifi_state denied") return } val handler = Handler(Looper.getMainLooper()) val checkInterval = 1000L fun periodicallyCheckIfPermissionDialogWasRejected(timePassed: Long) { handler.postDelayed({ if (!appOpsManager.hasWifiPermission(reactContext)) { failCallback(ErrorCodes.userRejected, "User rejected to apply configuration") } else { if (timePassed >= (connectionSettings.timeSpanToWaitForPermissionDialogConfirmationInSeconds * 1000)) { // we waited the configured time span for confirmation but there is no way to // finally know if the user really confirmed. So, we register a background worker to // check if the user rejected the permission. BackgroundPermissionChecker.start(reactContext) sharedPreferences.edit().putBoolean(IS_NETWORK_CONFIGURED_KEY, true).apply() successCallback() } else { periodicallyCheckIfPermissionDialogWasRejected(timePassed + checkInterval) } } }, checkInterval) } when (result) { WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS -> { // Android 10 shows a permission dialog in the notification center of the OS, if this // is the first try to configure a network. In this case we have no chance to detect // when the dialog is closed and we also don't know if the user accepted the configuration. // If this is not the first try, we know that everything is fine. if (isRetry) { // We already were allowed to delete the configuration, so just return with success. sharedPreferences.edit().putBoolean(IS_NETWORK_CONFIGURED_KEY, true).apply() successCallback() } else { periodicallyCheckIfPermissionDialogWasRejected(0) } } WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_DUPLICATE -> { if (!isRetry) { // delete network and try to connect again wifiManager.removeNetworkSuggestions(suggestions) connect(connectionSettings, successCallback, failCallback, true) } } WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_INVALID -> { failCallback(ErrorCodes.invalidWifiConfiguration, "WiFi configuration seems to be invalid") } WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_NOT_ALLOWED -> { failCallback(ErrorCodes.permissionDenied, "Configuring this network is not allowed by the operating system") } WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_APP_DISALLOWED -> { failCallback(ErrorCodes.userRejected, "User rejected to apply configuration") } else -> { val errorMessage = "An unknown error occurred - internal status code = $result - see status codes starting with 'STATUS_NETWORK_SUGGESTIONS_' at https://developer.android.com/reference/android/net/wifi/WifiManager" failCallback(ErrorCodes.unknownError, errorMessage) } } } private fun getSharedPreferences(): SharedPreferences? { return reactContext.currentActivity?.getSharedPreferences("abl-solutions_wifi-connect", Context.MODE_PRIVATE) } class BackgroundPermissionChecker { companion object { var isListenerRegistered: Boolean = false private var checkPeriodically: Boolean = false private val handler = Handler(Looper.getMainLooper()) private const val CHECK_INTERVAL_IN_SECONDS = 5 fun start(reactContext: ReactApplicationContext) { if (!checkPeriodically) { checkPeriodically = true periodicallyCheckIfPermissionDialogWasRejectedAfterReturning(reactContext) } } private fun periodicallyCheckIfPermissionDialogWasRejectedAfterReturning(reactContext: ReactApplicationContext) { if (isListenerRegistered && checkPeriodically) { handler.postDelayed({ val appOpsManager = reactContext.getSystemService(AppCompatActivity.APP_OPS_SERVICE) as AppOpsManager if (!appOpsManager.hasWifiPermission(reactContext)) { reactContext.getSharedPreferences()!!.edit().putBoolean(IS_NETWORK_CONFIGURED_KEY, false).apply() sendNotification(reactContext) } else { periodicallyCheckIfPermissionDialogWasRejectedAfterReturning(reactContext) } }, CHECK_INTERVAL_IN_SECONDS * 1000L) } } private fun sendNotification(reactContext: ReactApplicationContext) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit("PermissionRejected", null) } } } }