I am building an android app that needs to communicate over a WiFi network that will not have any internet access. The problem is that even when the WiFi is connected android chooses to use cellular/mobile data when no connection internet is present on the wifi network.
I have read many posts on the issue many of which involve rooting the device but that is not possible with a production app (rooting devices is not an option). other solution (like my code bellow) suggest using bindProcessToNetwork() which works perfectly on my Sony Z2 but not on other devices I have tested on (all running 6.0.1)
private void bindToNetwork() {
final ConnectivityManager connectivityManager = (ConnectivityManager) mActivity.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkRequest.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder = new NetworkRequest.Builder();
//set the transport type do WIFI
builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
connectivityManager.requestNetwork(builder.build(), new ConnectivityManager.NetworkCallback() {
#Override
public void onAvailable(Network network) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connectivityManager.bindProcessToNetwork(null);
if (barCodeData.getSsid().contains("ap-name")) {
connectivityManager.bindProcessToNetwork(network);
}
} else {
//This method was deprecated in API level 23
ConnectivityManager.setProcessDefaultNetwork(null);
if (barCodeData.getSsid().contains("ap-name")) {
ConnectivityManager.setProcessDefaultNetwork(network);
}
}
connectivityManager.unregisterNetworkCallback(this);
}
});
}
}
You can solve this by setting captive_portal_detection_enabled to 0 (false).
What's actually happening is that by default, everytime you connect to a wifi, the FW will test against a server (typically google) to see if it's a captive wifi (needs login). So if your wifi is not connected to google, this check will fail. After that, the device knows that wifi has no internet connection and simply will not autoconnect to it.
Setting this setting to 0 will avoid this check.
Programatically:
Settings.Global.putInt(getContentResolver(), Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 0);
Edit: You may need to use the string "captive_portal_detection_enabled" directly, instead of the constant that's not visible depending on Android version.
you'd need to disable mobile data in the Settings (not certain, if this can be done programmatically, which might be a possible option) - or take out the USIM;
else the common behavior is, that it will always fall back to the best available connection (while a connection with internet gateway might be preferred, because it is used by most application).
also see this answer.
Solution on Kotlin
class ConnectWithoutInternetTest constructor(
private val mContext: Context,
private val connectivityManager: ConnectivityManager,
private val wifiManager: WifiManager
) {
private val mWifiBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
WifiManager.NETWORK_STATE_CHANGED_ACTION -> {
val info = intent.getParcelableExtra<NetworkInfo>(WifiManager.EXTRA_NETWORK_INFO)
val isConnected = info.isConnected
val ssid: String? = normalizeAndroidWifiSsid(wifiManager.connectionInfo?.ssid)
if (isConnected) {
val builder = NetworkRequest.Builder()
builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
connectivityManager.registerNetworkCallback(
builder.build(),
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
val networkInfo = connectivityManager.getNetworkInfo(network)
val networkSsid = networkInfo.extraInfo
if (networkSsid == ssid) {
connectivityManager.unregisterNetworkCallback(this)
}
}
})
}
}
}
}
}
private fun init() {
val intentFilter = IntentFilter()
intentFilter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION)
mContext.registerReceiver(mWifiBroadcastReceiver, intentFilter)
}
private fun destroy() {
mContext.unregisterReceiver(mWifiBroadcastReceiver)
}
private fun normalizeAndroidWifiSsid(ssid: String?): String? {
return ssid?.replace("\"", "") ?: ssid
}
fun connectToWifi(ssidParam: String, password: String?) {
init()
val ssid = "\"$ssidParam\""
val config = wifiManager.configuredNetworks.find { it.SSID == ssid }
val netId = if (config != null) {
config.networkId
} else {
val wifiConfig = WifiConfiguration()
wifiConfig.SSID = ssid
password?.let { wifiConfig.preSharedKey = "\"$password\"" }
wifiConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
wifiManager.addNetwork(wifiConfig)
}
wifiManager.disconnect()
val successful = wifiManager.enableNetwork(netId, true)
}
You're in the right path, the solution is indeed with ConnectivityManager.bindProcessToNetwork(network). This information was posted on the Android Developers Blog in this article: Connecting your App to a Wi-Fi Device.
Looking into your code this barCodeData.getSsid() doesn't look that is getting the SSID of the currently connected wifi network. If that's the case your code will never successfully bind to the network.
Try replace your if statement
if (barCodeData.getSsid().contains("ap-name"))
With
if (getNetworkSsid(context).equals("ap-name"))
Helper method in kotlin to retrieve the SSID of the connected wifi network
private fun getNetworkSsid(context: Context?): String {
// WiFiManager must use application context (not activity context) otherwise a memory leak can occur
val mWifiManager = context?.applicationContext?.getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiInfo: WifiInfo? = mWifiManager.connectionInfo
if (wifiInfo?.supplicantState == SupplicantState.COMPLETED) {
return wifiInfo.ssid.removeSurrounding("\"")
}
return ""
}
If still doesn't work please follow my complete solution where I used the same method but with some extra checks. I tested it in the Android versions 5.1.1, 6.0, 6.0.1, 7.1.1 and 8.1.0.
You're on the right path, you only need to tweak what you have a bit. One of the main issues I see is that you unregister the network callback on onAvailable() which is not a good idea as networks tend to switch between available/unavailable from time-to-time which would cause issues on your device.
The other thing to consider is requesting a network that's Wi-Fi and clearing the other network capabilities as they may cause you issues depending on your network's setup.
Here is another version of how to do that which is hopefully a bit simpler.
final NetworkRequest requestForWifi =
new NetworkRequest.Builder()
.clearCapabilities() // We only care about Wi-Fi
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build();
final ConnectivityManager connectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkCallback networkCallbackWifi = new NetworkCallback() {
#Override
void onAvailable(Network network) {
// Once this callback triggers, a Wi-Fi network is available
WifiInfo wifiInfo = connectivity.getNetworkCapabilities(network).TransportInfo;
string ssid = wifiInfo.SSID.Trim(new char[] {'"', '\"' });
if (!ssid.contains("ap-name")) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connectivityManager.bindProcessToNetwork(network);
} else {
ConnectivityManager.setProcessDefaultNetwork(network);
}
}
};
// Last thing is to actually request a network.
connectivityManager.requestNetwork(requestForWifi, networkCallbackWifi);
You can check if wifi is connected then proceed else show a dialog to user asking him to connect to a wifi network
Since the method NetworkInfo.isConnected() is now deprecated in API-23, here is a method which detects if the Wi-Fi adapter is on and also connected to an access point using WifiManager instead:
private boolean checkWifiOnAndConnected() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
if (wifiMgr.isWifiEnabled()) { // Wi-Fi adapter is ON
WifiInfo wifiInfo = wifiMgr.getConnectionInfo();
if( wifiInfo.getNetworkId() == -1 ){
return false; // Not connected to an access point
}
return true; // Connected to an access point
}
else {
return false; // Wi-Fi adapter is OFF
}
}
Related
As we know Google has updated their policies that New apps will need to must target Android 11 (API level 30) or higher link. My app is working as expected if I am using the lower targeted SDK but When am I using targeted SDK 30 then It is not working as Expected.
There are the following main functionality in the App:
Available WiFi should be visible on list for Android 6 to 11 (with latest Android version)
After clicking on any list item user should connect with their respected WiFi. It can be OPEN or other.
After connected with respective WiFi, It will redirect to captive page if any. Captive page will be WebView into the app.
Now coming to the point, I am facing the following issue with the targeted 30 SDK.
When I am using the latest suggestion wifi connection code [1] then it's working partially.There is one issue, Suppose we are already connected to another WiFi connection and I am trying to connect with new WiFi then It's not connecting to new WiFi but I need to connect with the New Wifi.
Code which is using in the App with targeted SDK 30 (Which is not connecting with New Wifi)-
val suggestion = WifiNetworkSuggestion.Builder()
.setSsid(SSID) // SSID of network
.setWpa2Passphrase(wifiPassword) // password is network is not open
//.setIsAppInteractionRequired(true) // Optional (Needs location permission)
.build()
val suggestionsList = listOf(suggestion)
val wifiManager =
applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
if (status != WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
// do error handling here
Log.e("NETWORK", "Error")
}
// Optional (Wait for post connection broadcast to one of your suggestions)
val intentFilter =
IntentFilter(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION);
val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.e("NETWORK", "broadcastReceiver")
if (!intent.action.equals(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)) {
return;
}
// do post connect processing here
Log.e("NETWORK", "post connect")
}
};
registerReceiver(broadcastReceiver, intentFilter)
Whenever my targeted SDK was 29 then I am using the WifiNetworkSpecifier [2] approach for connecting WiFi in Android 10 and 11. It was also working.
Code which is using in the App with targeted SDK 29 (Which is working as expected but targeted SDK must be 28 or 29 otherwise Internet will not work out of the Application)-
private fun android10andMoreVersionsWithoutOuterInternet(
scanResult: ScanResult,
wifiSSID: String,
wifiPassword: String,
capabilities: String
) {
// Android 10 (API level 29) -- Android Q (Android 10)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val wifiManager =
this.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiNetworkSpecifier = WifiNetworkSpecifier.Builder()
.setSsid(wifiSSID)
//.setSsidPattern(PatternMatcher(wifiSSID, PatternMatcher.PATTERN_PREFIX))
.setWpa2Passphrase(wifiPassword)
.build()
val networkRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
//.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
//.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
//.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.setNetworkSpecifier(wifiNetworkSpecifier)
.build()
val connectivityManager =
this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Log.d("NETWORK", "Network available")
super.onAvailable(network)
// To make sure that requests don't go over mobile data
connectivityManager.bindProcessToNetwork(network)
//unregister network callback
//connectivityManager.unregisterNetworkCallback(this)
// connectivityManager.bindProcessToNetwork(null)
gotoNextScreen(scanResult, wifiManager)
}
override fun onUnavailable() {
Log.d("NETWORK", "Network unavailable")
super.onUnavailable()
}
override fun onLosing(network: Network, maxMsToLive: Int) {
Log.d("NETWORK", "onLosing")
super.onLosing(network, maxMsToLive)
}
override fun onLost(network: Network) {
Log.d("NETWORK", "onLost")
super.onLost(network)
//connectivityManager.bindProcessToNetwork(null)
//connectivityManager.unregisterNetworkCallback(this)
}
}
connectivityManager.requestNetwork(networkRequest, networkCallback)
val builder = NetworkRequest.Builder()
builder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
connectivityManager.registerNetworkCallback(builder.build(), networkCallback)
//connectivityManager.registerNetworkCallback(networkRequest, networkCallback) // For listen
}
}
Everything is working as expected with old depreciated code with Android 9..
Code which is using in Android 9 abd below (Which is working as expected and It is not impacting with any targeted SDK)-
private fun android9AndPreviousVersion(
scanResult: ScanResult,
wifiSSID: String,
wifiPassword: String,
capabilities: String
) {
val conf = WifiConfiguration()
conf.SSID =
"\"" + wifiSSID + "\"" // Please note the quotes. String should contain ssid in quotes
conf.status = WifiConfiguration.Status.ENABLED
conf.priority = 40
if (Common.checkWifiType(capabilities) == "WEP") {
Log.e("NETWORK", "Configuring WEP")
conf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
conf.allowedProtocols.set(WifiConfiguration.Protocol.RSN)
conf.allowedProtocols.set(WifiConfiguration.Protocol.WPA)
conf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
conf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED)
conf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
conf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
conf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
conf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
if (wifiPassword.matches(Regex("^[0-9a-fA-F]+$"))) {
conf.wepKeys[0] = wifiPassword
} else {
conf.wepKeys[0] = "\"" + wifiPassword + "\""
}
conf.wepTxKeyIndex = 0
} else if (Common.checkWifiType(capabilities) == "WPA") {
Log.e("NETWORK", "Configuring WPA")
conf.allowedProtocols.set(WifiConfiguration.Protocol.RSN)
conf.allowedProtocols.set(WifiConfiguration.Protocol.WPA)
conf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK)
conf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
conf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
conf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40)
conf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
conf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
conf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP)
conf.preSharedKey = "\"" + wifiPassword + "\""
} else {
Log.e("NETWORK", "Configuring OPEN network")
conf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
}
val wifiManager =
this.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val networkId = wifiManager.addNetwork(conf)
Log.e("NETWORK", "Add result $networkId")
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
val list = wifiManager.configuredNetworks
for (i in list) {
if (i.SSID != null && i.SSID == "\"" + wifiSSID + "\"") {
Log.e("NETWORK", "WifiConfiguration SSID " + i.SSID)
val isDisconnected = wifiManager.disconnect()
Log.e("NETWORK", "isDisconnected : $isDisconnected")
val isEnabled = wifiManager.enableNetwork(i.networkId, true)
Log.e("NETWORK", "isEnabled : $isEnabled")
val isReconnected = wifiManager.reconnect()
Log.e("NETWORK", "isReconnected : $isReconnected")
break
}
}
//val connectionInfo: WifiInfo = wifiManager.getConnectionInfo()
gotoNextScreen(scanResult, wifiManager)
}
Conclude: When am I using WifiNetworkSpecifier for connecting to Available WiFi with targeted SDK 30 then I am able to connect but My Internet is only working in the App. When am I using latest Suggestion wifi for connecting to Available WiFi with targeted SDK 30 then I am unable to connect with the New WiFi. I am facing this issue in Android 10 and Android 11 devices.
Please suggest me for the solution. Please check my POC code [here]
The only way to have internet with wifi is :
In Android 10, you should implement wifiConfiguration to connect/disconnect
connect function :
val wifiConfiguration = WifiConfiguration()
wifiConfiguration.SSID = "\"${configuration.ssid}\""
wifiConfiguration.preSharedKey = "\"${configuration.password}\""
var netId = wifiManager.addNetwork(wifiConfiguration)
if (netId == -1) {
// If the network configuration with the same SSID already exists, we need to retrieve the configured networks and get the network id of the network corresponding to the given SSID.
netId = configuredNetworkForSSID(configuration.ssid)?.networkId ?: -1
}
wifiManager.enableNetwork(netId, true)
disconnect function :
wifiManager.removeNetwork(it.networkId)
wifiManager.disconnect()
In Android 11
You should use :
WifiSuggestion
final WifiNetworkSuggestion suggestion1 =
new WifiNetworkSuggestion.Builder()
.setSsid("test111111")
.setIsAppInteractionRequired(true) // Optional (Needs location permission)
.build();
final IntentFilter intentFilter =
new IntentFilter(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION);
final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
if (!intent.getAction().equals(
WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)) {
return;
}
// do post connect processing here...
}
};
context.registerReceiver(broadcastReceiver, intentFilter);
to disconnect from Android 11, it's really weird, Google use a stupid solution witch: you should leave the place, if you try to disconnect manually by deleting the wifi from settings, then you can't re-connect automatically. (You should add the wifi network manually to recover the auto-connect behaviour)
Wifi Connection in Android 10 : the OS present a small push-notification to accept the connection, if you didn't add a pop-up to alert the user, he will be missed it;
in Android 11, Google changed the notification with an alertBox to alert the user.
if you use WifiNetworkSpecifier or another API you will have a wifi connection but without internet.
To restrict the control of third party app, Android has changed the way of connecting to wifi network in Android 10 and Android 11
If you look closely to your first solution, you can see that you are suggesting the system that these wifi are available to connect, now the system will decide whether to connect or not. Now here if system is already connection to some wifi which have active internet connection then Android will ignore your suggestion until that wifi is unavailable.
Right, if you use WifiNetworkSpecifier it restrict the uses of the network to your app only.
The code below is used to detect network connectivity. The scenario is the software connected to the phone's hotspot but the phone's cellular network doesn't switch on. The code below return isConnected = true, which means it is only detects connectivity? The question is how I change it to detect network availability?
fun Context.isOnline(): Boolean {
val connectivityManager =
this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkInfo = connectivityManager.activeNetworkInfo
if (networkInfo == null) {
log(message = "Network info is **NULL**")
return false
} else {
val isConnected = when(networkInfo.detailedState) {
NetworkInfo.DetailedState.CONNECTING -> true
NetworkInfo.DetailedState.AUTHENTICATING -> true
NetworkInfo.DetailedState.OBTAINING_IPADDR -> true
NetworkInfo.DetailedState.CONNECTED -> true
NetworkInfo.DetailedState.VERIFYING_POOR_LINK -> true
else -> false
}
if (!isConnected) {
log(message = "Network state is **${networkInfo.detailedState?.name ?: "NULL"}**")
}
return isConnected
}
}
You're using a deprecated API with NetworkInfo. You should be using NetworkCapabilities.NET_CAPABILITY_VALIDATED
Indicates that connectivity on this network was successfully validated. For example, for a network with NET_CAPABILITY_INTERNET, it means that Internet connectivity was successfully detected.
SDK >= 23
You could change your code to something like this to see if that's true to confirm internet connectivity:
connectivityManager
.getNetworkCapabilities(connectivityManager.getActiveNetwork())
.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
SDK >= 21
If you are using anything as old as SDK 21, you can use the ConnectivityManager.registerNetworkCallback API.
boolean isConnected = false;
final NetworkRequest request =
new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build();
final ConnectivityManager connectivityManager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
final NetworkCallback networkCallback = new NetworkCallback() {
#Override
void onAvailable(Network network) {
// Triggers when this network is available.
isConnected = true;
}
#Override
void onLost(Network network) {
// Triggers when this network is lost.
isConnected = false;
}
};
connectivityManager.requestNetwork(request, networkCallback);
I run the following connectAndroidQ code to connect to open ap, and disconnectAndroidQ code to disconnect, but after connecting and disconnecting, the return value of NetworkInterface.getNetworkInterfaces() is null.
Before executing the code below, the return value of NetworkInterface.getNetworkInterfaces() is normal, but just running the code returns a null value.
test phone : pixel2
android version : 10
#RequiresApi(api = Build.VERSION_CODES.Q)
private boolean connectAndroidQ(#Nullable ConnectivityManager connectivityManager, ScanResult scanResult) {
if (connectivityManager == null) {
return false;
}
WifiNetworkSpecifier.Builder wifiNetworkSpecifierBuilder = new WifiNetworkSpecifier.Builder()
.setSsid(scanResult.SSID);
NetworkRequest networkRequest = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setNetworkSpecifier(wifiNetworkSpecifierBuilder.build())
.build();
if (networkCallback != null) {
connectivityManager.unregisterNetworkCallback(networkCallback);
}
networkCallback = new ConnectivityManager.NetworkCallback() {
#Override
public void onAvailable(#NonNull Network network) {
super.onAvailable(network);
connectivityManager.bindProcessToNetwork(network);
}
#Override
public void onUnavailable() {
super.onUnavailable();
}
};
connectivityManager.requestNetwork(networkRequest, networkCallback);
return true;
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private boolean disconnectAndroidQ(#NonNull final ConnectivityManager connectivityManager) {
if (networkCallback != null) {
connectivityManager.unregisterNetworkCallback(networkCallback);
networkCallback = null;
}
return true;
}
Please help.
Thanks in advance.
I solved that problem by calling bindProcessToNetwork(null).
See the link below, Note that if network ever disconnects, all Sockets created in this way will cease to work and all host name resolutions will fail. This is by design so an application doesn't accidentally use Sockets it thinks are still bound to a particular Network. To clear binding pass null for network.
https://developer.android.com/reference/android/net/ConnectivityManager#bindProcessToNetwork(android.net.Network)
My application seems to keep crashing giving me an E/AndroidRuntime: FATAL EXCEPTION: main error when i try to make a Get request to a server and there is no internet. I expected the app to run but no data be displayed.
Log.i("getStoreData()" , "Inside the coroutine before getData")
this is the last log that I have put myself gets printed before the app crashes.
private fun getStoreData() {
Log.i("getStoreData()", " inside getStoreData")
val job = coroutineScope.launch {
Log.i("getStoreData()" , "Inside the coroutine before getData")
var data = StoreAPI.retrofitService.getData()
Log.i("getStoreData()" , "Inside the coroutine after getData")
try {
var storeData = data.stores
_status.value = "Success: ${storeData.size} Stores received"
if(storeData.size > 0){
_stores.value = storeData
}
} catch (t: Throwable) {
Log.i("Retrofit catch block", _status.value)
_status.value = "Failure: " + t.message
t.printStackTrace()
}
}
}
StoreAPIService.kt
private const val URL = "http://sandbox.bottlerocketapps.com/BR_Android_CodingExam_2015_Server/"
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(URL)
.build()
interface StoreAPIService{
//Initially was using Jake Wharton's library for retrofit2 kotlin coroutines support but it has been deprecated since the support
// addition of the suspend keyword in retrofit 2.6.0
//Suspend does all the task of coroutines for us by just adding it before the function declaration
#GET("stores.json")
suspend fun getData():
Data //return Data object because Data has access to the Store JSON Object/Array
}
object StoreAPI{
val retrofitService: StoreAPIService by lazy {
retrofit.create(StoreAPIService::class.java)
}
}
Any idea why?
EDIT:
I cannot use these network connectivity functions because I my fragment is not connected to any activity and the fragment is connected to a viewModel. Therefore this line of code doesn't work as there is no context to bound it to. If you have a work around for this that would be great too.
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NB: before making any Network call or sending any requesting you must ensure that the device is connected to internet. I entice you to write a simple function to check if you're connected, if you're connected then you can send the request or make a network call.
Try using this
Create Class For NetworkConnectionDetection
Manifest
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
NetworkConnection Class
class NetworkConnection(val context: Context) : LiveData<Boolean>() {
var connectionManger: ConnectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
lateinit var netwrokCallback: ConnectivityManager.NetworkCallback
override fun onActive() {
super.onActive()
updateConnection()
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
connectionManger.registerDefaultNetworkCallback(NetworkConnectioncallback())
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> {
lollipopNetworkRequest()
}
else -> {
context.registerReceiver(
networkReciever(),
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
)
}
}
}
/*override fun onInactive() {
super.onInactive()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
connectionManger.unregisterNetworkCallback(NetworkConnectioncallback())
} else {
context.unregisterReceiver(networkReciever())
}
}*/
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
fun lollipopNetworkRequest() {
val requestBuilder = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
connectionManger.registerNetworkCallback(
requestBuilder.build(),
NetworkConnectioncallback()
)
}
fun NetworkConnectioncallback(): ConnectivityManager.NetworkCallback {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
netwrokCallback = object : ConnectivityManager.NetworkCallback() {
override fun onLost(network: Network) {
super.onLost(network)
postValue(false)
}
override fun onAvailable(network: Network) {
super.onAvailable(network)
postValue(true)
}
}
return netwrokCallback
} else {
throw IllegalAccessError("Error!")
}
}
fun networkReciever() = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
updateConnection()
}
}
fun updateConnection() {
val activeNetwork: NetworkInfo? = connectionManger.activeNetworkInfo
postValue((activeNetwork?.isConnected == true))
}
}
Now Inside your Activity/Fragment Check the connection either it is connected or not. Here is how it will be achieved
val networkConnection = NetworkConnection(requireContext())
networkConnection.observe(viewLifecycleOwner, { isConnected ->
if (isConnected) {
// Do what ever you want to do
} else {
// Show No internet connection message
}
})
You need to add internet checks before calling your retrofit service because to get some data from server, internet connectivity is mandatory
This method checks whether mobile is connected to internet and returns true
if connected:
private boolean isNetworkConnected() {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isConnected();
}
in manifest,
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
Edit: This method actually checks if device is connected to internet(There is
a possibility it's connected to a network but not to internet).
public boolean isInternetAvailable() {
try {
InetAddress ipAddr = InetAddress.getByName("google.com");
//You can replace it with your name
return !ipAddr.equals("");
}catch (Exception e) {
return false;
}
}
This will tell you if you're connected to a network:
boolean connected = false;
ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
if(connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE).getState() == NetworkInfo.State.CONNECTED ||
connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI).getState() == NetworkInfo.State.CONNECTED) {
//we are connected to a network
connected = true;
}
else
connected = false;
I also followed that tutorial, but I don't remember it having an offline mode. That is an option that you have to integrate on yourself.
When you create the viewModel, because it has an init block, it makes the call to the API and if you don't have an Internet connection, it crashes.
So you should write the init viewModel some code that checks whether you have an Internet connection or not. Or in the method that makes the API call to get the data.
In the next lesson from that tutorial, "Behind the scenes", they talk about offline mode.
I have to send UDP packets to a WiFi module (provided with own AP) with no internet connection but when I connect the mobile with the AP, Android redirects my packets on the mobile data interface because it has got internet connection.
I've used the code below to do my job but it seems not working on Android M.
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void setWifiInterfaceAsDefault() {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkRequest.Builder builder = new NetworkRequest.Builder();
NetworkRequest networkRequest= builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build();
connectivityManager.requestNetwork(networkRequest, new ConnectivityManager.NetworkCallback());
}
I've also added
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
on my AndroidManifest.xml and I ensured myself that Settings.System.canWrite(this) returns true but still nothing.
Thanks in advance.
Stanislav's answer is correct but incomplete because only works in Lollipop.
I've wrote a complete solution for Lollipop and Marshmallow onwards for you to route all network requests through WiFi when connected to a specific network of your choice.
Kotlin
In your Activity,
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class RoutingActivity : Activity() {
private var mConnectivityManager: ConnectivityManager? = null
private var mNetworkCallback: ConnectivityManager.NetworkCallback? = null
//...
override fun onCreate(savedInstanceState: Bundle?) {
//...
routeNetworkRequestsThroughWifi("Access-Point-SSID-You-Want-To-Route-Your-Requests")
}
Route future network requests from application through WiFi (even if given WiFi network is without internet and mobile data has internet connection)
/**
* This method sets a network callback that is listening for network changes and once is
* connected to the desired WiFi network with the given SSID it will bind to that network.
*
* Note: requires android.permission.INTERNET and android.permission.CHANGE_NETWORK_STATE in
* the manifest.
*
* #param ssid The name of the WiFi network you want to route your requests
*/
private fun routeNetworkRequestsThroughWifi(ssid: String) {
mConnectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
// ensure prior network callback is invalidated
unregisterNetworkCallback(mNetworkCallback)
// new NetworkRequest with WiFi transport type
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build()
// network callback to listen for network changes
mNetworkCallback = object : ConnectivityManager.NetworkCallback() {
// on new network ready to use
override fun onAvailable(network: Network) {
if (getNetworkSsid(this#RoutingActivity).equals(ssid, ignoreCase = false)) {
releaseNetworkRoute()
createNetworkRoute(network)
} else {
releaseNetworkRoute()
}
}
}
mConnectivityManager?.requestNetwork(request, mNetworkCallback)
}
Unregister network callback
private fun unregisterNetworkCallback(networkCallback: ConnectivityManager.NetworkCallback?) {
if (networkCallback != null) {
try {
mConnectivityManager?.unregisterNetworkCallback(networkCallback)
} catch (ignore: Exception) {
} finally {
mNetworkCallback = null
}
}
}
Create network route
private fun createNetworkRoute(network: Network): Boolean? {
var processBoundToNetwork: Boolean? = false
when {
// 23 = Marshmallow
Build.VERSION.SDK_INT >= 23 -> {
processBoundToNetwork = mConnectivityManager?.bindProcessToNetwork(network)
}
// 21..22 = Lollipop
Build.VERSION.SDK_INT in 21..22 -> {
processBoundToNetwork = ConnectivityManager.setProcessDefaultNetwork(network)
}
}
return processBoundToNetwork
}
Release network route
private fun releaseNetworkRoute(): Boolean? {
var processBoundToNetwork: Boolean? = false
when {
// 23 = Marshmallow
Build.VERSION.SDK_INT >= 23 -> {
processBoundToNetwork = mConnectivityManager?.bindProcessToNetwork(null)
}
// 21..22 = Lollipop
Build.VERSION.SDK_INT in 21..22 -> {
processBoundToNetwork = ConnectivityManager.setProcessDefaultNetwork(null)
}
}
return processBoundToNetwork
}
Helper
private fun getNetworkSsid(context: Context?): String {
// WiFiManager must use application context (not activity context) otherwise a memory leak can occur
val mWifiManager = context?.applicationContext?.getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiInfo: WifiInfo? = mWifiManager.connectionInfo
if (wifiInfo?.supplicantState == SupplicantState.COMPLETED) {
return wifiInfo.ssid.removeSurrounding("\"")
}
return ""
}
Bind the network using ConnectivityManager.setProcessDefaultNetwork() prevents roaming and allows for full TCP access. Thus, within the onAvailable() callback you could bind the application process to that network rather than opening a connection to a particular URL.
ConnectivityManager connection_manager =
(ConnectivityManager) activity.getApplication().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkRequest.Builder request = new NetworkRequest.Builder();
request.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
connection_manager.registerNetworkCallback(request.build(), new NetworkCallback() {
#Override
public void onAvailable(Network network) {
ConnectivityManager.setProcessDefaultNetwork(network);
}
}
Original answer