diff --git a/build.gradle b/build.gradle index af41ce2..02e00ad 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ allprojects { - version = '1.1.8' + version = '1.2.0' repositories { mavenCentral() google() diff --git a/core/src/main/java/com/novoda/merlin/Endpoint.java b/core/src/main/java/com/novoda/merlin/Endpoint.java index 8ba8c14..423355c 100644 --- a/core/src/main/java/com/novoda/merlin/Endpoint.java +++ b/core/src/main/java/com/novoda/merlin/Endpoint.java @@ -5,12 +5,12 @@ public class Endpoint { - private static final Endpoint DEFAULT_ENDPOINT = Endpoint.from("http://connectivitycheck.android.com/generate_204"); + private static final Endpoint CAPTIVE_PORTAL_ENDPOINT = Endpoint.from("https://connectivitycheck.android.com/generate_204"); private final String endpoint; - public static Endpoint defaultEndpoint() { - return DEFAULT_ENDPOINT; + public static Endpoint captivePortalEndpoint() { + return CAPTIVE_PORTAL_ENDPOINT; } public static Endpoint from(String endpoint) { diff --git a/core/src/main/java/com/novoda/merlin/MerlinBuilder.java b/core/src/main/java/com/novoda/merlin/MerlinBuilder.java index 50887a3..514bbde 100644 --- a/core/src/main/java/com/novoda/merlin/MerlinBuilder.java +++ b/core/src/main/java/com/novoda/merlin/MerlinBuilder.java @@ -2,7 +2,7 @@ import android.content.Context; -import static com.novoda.merlin.ResponseCodeValidator.DefaultEndpointResponseCodeValidator; +import static com.novoda.merlin.ResponseCodeValidator.CaptivePortalResponseCodeValidator; public class MerlinBuilder { @@ -14,8 +14,8 @@ public class MerlinBuilder { private Register disconnectables; private Register bindables; - private Endpoint endpoint = Endpoint.defaultEndpoint(); - private ResponseCodeValidator responseCodeValidator = new DefaultEndpointResponseCodeValidator(); + private Endpoint endpoint = Endpoint.captivePortalEndpoint(); + private ResponseCodeValidator responseCodeValidator = new CaptivePortalResponseCodeValidator(); MerlinBuilder() { } diff --git a/core/src/main/java/com/novoda/merlin/MerlinsBeard.java b/core/src/main/java/com/novoda/merlin/MerlinsBeard.java index 7cf6ddd..1ea74ca 100644 --- a/core/src/main/java/com/novoda/merlin/MerlinsBeard.java +++ b/core/src/main/java/com/novoda/merlin/MerlinsBeard.java @@ -6,6 +6,7 @@ import android.net.Network; import android.net.NetworkInfo; import android.os.Build; +import android.support.annotation.WorkerThread; /** * This class provides a mechanism for retrieving the current @@ -17,22 +18,27 @@ public class MerlinsBeard { private final ConnectivityManager connectivityManager; private final AndroidVersion androidVersion; + private final EndpointPinger captivePortalPinger; + private final Ping captivePortalPing; /** - * Use this method to create a MerlinsBeard object, this is how you can retrieve the current network state. + * @deprecated Use {@link MerlinsBeard.Builder} instead. * + * Use this method to create a MerlinsBeard object, this is how you can retrieve the current network state. * @param context pass any context application or activity. * @return MerlinsBeard. */ + @Deprecated public static MerlinsBeard from(Context context) { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); - AndroidVersion androidVersion = new AndroidVersion(); - return new MerlinsBeard(connectivityManager, androidVersion); + return new MerlinsBeard.Builder() + .build(context); } - MerlinsBeard(ConnectivityManager connectivityManager, AndroidVersion androidVersion) { + MerlinsBeard(ConnectivityManager connectivityManager, AndroidVersion androidVersion, EndpointPinger captivePortalPinger, Ping CaptivePortalPing) { this.connectivityManager = connectivityManager; this.androidVersion = androidVersion; + this.captivePortalPinger = captivePortalPinger; + this.captivePortalPing = CaptivePortalPing; } /** @@ -116,4 +122,41 @@ public String getMobileNetworkSubtypeName() { return networkInfo.getSubtypeName(); } + /** + * Detects if a client has internet access by pinging an {@link Endpoint}. + * + * @param callback to call with boolean result representing if a client has internet access. + */ + public void hasInternetAccess(final InternetAccessCallback callback) { + captivePortalPinger.ping(new EndpointPinger.PingerCallback() { + @Override + public void onSuccess() { + callback.onResult(true); + } + + @Override + public void onFailure() { + callback.onResult(false); + } + }); + } + + /** + * Synchronously detects if a client has internet access by pinging an {@link Endpoint}. + * Clients are expected to handle their own threading. + * + * @return Boolean result representing if a client has internet access. + */ + @WorkerThread + public boolean hasInternetAccess() { + return captivePortalPing.doSynchronousPing(); + } + + public interface InternetAccessCallback { + void onResult(boolean hasAccess); + } + + public static class Builder extends MerlinsBeardBuilder { + } + } diff --git a/core/src/main/java/com/novoda/merlin/MerlinsBeardBuilder.java b/core/src/main/java/com/novoda/merlin/MerlinsBeardBuilder.java new file mode 100644 index 0000000..c51dd40 --- /dev/null +++ b/core/src/main/java/com/novoda/merlin/MerlinsBeardBuilder.java @@ -0,0 +1,46 @@ +package com.novoda.merlin; + +import android.content.Context; +import android.net.ConnectivityManager; + +public class MerlinsBeardBuilder { + + private Endpoint endpoint = Endpoint.captivePortalEndpoint(); + private ResponseCodeValidator responseCodeValidator = new ResponseCodeValidator.CaptivePortalResponseCodeValidator(); + + MerlinsBeardBuilder() { + // Uses builder pattern. + } + + /** + * Sets a custom endpoint. + * + * @param endpoint to ping, by default {@link Endpoint#CAPTIVE_PORTAL_ENDPOINT}. + * @return MerlinsBeardBuilder. + */ + public MerlinsBeardBuilder withEndpoint(Endpoint endpoint) { + this.endpoint = endpoint; + return this; + } + + /** + * Validator used to check the response code when performing a ping. + * + * @param responseCodeValidator A validator implementation used for checking that the response code is what you expect. + * The default endpoint returns a 204 No Content response, so the default validator checks for that. + * @return MerlinsBeardBuilder. + */ + public MerlinsBeardBuilder withResponseCodeValidator(ResponseCodeValidator responseCodeValidator) { + this.responseCodeValidator = responseCodeValidator; + return this; + } + + public MerlinsBeard build(Context context) { + ConnectivityManager connectivityManager = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); + AndroidVersion androidVersion = new AndroidVersion(); + EndpointPinger captivePortalpinger = EndpointPinger.withCustomEndpointAndValidation(endpoint, responseCodeValidator); + Ping captivePortalPing = new Ping(endpoint, new EndpointPinger.ResponseCodeFetcher(), responseCodeValidator); + + return new MerlinsBeard(connectivityManager, androidVersion, captivePortalpinger, captivePortalPing); + } +} diff --git a/core/src/main/java/com/novoda/merlin/ResponseCodeValidator.java b/core/src/main/java/com/novoda/merlin/ResponseCodeValidator.java index 945202e..06d3052 100644 --- a/core/src/main/java/com/novoda/merlin/ResponseCodeValidator.java +++ b/core/src/main/java/com/novoda/merlin/ResponseCodeValidator.java @@ -4,7 +4,7 @@ public interface ResponseCodeValidator { boolean isResponseCodeValid(int responseCode); - class DefaultEndpointResponseCodeValidator implements ResponseCodeValidator { + class CaptivePortalResponseCodeValidator implements ResponseCodeValidator { @Override public boolean isResponseCodeValid(int responseCode) { return responseCode == 204; diff --git a/core/src/test/java/com/novoda/merlin/MerlinsBeardTest.java b/core/src/test/java/com/novoda/merlin/MerlinsBeardTest.java index ad761ca..2ba6ac9 100644 --- a/core/src/test/java/com/novoda/merlin/MerlinsBeardTest.java +++ b/core/src/test/java/com/novoda/merlin/MerlinsBeardTest.java @@ -8,9 +8,11 @@ import org.junit.Before; import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.*; import static org.mockito.Mockito.mock; public class MerlinsBeardTest { @@ -24,12 +26,15 @@ public class MerlinsBeardTest { private final ConnectivityManager connectivityManager = mock(ConnectivityManager.class); private final NetworkInfo networkInfo = mock(NetworkInfo.class); private final AndroidVersion androidVersion = mock(AndroidVersion.class); + private final EndpointPinger endpointPinger = mock(EndpointPinger.class); + private final MerlinsBeard.InternetAccessCallback mockCaptivePortalCallback = mock(MerlinsBeard.InternetAccessCallback.class); + private final Ping mockPing = mock(Ping.class); private MerlinsBeard merlinsBeard; @Before public void setUp() { - merlinsBeard = new MerlinsBeard(connectivityManager, androidVersion); + merlinsBeard = new MerlinsBeard(connectivityManager, androidVersion, endpointPinger, mockPing); } @Test @@ -138,6 +143,56 @@ public void givenNetworkIsDisconnected_andAndroidVersionIsBelowLollipop_whenChec assertThat(connectedToMobileNetwork).isFalse(); } + @Test + public void givenSuccessfulPing_whenCheckingCaptivePortal_thenCallsOnResultWithTrue() { + willAnswer(new Answer() { + @Override + public EndpointPinger.PingerCallback answer(InvocationOnMock invocation) { + EndpointPinger.PingerCallback cb = invocation.getArgument(0); + cb.onSuccess(); + return cb; + } + }).given(endpointPinger).ping(any(EndpointPinger.PingerCallback.class)); + + merlinsBeard.hasInternetAccess(mockCaptivePortalCallback); + + then(mockCaptivePortalCallback).should().onResult(true); + } + + @Test + public void givenFailurePing_whenCheckingCaptivePortal_thenCallsOnResultWithFalse() { + willAnswer(new Answer() { + @Override + public EndpointPinger.PingerCallback answer(InvocationOnMock invocation) { + EndpointPinger.PingerCallback cb = invocation.getArgument(0); + cb.onFailure(); + return cb; + } + }).given(endpointPinger).ping(any(EndpointPinger.PingerCallback.class)); + + merlinsBeard.hasInternetAccess(mockCaptivePortalCallback); + + then(mockCaptivePortalCallback).should().onResult(false); + } + + @Test + public void givenSuccessfulPing_whenCheckingHasInternetAccessSync_thenReturnsTrue() { + given(mockPing.doSynchronousPing()).willReturn(true); + + boolean result = merlinsBeard.hasInternetAccess(); + + assertThat(result).isTrue(); + } + + @Test + public void givenFailedPing_whenCheckingHasInternetAccessSync_thenReturnsFalse() { + given(mockPing.doSynchronousPing()).willReturn(false); + + boolean result = merlinsBeard.hasInternetAccess(); + + assertThat(result).isFalse(); + } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) @Test public void givenNetworkIsConnectedViaMobile_andAndroidVersionIsLollipopOrAbove_whenCheckingIfConnectedToMobile_thenReturnsTrue() { diff --git a/core/src/test/java/com/novoda/merlin/ResponseCodeValidatorTest.java b/core/src/test/java/com/novoda/merlin/ResponseCodeValidatorTest.java index 2073893..e661c41 100644 --- a/core/src/test/java/com/novoda/merlin/ResponseCodeValidatorTest.java +++ b/core/src/test/java/com/novoda/merlin/ResponseCodeValidatorTest.java @@ -9,12 +9,12 @@ import org.junit.runners.Parameterized; import static com.google.common.truth.Truth.assertThat; -import static com.novoda.merlin.ResponseCodeValidator.DefaultEndpointResponseCodeValidator; +import static com.novoda.merlin.ResponseCodeValidator.CaptivePortalResponseCodeValidator; public class ResponseCodeValidatorTest { @RunWith(Parameterized.class) - public static class DefaultEndpointResponseCodeValidatorTest { + public static class CaptivePortalResponseCodeValidatorTest { private final int responseCode; private final boolean isValid; @@ -24,14 +24,14 @@ public static Collection data() { return Responses.toParameterList(); } - public DefaultEndpointResponseCodeValidatorTest(int responseCode, boolean isValid) { + public CaptivePortalResponseCodeValidatorTest(int responseCode, boolean isValid) { this.responseCode = responseCode; this.isValid = isValid; } @Test public void whenCheckingResponseCodeValidity() { - boolean actual = new DefaultEndpointResponseCodeValidator().isResponseCodeValid(responseCode); + boolean actual = new CaptivePortalResponseCodeValidator().isResponseCodeValid(responseCode); assertThat(actual).isEqualTo(isValid); } diff --git a/demo/src/main/java/com/novoda/merlin/demo/presentation/DemoActivity.java b/demo/src/main/java/com/novoda/merlin/demo/presentation/DemoActivity.java index 58d22b0..aeff9a0 100644 --- a/demo/src/main/java/com/novoda/merlin/demo/presentation/DemoActivity.java +++ b/demo/src/main/java/com/novoda/merlin/demo/presentation/DemoActivity.java @@ -26,10 +26,12 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.main); viewToAttachDisplayerTo = findViewById(R.id.displayerAttachableView); - merlinsBeard = MerlinsBeard.from(this); + merlinsBeard = new MerlinsBeard.Builder() + .build(this); networkStatusDisplayer = new NetworkStatusDisplayer(getResources(), merlinsBeard); findViewById(R.id.current_status).setOnClickListener(networkStatusOnClick); + findViewById(R.id.has_internet_access).setOnClickListener(hasInternetAccessClick); findViewById(R.id.wifi_connected).setOnClickListener(wifiConnectedOnClick); findViewById(R.id.mobile_connected).setOnClickListener(mobileConnectedOnClick); findViewById(R.id.network_subtype).setOnClickListener(networkSubtypeOnClick); @@ -47,6 +49,22 @@ public void onClick(View v) { } }; + private final View.OnClickListener hasInternetAccessClick = new View.OnClickListener() { + @Override + public void onClick(final View view) { + merlinsBeard.hasInternetAccess(new MerlinsBeard.InternetAccessCallback() { + @Override + public void onResult(boolean hasAccess) { + if (hasAccess) { + networkStatusDisplayer.displayPositiveMessage(R.string.has_internet_access_true, viewToAttachDisplayerTo); + } else { + networkStatusDisplayer.displayNegativeMessage(R.string.has_internet_access_false, viewToAttachDisplayerTo); + } + } + }); + } + }; + private final View.OnClickListener wifiConnectedOnClick = new View.OnClickListener() { @Override diff --git a/demo/src/main/java/com/novoda/merlin/demo/presentation/RxJava2DemoActivity.java b/demo/src/main/java/com/novoda/merlin/demo/presentation/RxJava2DemoActivity.java index 5140e93..7bf122c 100644 --- a/demo/src/main/java/com/novoda/merlin/demo/presentation/RxJava2DemoActivity.java +++ b/demo/src/main/java/com/novoda/merlin/demo/presentation/RxJava2DemoActivity.java @@ -28,11 +28,13 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); viewToAttachDisplayerTo = findViewById(R.id.displayerAttachableView); - merlinsBeard = MerlinsBeard.from(this); + merlinsBeard = new MerlinsBeard.Builder() + .build(this); networkStatusDisplayer = new NetworkStatusDisplayer(getResources(), merlinsBeard); disposables = new CompositeDisposable(); findViewById(R.id.current_status).setOnClickListener(networkStatusOnClick); + findViewById(R.id.has_internet_access).setOnClickListener(hasInternetAccessClick); findViewById(R.id.wifi_connected).setOnClickListener(wifiConnectedOnClick); findViewById(R.id.mobile_connected).setOnClickListener(mobileConnectedOnClick); findViewById(R.id.network_subtype).setOnClickListener(networkSubtypeOnClick); @@ -62,6 +64,22 @@ public void onClick(View view) { } }; + private final View.OnClickListener hasInternetAccessClick = new View.OnClickListener() { + @Override + public void onClick(final View view) { + merlinsBeard.hasInternetAccess(new MerlinsBeard.InternetAccessCallback() { + @Override + public void onResult(boolean hasAccess) { + if (hasAccess) { + networkStatusDisplayer.displayPositiveMessage(R.string.has_internet_access_true, viewToAttachDisplayerTo); + } else { + networkStatusDisplayer.displayNegativeMessage(R.string.has_internet_access_false, viewToAttachDisplayerTo); + } + } + }); + } + }; + private final View.OnClickListener mobileConnectedOnClick = new View.OnClickListener() { @Override diff --git a/demo/src/main/java/com/novoda/merlin/demo/presentation/RxJavaDemoActivity.java b/demo/src/main/java/com/novoda/merlin/demo/presentation/RxJavaDemoActivity.java index df1ddf6..e65e406 100644 --- a/demo/src/main/java/com/novoda/merlin/demo/presentation/RxJavaDemoActivity.java +++ b/demo/src/main/java/com/novoda/merlin/demo/presentation/RxJavaDemoActivity.java @@ -27,11 +27,13 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); viewToAttachDisplayerTo = findViewById(R.id.displayerAttachableView); - merlinsBeard = MerlinsBeard.from(this); + merlinsBeard = new MerlinsBeard.Builder() + .build(this); networkStatusDisplayer = new NetworkStatusDisplayer(getResources(), merlinsBeard); subscriptions = new CompositeSubscription(); findViewById(R.id.current_status).setOnClickListener(networkStatusOnClick); + findViewById(R.id.has_internet_access).setOnClickListener(hasInternetAccessClick); findViewById(R.id.wifi_connected).setOnClickListener(wifiConnectedOnClick); findViewById(R.id.mobile_connected).setOnClickListener(mobileConnectedOnClick); findViewById(R.id.network_subtype).setOnClickListener(networkSubtypeOnClick); @@ -49,6 +51,22 @@ public void onClick(View v) { } }; + private final View.OnClickListener hasInternetAccessClick = new View.OnClickListener() { + @Override + public void onClick(final View view) { + merlinsBeard.hasInternetAccess(new MerlinsBeard.InternetAccessCallback() { + @Override + public void onResult(boolean hasAccess) { + if (hasAccess) { + networkStatusDisplayer.displayPositiveMessage(R.string.has_internet_access_true, viewToAttachDisplayerTo); + } else { + networkStatusDisplayer.displayNegativeMessage(R.string.has_internet_access_false, viewToAttachDisplayerTo); + } + } + }); + } + }; + private final View.OnClickListener wifiConnectedOnClick = new View.OnClickListener() { @Override diff --git a/demo/src/main/res/layout/main.xml b/demo/src/main/res/layout/main.xml index e39a7be..a3f1faf 100644 --- a/demo/src/main/res/layout/main.xml +++ b/demo/src/main/res/layout/main.xml @@ -42,6 +42,13 @@ android:layout_gravity="center_horizontal" android:text="@string/get_network_subtype" /> +