diff --git a/README.md b/README.md index f1cd1fec..36a3c907 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ You only need to use the Builder setters like: ```java Intent intent = new LocationPickerActivity.Builder() .withLocation(41.4036299, 2.1743558) + .withGeolocApiKey("") .withSearchZone("es_ES") .shouldReturnOkOnBackPressed() .withStreetHidden() @@ -305,6 +306,13 @@ Available tracking events are: |RESULT_OK|Return location| |CANCEL|Return without location| +#### Geocoding API Fallback + +In few cases, the geocoding service from Android fails due to an issue with the NetworkLocator. The only way of fixing this is rebooting the device. + +In order to cover these cases, you can instruct Leku to use the Geocoding API. To enable it, just use the method '''withGeolocApiKey''' when invoking the LocationPicker. + +You should provide your Server Key as parameter. Keep in mind that the free tier only allows 2,500 requests per day. You can track how many times is it used in the Developer Console from Google. #### Extra diff --git a/app/src/main/java/com/schibsted/mappicker/MainActivity.java b/app/src/main/java/com/schibsted/mappicker/MainActivity.java index ebe4cb85..11ba38a2 100644 --- a/app/src/main/java/com/schibsted/mappicker/MainActivity.java +++ b/app/src/main/java/com/schibsted/mappicker/MainActivity.java @@ -36,6 +36,7 @@ protected void onCreate(Bundle savedInstanceState) { public void onClick(View view) { Intent locationPickerIntent = new LocationPickerActivity.Builder() .withLocation(41.4036299, 2.1743558) + //.withGeolocApiKey("") //.withSearchZone("es_ES") //.shouldReturnOkOnBackPressed() //.withStreetHidden() diff --git a/leku/src/androidTest/java/com/schibsted/leku/geocoder/api/AddressBuilderShould.java b/leku/src/androidTest/java/com/schibsted/leku/geocoder/api/AddressBuilderShould.java new file mode 100644 index 00000000..35cd8fc9 --- /dev/null +++ b/leku/src/androidTest/java/com/schibsted/leku/geocoder/api/AddressBuilderShould.java @@ -0,0 +1,85 @@ +package com.schibsted.leku.geocoder.api; + +import android.location.Address; +import android.support.annotation.NonNull; +import com.schibstedspain.leku.geocoder.api.AddressBuilder; +import java.util.List; +import org.json.JSONException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.junit.MockitoRule; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; +import static org.mockito.junit.MockitoJUnit.rule; + +public class AddressBuilderShould { + + @Rule public MockitoRule mockitoRule = rule(); + + private AddressBuilder addressBuilder; + + @Before + public void setUp() { + addressBuilder = new AddressBuilder(); + } + + @Test + public void returnExpectedAddressWhenJsonProvided() throws JSONException { + String json = getJson(); + + List
addresses = addressBuilder.parseResult(json); + + assertEquals("Barcelona", addresses.get(0).getLocality()); + assertEquals("Carrer del Comte d'Urgell, 102", addresses.get(0).getAddressLine(0)); + assertEquals("08011", addresses.get(0).getPostalCode()); + assertTrue(Double.valueOf(41.3838035).equals(addresses.get(0).getLatitude())); + assertTrue(Double.valueOf(2.1568617).equals(addresses.get(0).getLongitude())); + + } + + @Test + public void returnExpectedAddressWhenJsonWithOnlyCityProvided() throws JSONException { + String json = getJsonForOnlyCity(); + + List
addresses = addressBuilder.parseResult(json); + + assertEquals("Barcelona", addresses.get(0).getLocality()); + assertEquals("", addresses.get(0).getAddressLine(0)); + assertEquals("", addresses.get(0).getPostalCode()); + assertTrue(Double.valueOf(41.3850639).equals(addresses.get(0).getLatitude())); + assertTrue(Double.valueOf(2.1734035).equals(addresses.get(0).getLongitude())); + + } + + @NonNull + private String getJson() { + return "{\"results\": [{\"address_components\": [{\"long_name\": \"102\",\"short_name\": \"102\",\"types\": " + + "[ \"street_number\"]},{\"long_name\": \"Carrer del Comte d'Urgell\",\"short_name\": \"Carrer del Comte d'Urgell\",\"types\": " + + "[ \"route\"]},{\"long_name\": \"Barcelona\",\"short_name\": \"Barcelona\",\"types\": [ \"locality\", \"political\"]}," + + "{\"long_name\": \"Barcelona\",\"short_name\": \"Barcelona\",\"types\": [ \"administrative_area_level_2\", \"political\"]}," + + "{\"long_name\": \"Catalunya\",\"short_name\": \"CT\",\"types\": [ \"administrative_area_level_1\", \"political\"]}," + + "{\"long_name\": \"Spain\",\"short_name\": \"ES\",\"types\": [ \"country\", \"political\"]},{\"long_name\": \"08011\"," + + "\"short_name\": \"08011\",\"types\": [ \"postal_code\"]}],\"formatted_address\"" + + ": \"Carrer del Comte d'Urgell, 102, 08011 Barcelona, Spain\",\"geometry\": {\"bounds\": {\"northeast\": " + + "{ \"lat\": 41.3839416, \"lng\": 2.1570442},\"southwest\": { \"lat\": 41.3836653, \"lng\": 2.1566792}},\"location\": " + + "{\"lat\": 41.3838035,\"lng\": 2.1568617},\"location_type\": \"ROOFTOP\",\"viewport\": {\"northeast\": " + + "{ \"lat\": 41.3851524302915, \"lng\": 2.158210680291502},\"southwest\": { \"lat\": 41.3824544697085, " + + "\"lng\": 2.155512719708498}}},\"partial_match\": true,\"place_id\": \"ChIJdehx-YiipBIR8hitzOckUuo\",\"types\": [\"premise\"] } " + + "], \"status\": \"OK\"}"; + } + + @NonNull + private String getJsonForOnlyCity() { + return "{\"results\": [{\"address_components\": [{\"long_name\": \"Barcelona\",\"short_name\": \"Barcelona\",\"types\": " + + "[\"locality\",\"political\"]},{\"long_name\": \"Barcelona\",\"short_name\": \"Barcelona\",\"types\": " + + "[\"administrative_area_level_2\",\"political\"]},{\"long_name\": \"Catalonia\",\"short_name\": \"CT\",\"types\": " + + "[\"administrative_area_level_1\",\"political\"]},{\"long_name\": \"Spain\",\"short_name\": \"ES\",\"types\": " + + "[\"country\",\"political\"]}],\"formatted_address\": \"Barcelona, Spain\",\"geometry\": {\"bounds\": {\"northeast\": " + + "{\"lat\": 41.4695761,\"lng\": 2.2280099},\"southwest\": {\"lat\": 41.320004,\"lng\": 2.0695258}},\"location\":" + + " {\"lat\": 41.3850639,\"lng\": 2.1734035},\"location_type\": \"APPROXIMATE\",\"viewport\": {\"northeast\": " + + "{\"lat\": 41.4695761,\"lng\": 2.2280099},\"southwest\": {\"lat\": 41.320004,\"lng\": 2.0695258}}},\"place_id\": " + + "\"ChIJ5TCOcRaYpBIRCmZHTz37sEQ\",\"types\": [\"locality\",\"political\"]}],\"status\": \"OK\"}"; + } +} \ No newline at end of file diff --git a/leku/src/main/java/com/schibstedspain/leku/LocationPickerActivity.java b/leku/src/main/java/com/schibstedspain/leku/LocationPickerActivity.java index 8efd5e07..86e2a2de 100644 --- a/leku/src/main/java/com/schibstedspain/leku/LocationPickerActivity.java +++ b/leku/src/main/java/com/schibstedspain/leku/LocationPickerActivity.java @@ -44,9 +44,12 @@ import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; +import com.schibstedspain.leku.geocoder.GeocoderAPIInteractor; import com.schibstedspain.leku.geocoder.GeocoderInteractor; import com.schibstedspain.leku.geocoder.GeocoderPresenter; import com.schibstedspain.leku.geocoder.GeocoderViewInterface; +import com.schibstedspain.leku.geocoder.api.AddressBuilder; +import com.schibstedspain.leku.geocoder.api.NetworkClient; import com.schibstedspain.leku.permissions.PermissionUtils; import com.schibstedspain.leku.tracker.TrackEvents; import java.util.ArrayList; @@ -77,6 +80,7 @@ public class LocationPickerActivity extends AppCompatActivity public static final String ENABLE_LOCATION_PERMISSION_REQUEST = "enable_location_permission_request"; public static final String POIS_LIST = "pois_list"; public static final String LEKU_POI = "leku_poi"; + private static final String GEOLOC_API_KEY = "geoloc_api_key"; private static final String LOCATION_KEY = "location_key"; private static final String LAST_LOCATION_QUERY = "last_location_query"; private static final String OPTIONS_HIDE_STREET = "street"; @@ -125,6 +129,7 @@ public class LocationPickerActivity extends AppCompatActivity private Map lekuPoisMarkersMap; private Marker currentMarker; private TextWatcher textWatcher; + private GeocoderAPIInteractor apiInteractor; @Override protected void onCreate(Bundle savedInstanceState) { @@ -154,8 +159,9 @@ protected void track(TrackEvents event) { private void setUpMainVariables() { Geocoder geocoder = new Geocoder(this, Locale.getDefault()); + apiInteractor = new GeocoderAPIInteractor(new NetworkClient(), new AddressBuilder()); geocoderPresenter = new GeocoderPresenter(new ReactiveLocationProvider(getApplicationContext()), - new GeocoderInteractor(geocoder)); + new GeocoderInteractor(geocoder), apiInteractor); geocoderPresenter.setUI(this); progressBar = (ProgressBar) findViewById(R.id.loading_progress_bar); progressBar.setVisibility(View.GONE); @@ -647,6 +653,9 @@ private void getSavedInstanceParams(Bundle savedInstanceState) { if (savedInstanceState.keySet().contains(LAYOUTS_TO_HIDE)) { setLayoutVisibilityFromBundle(savedInstanceState); } + if (savedInstanceState.keySet().contains(GEOLOC_API_KEY)) { + apiInteractor.setApiKey(savedInstanceState.getString(GEOLOC_API_KEY)); + } if (savedInstanceState.keySet().contains(SEARCH_ZONE)) { searchZone = savedInstanceState.getString(SEARCH_ZONE); } @@ -685,6 +694,9 @@ private void getTransitionBundleParams(Bundle transitionBundle) { if (transitionBundle.keySet().contains(POIS_LIST)) { poisList = transitionBundle.getParcelableArrayList(POIS_LIST); } + if (transitionBundle.keySet().contains(GEOLOC_API_KEY)) { + apiInteractor.setApiKey(transitionBundle.getString(GEOLOC_API_KEY)); + } } private void setLayoutVisibilityFromBundle(Bundle transitionBundle) { @@ -1057,6 +1069,7 @@ public static class Builder { private boolean enableSatelliteView = true; private boolean shouldReturnOkOnBackPressed = false; private List lekuPois; + private String geolocApiKey = null; public Builder() { } @@ -1110,6 +1123,11 @@ public Builder withPois(List pois) { return this; } + public Builder withGeolocApiKey(String apiKey) { + this.geolocApiKey = apiKey; + return this; + } + public Intent build(Context context) { Intent intent = new Intent(context, LocationPickerActivity.class); @@ -1130,6 +1148,9 @@ public Intent build(Context context) { if (lekuPois != null && !lekuPois.isEmpty()) { intent.putExtra(POIS_LIST, new ArrayList<>(lekuPois)); } + if (geolocApiKey != null) { + intent.putExtra(GEOLOC_API_KEY, geolocApiKey); + } return intent; } diff --git a/leku/src/main/java/com/schibstedspain/leku/geocoder/GeocoderAPIInteractor.java b/leku/src/main/java/com/schibstedspain/leku/geocoder/GeocoderAPIInteractor.java new file mode 100644 index 00000000..8719277e --- /dev/null +++ b/leku/src/main/java/com/schibstedspain/leku/geocoder/GeocoderAPIInteractor.java @@ -0,0 +1,89 @@ +package com.schibstedspain.leku.geocoder; + +import android.location.Address; +import com.google.android.gms.maps.model.LatLng; +import com.schibstedspain.leku.geocoder.api.AddressBuilder; +import com.schibstedspain.leku.geocoder.api.NetworkClient; +import java.util.List; +import java.util.Locale; +import org.json.JSONException; +import rx.Observable; + +public class GeocoderAPIInteractor implements GeocoderInteractorInterface { + + private static final String QUERY_REQUEST = "https://maps.googleapis.com/maps/api/geocode/json?address=%1$s&key=%2$s"; + private static final String QUERY_REQUEST_WITH_RECTANGLE + = "https://maps.googleapis.com/maps/api/geocode/json?address=%1$s&key=%2$s&bounds=%3$f,%4$f|%5$f,%6$f"; + private static final String QUERY_LAT_LONG = "https://maps.googleapis.com/maps/api/geocode/json?latlng=%1$f,%2$f&key=%3$s"; + private String apiKey; + private final NetworkClient networkClient; + private final AddressBuilder addressBuilder; + + public GeocoderAPIInteractor(NetworkClient networkClient, AddressBuilder addressBuilder) { + this.networkClient = networkClient; + this.addressBuilder = addressBuilder; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public Observable> getFromLocationName(String query) { + return Observable.create(subscriber -> { + if (apiKey == null) { + subscriber.onCompleted(); + return; + } + try { + String result = networkClient.requestFromLocationName(String.format(Locale.ENGLISH, + QUERY_REQUEST, query.trim(), apiKey)); + List
addresses = addressBuilder.parseResult(result); + subscriber.onNext(addresses); + subscriber.onCompleted(); + } catch (JSONException e) { + subscriber.onError(e); + } + }); + } + + @Override + public Observable> getFromLocationName(String query, LatLng lowerLeft, + LatLng upperRight) { + return Observable.create(subscriber -> { + if (apiKey == null) { + subscriber.onCompleted(); + return; + } + try { + String result = networkClient.requestFromLocationName(String.format(Locale.ENGLISH, + QUERY_REQUEST_WITH_RECTANGLE, query.trim(), apiKey, lowerLeft.latitude, + lowerLeft.longitude, upperRight.latitude, upperRight.longitude)); + List
addresses = addressBuilder.parseResult(result); + subscriber.onNext(addresses); + subscriber.onCompleted(); + } catch (JSONException e) { + subscriber.onError(e); + } + }); + } + + @Override + public Observable> getFromLocation(double latitude, double longitude) { + return Observable.create(subscriber -> { + if (apiKey == null) { + subscriber.onCompleted(); + return; + } + try { + String result = networkClient.requestFromLocationName(String.format(Locale.ENGLISH, + QUERY_LAT_LONG, latitude, longitude, apiKey)); + List
addresses = addressBuilder.parseResult(result); + subscriber.onNext(addresses); + subscriber.onCompleted(); + } catch (JSONException e) { + subscriber.onError(e); + } + }); + } +} diff --git a/leku/src/main/java/com/schibstedspain/leku/geocoder/GeocoderPresenter.java b/leku/src/main/java/com/schibstedspain/leku/geocoder/GeocoderPresenter.java index c9667e75..20ca5917 100644 --- a/leku/src/main/java/com/schibstedspain/leku/geocoder/GeocoderPresenter.java +++ b/leku/src/main/java/com/schibstedspain/leku/geocoder/GeocoderPresenter.java @@ -13,18 +13,21 @@ public class GeocoderPresenter { private static final int RETRY_COUNT = 3; private final GeocoderInteractorInterface interactor; + private final GeocoderInteractorInterface apiInteractor; private GeocoderViewInterface view; private final GeocoderViewInterface nullView = new GeocoderViewInterface.NullView(); private CompositeSubscription compositeSubscription; private final Scheduler scheduler; private ReactiveLocationProvider locationProvider; - public GeocoderPresenter(ReactiveLocationProvider reactiveLocationProvider, GeocoderInteractorInterface interactor) { - this(reactiveLocationProvider, interactor, AndroidSchedulers.mainThread()); + public GeocoderPresenter(ReactiveLocationProvider reactiveLocationProvider, GeocoderInteractorInterface interactor, + GeocoderInteractorInterface apiInteractor) { + this(reactiveLocationProvider, interactor, apiInteractor, AndroidSchedulers.mainThread()); } public GeocoderPresenter(ReactiveLocationProvider reactiveLocationProvider, GeocoderInteractorInterface interactor, - Scheduler scheduler) { + GeocoderInteractorInterface apiInteractor, Scheduler scheduler) { + this.apiInteractor = apiInteractor; this.view = nullView; this.scheduler = scheduler; this.locationProvider = reactiveLocationProvider; @@ -56,6 +59,7 @@ public void getFromLocationName(String query) { .subscribeOn(Schedulers.newThread()) .observeOn(scheduler) .retry(RETRY_COUNT) + .onErrorResumeNext(apiInteractor.getFromLocationName(query)) .subscribe(view::showLocations, throwable -> view.showLoadLocationError(), view::didLoadLocation); compositeSubscription.add(locationNameSubscription); @@ -67,6 +71,7 @@ public void getFromLocationName(String query, LatLng lowerLeft, LatLng upperRigh .subscribeOn(Schedulers.newThread()) .observeOn(scheduler) .retry(RETRY_COUNT) + .onErrorResumeNext(apiInteractor.getFromLocationName(query, lowerLeft, upperRight)) .subscribe(view::showLocations, throwable -> view.showLoadLocationError(), view::didLoadLocation); compositeSubscription.add(locationNameSubscription); @@ -79,6 +84,7 @@ public void getDebouncedFromLocationName(String query, int debounceTime) { .subscribeOn(Schedulers.newThread()) .observeOn(scheduler) .retry(RETRY_COUNT) + .onErrorResumeNext(apiInteractor.getFromLocationName(query)) .subscribe(view::showDebouncedLocations, throwable -> view.showLoadLocationError(), view::didLoadLocation); compositeSubscription.add(locationNameDebounceSubscription); @@ -91,6 +97,7 @@ public void getDebouncedFromLocationName(String query, LatLng lowerLeft, LatLng .subscribeOn(Schedulers.newThread()) .observeOn(scheduler) .retry(RETRY_COUNT) + .onErrorResumeNext(apiInteractor.getFromLocationName(query, lowerLeft, upperRight)) .subscribe(view::showDebouncedLocations, throwable -> view.showLoadLocationError(), view::didLoadLocation); compositeSubscription.add(locationNameDebounceSubscription); @@ -102,6 +109,7 @@ public void getInfoFromLocation(LatLng latLng) { .subscribeOn(Schedulers.newThread()) .observeOn(scheduler) .retry(RETRY_COUNT) + .onErrorResumeNext(apiInteractor.getFromLocation(latLng.latitude, latLng.longitude)) .subscribe(view::showLocationInfo, throwable -> view.showGetLocationInfoError(), view::didGetLocationInfo); compositeSubscription.add(locationSubscription); diff --git a/leku/src/main/java/com/schibstedspain/leku/geocoder/api/AddressBuilder.java b/leku/src/main/java/com/schibstedspain/leku/geocoder/api/AddressBuilder.java new file mode 100644 index 00000000..6f9c9a38 --- /dev/null +++ b/leku/src/main/java/com/schibstedspain/leku/geocoder/api/AddressBuilder.java @@ -0,0 +1,85 @@ +package com.schibstedspain.leku.geocoder.api; + +import android.location.Address; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class AddressBuilder { + + public List
parseResult(String json) throws JSONException { + List
addresses = new ArrayList<>(); + JSONObject root = new JSONObject(json); + JSONArray results = root.getJSONArray("results"); + for (int i = 0; i < results.length(); i++) { + addresses.add(parseAddress(results.getJSONObject(i))); + } + return addresses; + } + + private Address parseAddress(JSONObject jsonObject) throws JSONException { + JSONObject location = jsonObject.getJSONObject("geometry").getJSONObject("location"); + double latitude = location.getDouble("lat"); + double longitude = location.getDouble("lng"); + + List components = getAddressComponents(jsonObject.getJSONArray("address_components")); + + + String postalCode = ""; + String city = ""; + String number = ""; + String street = ""; + for (AddressComponent component : components) { + if (component.types.contains("postal_code")) { + postalCode = component.name; + } + if (component.types.contains("locality")) { + city = component.name; + } + if (component.types.contains("street_number")) { + number = component.name; + } + if (component.types.contains("route")) { + street = component.name; + } + } + StringBuilder fullAddress = new StringBuilder(); + fullAddress.append(street); + if (!street.isEmpty() && !number.isEmpty()) { + fullAddress.append(", ").append(number); + } + Address address = new Address(Locale.getDefault()); + address.setLatitude(latitude); + address.setLongitude(longitude); + address.setPostalCode(postalCode); + address.setAddressLine(0, fullAddress.toString()); + address.setAddressLine(1, postalCode); + address.setAddressLine(2, city); + address.setLocality(city); + return address; + } + + private List getAddressComponents(JSONArray jsonComponents) throws JSONException { + List components = new ArrayList<>(); + for (int i = 0; i < jsonComponents.length(); i++) { + AddressComponent component = new AddressComponent(); + JSONObject jsonComponent = jsonComponents.getJSONObject(i); + component.name = jsonComponent.getString("long_name"); + component.types = new ArrayList<>(); + JSONArray jsonTypes = jsonComponent.getJSONArray("types"); + for (int j = 0; j < jsonTypes.length(); j++) { + component.types.add(jsonTypes.getString(j)); + } + components.add(component); + } + return components; + } + + private static class AddressComponent { + String name; + List types; + } +} diff --git a/leku/src/main/java/com/schibstedspain/leku/geocoder/api/NetworkClient.java b/leku/src/main/java/com/schibstedspain/leku/geocoder/api/NetworkClient.java new file mode 100644 index 00000000..86d763a3 --- /dev/null +++ b/leku/src/main/java/com/schibstedspain/leku/geocoder/api/NetworkClient.java @@ -0,0 +1,63 @@ +package com.schibstedspain.leku.geocoder.api; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import javax.net.ssl.HttpsURLConnection; + +public class NetworkClient { + private static final int REPONSE_MAX_LENGTH = 1024; + private static final int READ_TIMEOUT = 3000; + private static final int CONNECT_TIMEOUT = 3000; + + public String requestFromLocationName(String request) { + String result = null; + InputStream stream = null; + HttpsURLConnection connection = null; + try { + URL url = new URL(request); + connection = (HttpsURLConnection) url.openConnection(); + connection.setReadTimeout(READ_TIMEOUT); + connection.setConnectTimeout(CONNECT_TIMEOUT); + connection.setRequestMethod("GET"); + connection.setDoInput(true); + connection.connect(); + int responseCode = connection.getResponseCode(); + if (responseCode != HttpsURLConnection.HTTP_OK) { + throw new NetworkException("HTTP error code: " + responseCode); + } + stream = connection.getInputStream(); + if (stream != null) { + result = readStream(stream, REPONSE_MAX_LENGTH); + } + } catch (IOException ioException) { + throw new NetworkException(ioException); + } finally { + + if (stream != null) { + try { + stream.close(); + } catch (IOException ioException) { + throw new NetworkException(ioException); + } + } + if (connection != null) { + connection.disconnect(); + } + } + return result; + } + + + private String readStream(InputStream stream, int maxLength) throws IOException { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + byte[] buffer = new byte[maxLength]; + int length; + while ((length = stream.read(buffer)) != -1) { + result.write(buffer, 0, length); + } + return result.toString("UTF-8"); + } + +} diff --git a/leku/src/main/java/com/schibstedspain/leku/geocoder/api/NetworkException.java b/leku/src/main/java/com/schibstedspain/leku/geocoder/api/NetworkException.java new file mode 100644 index 00000000..7b1d07f0 --- /dev/null +++ b/leku/src/main/java/com/schibstedspain/leku/geocoder/api/NetworkException.java @@ -0,0 +1,18 @@ +package com.schibstedspain.leku.geocoder.api; + +public class NetworkException extends RuntimeException { + public NetworkException() { + } + + public NetworkException(String message) { + super(message); + } + + public NetworkException(String message, Throwable cause) { + super(message, cause); + } + + public NetworkException(Throwable cause) { + super(cause); + } +}