Merge pull request #9139 from nextcloud/suppress-walled-network-test-on-metered-wifi

Suppress walled network check when on metered wifi
This commit is contained in:
Tobias Kaminsky 2021-10-25 09:55:55 +02:00 committed by GitHub
commit 2693c103b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 103 additions and 28 deletions

View file

@ -24,6 +24,7 @@ package com.nextcloud.client.network
import android.accounts.AccountManager import android.accounts.AccountManager
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.os.Build
import com.nextcloud.client.account.UserAccountManagerImpl import com.nextcloud.client.account.UserAccountManagerImpl
import com.nextcloud.client.network.ConnectivityServiceImpl.GetRequestBuilder import com.nextcloud.client.network.ConnectivityServiceImpl.GetRequestBuilder
import com.owncloud.android.AbstractOnServerIT import com.owncloud.android.AbstractOnServerIT
@ -44,7 +45,8 @@ class ConnectivityServiceImplIT : AbstractOnServerIT() {
connectivityManager, connectivityManager,
userAccountManager, userAccountManager,
clientFactory, clientFactory,
requestBuilder requestBuilder,
Build.VERSION.SDK_INT
) )
assertTrue(sut.connectivity.isConnected) assertTrue(sut.connectivity.isConnected)

View file

@ -2,7 +2,7 @@
* Nextcloud Android client application * Nextcloud Android client application
* *
* @author Chris Narkiewicz * @author Chris Narkiewicz
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com> * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
@ -20,7 +20,9 @@
package com.nextcloud.client.network; package com.nextcloud.client.network;
import android.annotation.SuppressLint;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.net.NetworkInfo; import android.net.NetworkInfo;
@ -32,6 +34,7 @@ import com.nextcloud.operations.GetMethod;
import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.HttpStatus;
import androidx.core.net.ConnectivityManagerCompat; import androidx.core.net.ConnectivityManagerCompat;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function1;
class ConnectivityServiceImpl implements ConnectivityService { class ConnectivityServiceImpl implements ConnectivityService {
@ -40,6 +43,7 @@ class ConnectivityServiceImpl implements ConnectivityService {
private final UserAccountManager accountManager; private final UserAccountManager accountManager;
private final ClientFactory clientFactory; private final ClientFactory clientFactory;
private final GetRequestBuilder requestBuilder; private final GetRequestBuilder requestBuilder;
private final int sdkVersion;
static class GetRequestBuilder implements Function1<String, GetMethod> { static class GetRequestBuilder implements Function1<String, GetMethod> {
@Override @Override
@ -51,17 +55,19 @@ class ConnectivityServiceImpl implements ConnectivityService {
ConnectivityServiceImpl(ConnectivityManager platformConnectivityManager, ConnectivityServiceImpl(ConnectivityManager platformConnectivityManager,
UserAccountManager accountManager, UserAccountManager accountManager,
ClientFactory clientFactory, ClientFactory clientFactory,
GetRequestBuilder requestBuilder) { GetRequestBuilder requestBuilder,
int sdkVersion) {
this.platformConnectivityManager = platformConnectivityManager; this.platformConnectivityManager = platformConnectivityManager;
this.accountManager = accountManager; this.accountManager = accountManager;
this.clientFactory = clientFactory; this.clientFactory = clientFactory;
this.requestBuilder = requestBuilder; this.requestBuilder = requestBuilder;
this.sdkVersion = sdkVersion;
} }
@Override @Override
public boolean isInternetWalled() { public boolean isInternetWalled() {
Connectivity c = getConnectivity(); Connectivity c = getConnectivity();
if (c.isConnected() && c.isWifi()) { if (c.isConnected() && c.isWifi() && !c.isMetered()) {
Server server = accountManager.getUser().getServer(); Server server = accountManager.getUser().getServer();
String baseServerAddress = server.getUri().toString(); String baseServerAddress = server.getUri().toString();
@ -76,7 +82,6 @@ class ConnectivityServiceImpl implements ConnectivityService {
// Content-Length is not available when using chunked transfer encoding, so check for -1 as well // Content-Length is not available when using chunked transfer encoding, so check for -1 as well
boolean result = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0); boolean result = !(status == HttpStatus.SC_NO_CONTENT && get.getResponseContentLength() <= 0);
get.releaseConnection(); get.releaseConnection();
return result; return result;
@ -96,21 +101,9 @@ class ConnectivityServiceImpl implements ConnectivityService {
if (networkInfo != null) { if (networkInfo != null) {
boolean isConnected = networkInfo.isConnectedOrConnecting(); boolean isConnected = networkInfo.isConnectedOrConnecting();
// more detailed check // more detailed check
boolean isMetered; boolean isMetered;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { isMetered = isNetworkMetered();
NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(
platformConnectivityManager.getActiveNetwork());
if (networkCapabilities != null) {
isMetered = !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
} else {
isMetered = ConnectivityManagerCompat.isActiveNetworkMetered(platformConnectivityManager);
}
} else {
isMetered = ConnectivityManagerCompat.isActiveNetworkMetered(platformConnectivityManager);
}
boolean isWifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI || hasNonCellularConnectivity(); boolean isWifi = networkInfo.getType() == ConnectivityManager.TYPE_WIFI || hasNonCellularConnectivity();
return new Connectivity(isConnected, isMetered, isWifi, null); return new Connectivity(isConnected, isMetered, isWifi, null);
} else { } else {
@ -118,6 +111,21 @@ class ConnectivityServiceImpl implements ConnectivityService {
} }
} }
@SuppressLint("NewApi") // false positive due to mocking
private boolean isNetworkMetered() {
if (sdkVersion >= android.os.Build.VERSION_CODES.M) {
final Network network = platformConnectivityManager.getActiveNetwork();
NetworkCapabilities networkCapabilities = platformConnectivityManager.getNetworkCapabilities(network);
if (networkCapabilities != null) {
return !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
} else {
return ConnectivityManagerCompat.isActiveNetworkMetered(platformConnectivityManager);
}
} else {
return ConnectivityManagerCompat.isActiveNetworkMetered(platformConnectivityManager);
}
}
private boolean hasNonCellularConnectivity() { private boolean hasNonCellularConnectivity() {
for (NetworkInfo networkInfo : platformConnectivityManager.getAllNetworkInfo()) { for (NetworkInfo networkInfo : platformConnectivityManager.getAllNetworkInfo()) {
if (networkInfo.isConnectedOrConnecting() && (networkInfo.getType() == ConnectivityManager.TYPE_WIFI || if (networkInfo.isConnectedOrConnecting() && (networkInfo.getType() == ConnectivityManager.TYPE_WIFI ||

View file

@ -22,6 +22,7 @@ package com.nextcloud.client.network;
import android.content.Context; import android.content.Context;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.os.Build;
import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManager;
@ -40,7 +41,8 @@ public class NetworkModule {
return new ConnectivityServiceImpl(connectivityManager, return new ConnectivityServiceImpl(connectivityManager,
accountManager, accountManager,
clientFactory, clientFactory,
new ConnectivityServiceImpl.GetRequestBuilder() new ConnectivityServiceImpl.GetRequestBuilder(),
Build.VERSION.SDK_INT
); );
} }

View file

@ -2,7 +2,7 @@
* Nextcloud Android client application * Nextcloud Android client application
* *
* @author Chris Narkiewicz * @author Chris Narkiewicz
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com> * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
@ -20,7 +20,10 @@
package com.nextcloud.client.network package com.nextcloud.client.network
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkInfo import android.net.NetworkInfo
import android.os.Build
import com.nextcloud.client.account.Server import com.nextcloud.client.account.Server
import com.nextcloud.client.account.User import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.account.UserAccountManager
@ -28,8 +31,10 @@ import com.nextcloud.client.logger.Logger
import com.nextcloud.common.PlainClient import com.nextcloud.common.PlainClient
import com.nextcloud.operations.GetMethod import com.nextcloud.operations.GetMethod
import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import com.owncloud.android.lib.resources.status.OwnCloudVersion import com.owncloud.android.lib.resources.status.OwnCloudVersion
@ -89,6 +94,12 @@ class ConnectivityServiceTest {
@Mock @Mock
lateinit var requestBuilder: ConnectivityServiceImpl.GetRequestBuilder lateinit var requestBuilder: ConnectivityServiceImpl.GetRequestBuilder
@Mock
lateinit var network: Network
@Mock
lateinit var networkCapabilities: NetworkCapabilities
@Mock @Mock
lateinit var logger: Logger lateinit var logger: Logger
@ -108,11 +119,16 @@ class ConnectivityServiceTest {
platformConnectivityManager, platformConnectivityManager,
accountManager, accountManager,
clientFactory, clientFactory,
requestBuilder requestBuilder,
Build.VERSION_CODES.M
) )
whenever(networkCapabilities.hasCapability(eq(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)))
.thenReturn(true)
whenever(platformConnectivityManager.activeNetwork).thenReturn(network)
whenever(platformConnectivityManager.activeNetworkInfo).thenReturn(networkInfo) whenever(platformConnectivityManager.activeNetworkInfo).thenReturn(networkInfo)
whenever(platformConnectivityManager.allNetworkInfo).thenReturn(arrayOf(networkInfo)) whenever(platformConnectivityManager.allNetworkInfo).thenReturn(arrayOf(networkInfo))
whenever(platformConnectivityManager.getNetworkCapabilities(any())).thenReturn(networkCapabilities)
whenever(requestBuilder.invoke(any())).thenReturn(getRequest) whenever(requestBuilder.invoke(any())).thenReturn(getRequest)
whenever(clientFactory.createPlainClient()).thenReturn(client) whenever(clientFactory.createPlainClient()).thenReturn(client)
whenever(user.server).thenReturn(newServer) whenever(user.server).thenReturn(newServer)
@ -235,12 +251,55 @@ class ConnectivityServiceTest {
whenever(networkInfo.isConnectedOrConnecting).thenReturn(true) whenever(networkInfo.isConnectedOrConnecting).thenReturn(true)
whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI) whenever(networkInfo.type).thenReturn(ConnectivityManager.TYPE_WIFI)
whenever(accountManager.getServerVersion(any())).thenReturn(OwnCloudVersion.nextcloud_20) whenever(accountManager.getServerVersion(any())).thenReturn(OwnCloudVersion.nextcloud_20)
assertTrue(
"Precondition failed",
connectivityService.connectivity.let { connectivityService.connectivity.let {
it.isConnected && it.isWifi assertTrue(it.isConnected)
assertTrue(it.isWifi)
assertFalse(it.isMetered)
} }
) }
@Test
fun `request not sent when not connected`() {
// GIVEN
// network is not connected
whenever(networkInfo.isConnectedOrConnecting).thenReturn(false)
whenever(networkInfo.isConnected).thenReturn(false)
// WHEN
// connectivity is checked
val result = connectivityService.isInternetWalled
// THEN
// connection is walled
// request is not sent
assertTrue("Server should not be accessible", result)
verify(requestBuilder, never()).invoke(any())
verify(client, never()).execute(any())
}
@Test
fun `request not sent when wifi is metered`() {
// GIVEN
// network is connected to wifi
// wifi is metered
whenever(networkCapabilities.hasCapability(any())).thenReturn(false) // this test is mocked for API M
whenever(platformConnectivityManager.isActiveNetworkMetered).thenReturn(true)
connectivityService.connectivity.let {
assertTrue("should be connected", it.isConnected)
assertTrue("should be connected to wifi", it.isWifi)
assertTrue("check mocking, this check is complicated and depends on SDK version", it.isMetered)
}
// WHEN
// connectivity is checked
val result = connectivityService.isInternetWalled
// THEN
// assume internet is not walled
// request is not sent
assertFalse("Server should not be accessible", result)
verify(requestBuilder, never()).invoke(any())
verify(getRequest, never()).execute(any<PlainClient>())
} }
@Test @Test
@ -260,7 +319,7 @@ class ConnectivityServiceTest {
// request is not sent // request is not sent
assertTrue("Server should not be accessible", result) assertTrue("Server should not be accessible", result)
verify(requestBuilder, never()).invoke(any()) verify(requestBuilder, never()).invoke(any())
verify(client, never()).execute(any()) verify(getRequest, never()).execute(any<PlainClient>())
} }
fun mockResponse(contentLength: Long = 0, status: Int = HttpStatus.SC_OK) { fun mockResponse(contentLength: Long = 0, status: Int = HttpStatus.SC_OK) {
@ -274,18 +333,21 @@ class ConnectivityServiceTest {
fun `status 204 means internet is not walled`() { fun `status 204 means internet is not walled`() {
mockResponse(contentLength = 0, status = HttpStatus.SC_NO_CONTENT) mockResponse(contentLength = 0, status = HttpStatus.SC_NO_CONTENT)
assertFalse(connectivityService.isInternetWalled) assertFalse(connectivityService.isInternetWalled)
verify(getRequest, times(1)).execute(client)
} }
@Test @Test
fun `status 204 and no content length means internet is not walled`() { fun `status 204 and no content length means internet is not walled`() {
mockResponse(contentLength = -1, status = HttpStatus.SC_NO_CONTENT) mockResponse(contentLength = -1, status = HttpStatus.SC_NO_CONTENT)
assertFalse(connectivityService.isInternetWalled) assertFalse(connectivityService.isInternetWalled)
verify(getRequest, times(1)).execute(client)
} }
@Test @Test
fun `other status than 204 means internet is walled`() { fun `other status than 204 means internet is walled`() {
mockResponse(contentLength = 0, status = HttpStatus.SC_GONE) mockResponse(contentLength = 0, status = HttpStatus.SC_GONE)
assertTrue(connectivityService.isInternetWalled) assertTrue(connectivityService.isInternetWalled)
verify(getRequest, times(1)).execute(client)
} }
@Test @Test
@ -295,6 +357,7 @@ class ConnectivityServiceTest {
val urlCaptor = ArgumentCaptor.forClass(String::class.java) val urlCaptor = ArgumentCaptor.forClass(String::class.java)
verify(requestBuilder).invoke(urlCaptor.capture()) verify(requestBuilder).invoke(urlCaptor.capture())
assertTrue("Invalid URL used to check status", urlCaptor.value.endsWith("/index.php/204")) assertTrue("Invalid URL used to check status", urlCaptor.value.endsWith("/index.php/204"))
verify(getRequest, times(1)).execute(client)
} }
} }
} }