Skip to content
This repository has been archived by the owner on Feb 11, 2022. It is now read-only.

Commit

Permalink
Merge pull request #179 from novoda/develop
Browse files Browse the repository at this point in the history
Captive Portal to release
  • Loading branch information
Ryan Feline authored Oct 29, 2018
2 parents 026b47e + 4967bb1 commit dbb5698
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 23 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
allprojects {
version = '1.1.8'
version = '1.2.0'
repositories {
mavenCentral()
google()
Expand Down
6 changes: 3 additions & 3 deletions core/src/main/java/com/novoda/merlin/Endpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions core/src/main/java/com/novoda/merlin/MerlinBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -14,8 +14,8 @@ public class MerlinBuilder {
private Register<Disconnectable> disconnectables;
private Register<Bindable> bindables;

private Endpoint endpoint = Endpoint.defaultEndpoint();
private ResponseCodeValidator responseCodeValidator = new DefaultEndpointResponseCodeValidator();
private Endpoint endpoint = Endpoint.captivePortalEndpoint();
private ResponseCodeValidator responseCodeValidator = new CaptivePortalResponseCodeValidator();

MerlinBuilder() {
}
Expand Down
53 changes: 48 additions & 5 deletions core/src/main/java/com/novoda/merlin/MerlinsBeard.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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 {
}

}
46 changes: 46 additions & 0 deletions core/src/main/java/com/novoda/merlin/MerlinsBeardBuilder.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
59 changes: 57 additions & 2 deletions core/src/test/java/com/novoda/merlin/MerlinsBeardTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -138,6 +143,56 @@ public void givenNetworkIsDisconnected_andAndroidVersionIsBelowLollipop_whenChec
assertThat(connectedToMobileNetwork).isFalse();
}

@Test
public void givenSuccessfulPing_whenCheckingCaptivePortal_thenCallsOnResultWithTrue() {
willAnswer(new Answer<EndpointPinger.PingerCallback>() {
@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<EndpointPinger.PingerCallback>() {
@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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,14 +24,14 @@ public static Collection<Object[]> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit dbb5698

Please sign in to comment.