From 27053bbcc1898bebcdb86f9d90939228c2edb520 Mon Sep 17 00:00:00 2001 From: danesfeder Date: Wed, 20 Mar 2019 14:05:17 -0400 Subject: [PATCH] Add dynamic offline routing to NavigationView --- .../EmbeddedNavigationActivity.java | 22 +- .../ui/v5/ConnectivityStatusProvider.java | 60 ++++ .../DamerauLevenshteinAlgorithm.java | 2 +- .../ui/v5/MobileNetworkChecker.java | 45 +++ .../navigation/ui/v5/NavigationViewModel.java | 53 ++-- .../ui/v5/NavigationViewOfflineRouter.java | 49 ++++ .../ui/v5/NavigationViewOptions.java | 28 ++ .../v5/NavigationViewRouteEngineListener.java | 4 +- .../ui/v5/NavigationViewRouter.java | 140 ++++++++++ .../ui/v5/OfflineRouteFoundCallback.java | 26 ++ .../v5/OfflineRouterConfiguredCallback.java | 27 ++ .../navigation/ui/v5/RouteComparator.java | 73 +++++ .../ui/v5/{route => }/ViewRouteListener.java | 6 +- .../navigation/ui/v5/WifiNetworkChecker.java | 33 +++ .../navigation/ui/v5/route/OffRouteEvent.java | 27 -- .../ui/v5/route/ViewRouteFetcher.java | 164 ----------- .../ui/v5/ConnectivityStatusProviderTest.java | 119 ++++++++ .../v5/NavigationViewOfflineRouterTest.java | 51 ++++ ...NavigationViewRouteEngineListenerTest.java | 8 +- .../ui/v5/NavigationViewRouterTest.java | 261 ++++++++++++++++++ .../ui/v5/OfflineRouteFoundCallbackTest.java | 38 +++ .../OfflineRouterConfiguredCallbackTest.java | 20 ++ .../ui/v5/ViewRouteFetcherTest.java | 133 --------- .../navigation/v5/route/RouteFetcher.java | 76 +++-- .../navigation/v5/route/RouteFetcherTest.java | 92 +++++- 25 files changed, 1154 insertions(+), 403 deletions(-) create mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/ConnectivityStatusProvider.java rename libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/{route => }/DamerauLevenshteinAlgorithm.java (98%) create mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MobileNetworkChecker.java create mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOfflineRouter.java create mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouter.java create mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouteFoundCallback.java create mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouterConfiguredCallback.java create mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/RouteComparator.java rename libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/{route => }/ViewRouteListener.java (57%) create mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/WifiNetworkChecker.java delete mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/OffRouteEvent.java delete mode 100644 libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/ViewRouteFetcher.java create mode 100644 libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/ConnectivityStatusProviderTest.java create mode 100644 libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOfflineRouterTest.java create mode 100644 libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouterTest.java create mode 100644 libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouteFoundCallbackTest.java create mode 100644 libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouterConfiguredCallbackTest.java delete mode 100644 libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/ViewRouteFetcherTest.java diff --git a/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/navigationui/EmbeddedNavigationActivity.java b/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/navigationui/EmbeddedNavigationActivity.java index a364f6e28f4..660e9ad3a54 100644 --- a/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/navigationui/EmbeddedNavigationActivity.java +++ b/app/src/main/java/com/mapbox/services/android/navigation/testapp/activity/navigationui/EmbeddedNavigationActivity.java @@ -4,6 +4,7 @@ import android.content.res.Configuration; import android.location.Location; import android.os.Bundle; +import android.os.Environment; import android.preference.PreferenceManager; import android.support.annotation.IdRes; import android.support.annotation.NonNull; @@ -39,8 +40,11 @@ import com.mapbox.services.android.navigation.v5.routeprogress.ProgressChangeListener; import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; +import java.io.File; + import retrofit2.Call; import retrofit2.Response; +import timber.log.Timber; public class EmbeddedNavigationActivity extends AppCompatActivity implements OnNavigationReadyCallback, NavigationListener, ProgressChangeListener, InstructionListListener, SpeechAnnouncementListener, @@ -194,13 +198,29 @@ private void startNavigation(DirectionsRoute directionsRoute) { .progressChangeListener(this) .instructionListListener(this) .speechAnnouncementListener(this) - .bannerInstructionsListener(this); + .bannerInstructionsListener(this) + .offlineRoutingTilesPath(obtainOfflineDirectory()) + .offlineRoutingTilesVersion(obtainOfflineTileVersion()); setBottomSheetCallback(options); setupNightModeFab(); navigationView.startNavigation(options.build()); } + private String obtainOfflineDirectory() { + File offline = Environment.getExternalStoragePublicDirectory("Offline"); + if (!offline.exists()) { + Timber.d("Offline directory does not exist"); + offline.mkdirs(); + } + return offline.getAbsolutePath(); + } + + private String obtainOfflineTileVersion() { + return PreferenceManager.getDefaultSharedPreferences(this) + .getString(getString(R.string.offline_version_key), ""); + } + private void fetchRoute() { NavigationRoute.builder(this) .accessToken(Mapbox.getAccessToken()) diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/ConnectivityStatusProvider.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/ConnectivityStatusProvider.java new file mode 100644 index 00000000000..2d494458dbb --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/ConnectivityStatusProvider.java @@ -0,0 +1,60 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; + +import java.util.HashMap; + +class ConnectivityStatusProvider { + + private final Context context; + private final WifiNetworkChecker wifiNetworkChecker; + private final MobileNetworkChecker mobileNetworkChecker; + + ConnectivityStatusProvider(Context applicationContext) { + this.context = applicationContext; + this.wifiNetworkChecker = new WifiNetworkChecker(new HashMap()); + this.mobileNetworkChecker = new MobileNetworkChecker(new HashMap()); + } + + boolean isConnected() { + NetworkInfo info = getNetworkInfo(context); + return (info != null && info.isConnected()); + } + + boolean isConnectedFast() { + NetworkInfo info = getNetworkInfo(context); + int wifiLevel = getWifiLevel(context); + return (info != null + && info.isConnected() + && isConnectionFast(info.getType(), info.getSubtype(), wifiLevel)); + } + + @SuppressLint("MissingPermission") + private NetworkInfo getNetworkInfo(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + return cm.getActiveNetworkInfo(); + } + + @SuppressLint({"MissingPermission", "WifiManagerPotentialLeak"}) + private int getWifiLevel(Context context) { + WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + int numberOfLevels = 5; + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + return WifiManager.calculateSignalLevel(wifiInfo.getRssi(), numberOfLevels); + } + + private boolean isConnectionFast(int type, int networkType, int wifiLevel) { + if (type == ConnectivityManager.TYPE_WIFI) { + return wifiNetworkChecker.isFast(wifiLevel); + } else if (type == ConnectivityManager.TYPE_MOBILE) { + return mobileNetworkChecker.isFast(networkType); + } else { + return false; + } + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/DamerauLevenshteinAlgorithm.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/DamerauLevenshteinAlgorithm.java similarity index 98% rename from libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/DamerauLevenshteinAlgorithm.java rename to libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/DamerauLevenshteinAlgorithm.java index 41f759741e1..92af480c1da 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/DamerauLevenshteinAlgorithm.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/DamerauLevenshteinAlgorithm.java @@ -1,4 +1,4 @@ -package com.mapbox.services.android.navigation.ui.v5.route; +package com.mapbox.services.android.navigation.ui.v5; import java.util.HashMap; import java.util.Map; diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MobileNetworkChecker.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MobileNetworkChecker.java new file mode 100644 index 00000000000..88818ef2bfe --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/MobileNetworkChecker.java @@ -0,0 +1,45 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.support.annotation.NonNull; +import android.telephony.TelephonyManager; + +import java.util.HashMap; + +class MobileNetworkChecker { + + private final HashMap statusMap; + + MobileNetworkChecker(HashMap statusMap) { + this.statusMap = statusMap; + initialize(statusMap); + } + + @NonNull + Boolean isFast(Integer networkType) { + Boolean isConnectionFast = statusMap.get(networkType); + if (isConnectionFast == null) { + isConnectionFast = false; + } + return isConnectionFast; + } + + private void initialize(HashMap statusMap) { + statusMap.put(TelephonyManager.NETWORK_TYPE_1xRTT, false); + statusMap.put(TelephonyManager.NETWORK_TYPE_GPRS, false); + statusMap.put(TelephonyManager.NETWORK_TYPE_CDMA, false); + statusMap.put(TelephonyManager.NETWORK_TYPE_EDGE, false); + statusMap.put(TelephonyManager.NETWORK_TYPE_IDEN, false); + statusMap.put(TelephonyManager.NETWORK_TYPE_UNKNOWN, false); + + statusMap.put(TelephonyManager.NETWORK_TYPE_EVDO_0, true); + statusMap.put(TelephonyManager.NETWORK_TYPE_EVDO_A, true); + statusMap.put(TelephonyManager.NETWORK_TYPE_HSDPA, true); + statusMap.put(TelephonyManager.NETWORK_TYPE_HSPA, true); + statusMap.put(TelephonyManager.NETWORK_TYPE_HSUPA, true); + statusMap.put(TelephonyManager.NETWORK_TYPE_UMTS, true); + statusMap.put(TelephonyManager.NETWORK_TYPE_EHRPD, true); + statusMap.put(TelephonyManager.NETWORK_TYPE_EVDO_B, true); + statusMap.put(TelephonyManager.NETWORK_TYPE_HSPAP, true); + statusMap.put(TelephonyManager.NETWORK_TYPE_LTE, true); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModel.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModel.java index 712aa325eaa..84488b8ee79 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModel.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewModel.java @@ -5,8 +5,6 @@ import android.arch.lifecycle.MutableLiveData; import android.content.Context; import android.location.Location; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; @@ -21,9 +19,6 @@ import com.mapbox.services.android.navigation.ui.v5.feedback.FeedbackItem; import com.mapbox.services.android.navigation.ui.v5.instruction.BannerInstructionModel; import com.mapbox.services.android.navigation.ui.v5.instruction.InstructionModel; -import com.mapbox.services.android.navigation.ui.v5.route.OffRouteEvent; -import com.mapbox.services.android.navigation.ui.v5.route.ViewRouteFetcher; -import com.mapbox.services.android.navigation.ui.v5.route.ViewRouteListener; import com.mapbox.services.android.navigation.ui.v5.summary.SummaryModel; import com.mapbox.services.android.navigation.ui.v5.voice.NavigationSpeechPlayer; import com.mapbox.services.android.navigation.ui.v5.voice.SpeechAnnouncement; @@ -42,6 +37,7 @@ import com.mapbox.services.android.navigation.v5.navigation.metrics.FeedbackEvent; import com.mapbox.services.android.navigation.v5.offroute.OffRouteListener; import com.mapbox.services.android.navigation.v5.route.FasterRouteListener; +import com.mapbox.services.android.navigation.v5.route.RouteFetcher; import com.mapbox.services.android.navigation.v5.routeprogress.ProgressChangeListener; import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; import com.mapbox.services.android.navigation.v5.utils.DistanceFormatter; @@ -69,14 +65,13 @@ public class NavigationViewModel extends AndroidViewModel { private final MutableLiveData destination = new MutableLiveData<>(); private MapboxNavigation navigation; - private ViewRouteFetcher routeFetcher; + private NavigationViewRouter router; private LocationEngineConductor locationEngineConductor; private NavigationViewEventDispatcher navigationViewEventDispatcher; private SpeechPlayer speechPlayer; private VoiceInstructionLoader voiceInstructionLoader; private VoiceInstructionCache voiceInstructionCache; private int voiceInstructionsToAnnounce = 0; - private ConnectivityManager connectivityManager; private RouteProgress routeProgress; private String feedbackId; private String screenshot; @@ -93,9 +88,8 @@ public class NavigationViewModel extends AndroidViewModel { public NavigationViewModel(Application application) { super(application); this.accessToken = Mapbox.getAccessToken(); - initializeConnectivityManager(application); - initializeNavigationRouteEngine(); - initializeNavigationLocationEngine(); + initializeLocationEngine(); + initializeRouter(); routeUtils = new RouteUtils(); localeUtils = new LocaleUtils(); } @@ -121,7 +115,7 @@ public NavigationViewModel(Application application) { public void onDestroy(boolean isChangingConfigurations) { this.isChangingConfigurations = isChangingConfigurations; if (!isChangingConfigurations) { - routeFetcher.onDestroy(); + router.onDestroy(); endNavigation(); deactivateInstructionPlayer(); isRunning = false; @@ -208,7 +202,7 @@ void initialize(NavigationViewOptions options) { initializeVoiceInstructionCache(); initializeNavigationSpeechPlayer(options); } - routeFetcher.extractRouteOptions(options); + router.extractRouteOptions(options); } void updateFeedbackScreenshot(String screenshot) { @@ -264,15 +258,14 @@ MutableLiveData retrieveDestination() { return destination; } - private void initializeConnectivityManager(Application application) { - connectivityManager = (ConnectivityManager) application.getSystemService(Context.CONNECTIVITY_SERVICE); + private void initializeRouter() { + RouteFetcher onlineRouter = new RouteFetcher(getApplication(), accessToken); + Context applicationContext = getApplication().getApplicationContext(); + ConnectivityStatusProvider connectivityStatus = new ConnectivityStatusProvider(applicationContext); + router = new NavigationViewRouter(onlineRouter, connectivityStatus, routeEngineListener); } - private void initializeNavigationRouteEngine() { - routeFetcher = new ViewRouteFetcher(getApplication(), accessToken, routeEngineListener); - } - - private void initializeNavigationLocationEngine() { + private void initializeLocationEngine() { locationEngineConductor = new LocationEngineConductor(); } @@ -365,7 +358,7 @@ private void addMilestones(NavigationViewOptions options) { @Override public void onProgressChange(Location location, RouteProgress routeProgress) { NavigationViewModel.this.routeProgress = routeProgress; - routeFetcher.updateLocation(location); + router.updateLocation(location); instructionModel.setValue(new InstructionModel(distanceFormatter, routeProgress)); summaryModel.setValue(new SummaryModel(getApplication(), distanceFormatter, routeProgress, timeFormatType)); navigationLocation.setValue(location); @@ -376,11 +369,9 @@ public void onProgressChange(Location location, RouteProgress routeProgress) { private OffRouteListener offRouteListener = new OffRouteListener() { @Override public void userOffRoute(Location location) { - if (hasNetworkConnection()) { - speechPlayer.onOffRoute(); - Point newOrigin = Point.fromLngLat(location.getLongitude(), location.getLatitude()); - handleOffRouteEvent(newOrigin); - } + speechPlayer.onOffRoute(); + Point newOrigin = Point.fromLngLat(location.getLongitude(), location.getLatitude()); + handleOffRouteEvent(newOrigin); } }; @@ -469,15 +460,6 @@ private void updateBannerInstruction(RouteProgress routeProgress, Milestone mile } } - @SuppressWarnings( {"MissingPermission"}) - private boolean hasNetworkConnection() { - if (connectivityManager == null) { - return false; - } - NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo(); - return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); - } - private void sendEventFeedback(FeedbackItem feedbackItem) { if (navigationViewEventDispatcher != null) { navigationViewEventDispatcher.onFeedbackSent(feedbackItem); @@ -493,8 +475,7 @@ private void sendEventArrival(RouteProgress routeProgress) { private void handleOffRouteEvent(Point newOrigin) { if (navigationViewEventDispatcher != null && navigationViewEventDispatcher.allowRerouteFrom(newOrigin)) { navigationViewEventDispatcher.onOffRoute(newOrigin); - OffRouteEvent event = new OffRouteEvent(newOrigin, routeProgress); - routeFetcher.fetchRouteFromOffRouteEvent(event); + router.findRouteFrom(routeProgress); isOffRoute.setValue(true); } } diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOfflineRouter.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOfflineRouter.java new file mode 100644 index 00000000000..f314e917877 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOfflineRouter.java @@ -0,0 +1,49 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.services.android.navigation.v5.navigation.MapboxOfflineRouter; +import com.mapbox.services.android.navigation.v5.navigation.NavigationRoute; +import com.mapbox.services.android.navigation.v5.navigation.OfflineRoute; + +import timber.log.Timber; + +class NavigationViewOfflineRouter { + + private final MapboxOfflineRouter offlineRouter; + private final NavigationViewRouter router; + private boolean isConfigured; + private String tileVersion; + + NavigationViewOfflineRouter(MapboxOfflineRouter offlineRouter, NavigationViewRouter router) { + this.offlineRouter = offlineRouter; + this.router = router; + } + + void configure(String tileVersion) { + if (!isConfigured || isNew(tileVersion)) { + offlineRouter.configure(tileVersion, new OfflineRouterConfiguredCallback(this)); + } + this.tileVersion = tileVersion; + } + + void setIsConfigured(boolean isConfigured) { + this.isConfigured = isConfigured; + } + + boolean isConfigured() { + return isConfigured; + } + + void findRouteWith(NavigationRoute.Builder builder) { + if (!isConfigured) { + Timber.e("Cannot find route - offline router is not configured"); + return; + } + + OfflineRoute offlineRoute = OfflineRoute.builder(builder).build(); + offlineRouter.findRoute(offlineRoute, new OfflineRouteFoundCallback(router)); + } + + private boolean isNew(String tileVersion) { + return !this.tileVersion.equals(tileVersion); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOptions.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOptions.java index 46a95cc2700..2c6acd92e0e 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOptions.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOptions.java @@ -61,6 +61,12 @@ public abstract class NavigationViewOptions extends NavigationUiOptions { @Nullable public abstract LocationEngine locationEngine(); + @Nullable + public abstract String offlineRoutingTilesPath(); + + @Nullable + public abstract String offlineRoutingTilesVersion(); + @AutoValue.Builder public abstract static class Builder { @@ -100,6 +106,28 @@ public abstract static class Builder { public abstract Builder locationEngine(LocationEngine locationEngine); + /** + * Add an offline path for loading offline routing data. + *

+ * When added, the {@link NavigationView} will try to initialize and use this data + * for offline routing when no or poor internet connection is found. + * + * @param offlinePath to offline data on device + * @return this builder + */ + public abstract Builder offlineRoutingTilesPath(String offlinePath); + + /** + * Add an offline tile version. When providing a routing tile path, this version + * is also required for configuration. + *

+ * This version should directly correspond to the data in the offline path also provided. + * + * @param offlineVersion of data in tile path + * @return this builder + */ + public abstract Builder offlineRoutingTilesVersion(String offlineVersion); + public abstract NavigationViewOptions build(); } diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouteEngineListener.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouteEngineListener.java index 16426026d59..312ba5b5da3 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouteEngineListener.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouteEngineListener.java @@ -2,7 +2,6 @@ import com.mapbox.api.directions.v5.models.DirectionsRoute; import com.mapbox.geojson.Point; -import com.mapbox.services.android.navigation.ui.v5.route.ViewRouteListener; class NavigationViewRouteEngineListener implements ViewRouteListener { @@ -19,9 +18,8 @@ public void onRouteUpdate(DirectionsRoute directionsRoute) { } @Override - public void onRouteRequestError(Throwable throwable) { + public void onRouteRequestError(String errorMessage) { if (navigationViewModel.isOffRoute()) { - String errorMessage = throwable.getMessage(); navigationViewModel.sendEventFailedReroute(errorMessage); } } diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouter.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouter.java new file mode 100644 index 00000000000..f29bc3e431c --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouter.java @@ -0,0 +1,140 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.location.Location; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.api.directions.v5.models.DirectionsResponse; +import com.mapbox.api.directions.v5.models.DirectionsRoute; +import com.mapbox.api.directions.v5.models.RouteOptions; +import com.mapbox.core.utils.TextUtils; +import com.mapbox.geojson.Point; +import com.mapbox.services.android.navigation.v5.navigation.MapboxOfflineRouter; +import com.mapbox.services.android.navigation.v5.navigation.NavigationRoute; +import com.mapbox.services.android.navigation.v5.route.RouteFetcher; +import com.mapbox.services.android.navigation.v5.route.RouteListener; +import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; + +import java.util.List; + +class NavigationViewRouter implements RouteListener { + + private final RouteFetcher onlineRouter; + private final ConnectivityStatusProvider connectivityStatus; + private final RouteComparator routeComparator; + private final ViewRouteListener listener; + @Nullable + private NavigationViewOfflineRouter offlineRouter; + + private RouteOptions routeOptions; + private DirectionsRoute currentRoute; + private Location location; + private boolean isRouting; + + NavigationViewRouter(RouteFetcher onlineRouter, ConnectivityStatusProvider connectivityStatus, + ViewRouteListener listener) { + this.onlineRouter = onlineRouter; + this.connectivityStatus = connectivityStatus; + this.listener = listener; + this.routeComparator = new RouteComparator(this); + onlineRouter.addRouteListener(this); + } + + // Extra fields for testing purposes + NavigationViewRouter(RouteFetcher onlineRouter, NavigationViewOfflineRouter offlineRouter, + ConnectivityStatusProvider connectivityStatus, RouteComparator routeComparator, + ViewRouteListener listener) { + this.onlineRouter = onlineRouter; + this.offlineRouter = offlineRouter; + this.connectivityStatus = connectivityStatus; + this.listener = listener; + this.routeComparator = routeComparator; + onlineRouter.addRouteListener(this); + } + + @Override + public void onResponseReceived(DirectionsResponse response, @Nullable RouteProgress routeProgress) { + routeComparator.compare(response, currentRoute); + isRouting = false; + } + + @Override + public void onErrorReceived(Throwable throwable) { + onRequestError(throwable.getMessage()); + isRouting = false; + } + + void extractRouteOptions(NavigationViewOptions options) { + extractRouteFrom(options); + initializeOfflineFrom(options); + } + + void findRouteFrom(@Nullable RouteProgress routeProgress) { + if (isRouting) { + return; + } + NavigationRoute.Builder builder = onlineRouter.buildRequestFrom(location, routeProgress); + if (connectivityStatus.isConnectedFast()) { + onlineRouter.findRouteWith(builder); + isRouting = true; + } else if (isOfflineConfigured()) { + offlineRouter.findRouteWith(builder); + isRouting = true; + } else if (connectivityStatus.isConnected()) { + onlineRouter.findRouteWith(builder); + isRouting = true; + } + } + + void updateLocation(@NonNull Location location) { + this.location = location; + } + + void updateCurrentRoute(DirectionsRoute currentRoute) { + this.currentRoute = currentRoute; + listener.onRouteUpdate(currentRoute); + } + + void onRequestError(String errorMessage) { + listener.onRouteRequestError(errorMessage); + } + + void onDestroy() { + onlineRouter.cancelRouteCall(); + onlineRouter.clearListeners(); + } + + private void extractRouteFrom(NavigationViewOptions options) { + DirectionsRoute route = options.directionsRoute(); + cacheRouteOptions(route.routeOptions()); + updateCurrentRoute(route); + } + + private void cacheRouteOptions(RouteOptions routeOptions) { + this.routeOptions = routeOptions; + cacheRouteDestination(); + } + + private void initializeOfflineFrom(NavigationViewOptions options) { + String offlinePath = options.offlineRoutingTilesPath(); + if (!TextUtils.isEmpty(offlinePath)) { + MapboxOfflineRouter offlineRouter = new MapboxOfflineRouter(offlinePath); + this.offlineRouter = new NavigationViewOfflineRouter(offlineRouter, this); + this.offlineRouter.configure(options.offlineRoutingTilesVersion()); + } + } + + private boolean isOfflineConfigured() { + return offlineRouter != null && offlineRouter.isConfigured(); + } + + private void cacheRouteDestination() { + boolean hasValidCoordinates = routeOptions != null && !routeOptions.coordinates().isEmpty(); + if (hasValidCoordinates) { + List coordinates = routeOptions.coordinates(); + int destinationCoordinate = coordinates.size() - 1; + Point destinationPoint = coordinates.get(destinationCoordinate); + listener.onDestinationSet(destinationPoint); + } + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouteFoundCallback.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouteFoundCallback.java new file mode 100644 index 00000000000..29e85b8a127 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouteFoundCallback.java @@ -0,0 +1,26 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.support.annotation.NonNull; + +import com.mapbox.api.directions.v5.models.DirectionsRoute; +import com.mapbox.services.android.navigation.v5.navigation.OfflineError; +import com.mapbox.services.android.navigation.v5.navigation.OnOfflineRouteFoundCallback; + +class OfflineRouteFoundCallback implements OnOfflineRouteFoundCallback { + + private final NavigationViewRouter router; + + OfflineRouteFoundCallback(NavigationViewRouter router) { + this.router = router; + } + + @Override + public void onRouteFound(@NonNull DirectionsRoute offlineRoute) { + router.updateCurrentRoute(offlineRoute); + } + + @Override + public void onError(@NonNull OfflineError error) { + router.onRequestError(error.getMessage()); + } +} diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouterConfiguredCallback.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouterConfiguredCallback.java new file mode 100644 index 00000000000..ee50cb3d5d1 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouterConfiguredCallback.java @@ -0,0 +1,27 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.support.annotation.NonNull; + +import com.mapbox.services.android.navigation.v5.navigation.OfflineError; +import com.mapbox.services.android.navigation.v5.navigation.OnOfflineTilesConfiguredCallback; + +import timber.log.Timber; + +class OfflineRouterConfiguredCallback implements OnOfflineTilesConfiguredCallback { + + private final NavigationViewOfflineRouter offlineRouter; + + OfflineRouterConfiguredCallback(NavigationViewOfflineRouter offlineRouter) { + this.offlineRouter = offlineRouter; + } + + @Override + public void onConfigured(int numberOfTiles) { + offlineRouter.setIsConfigured(true); + } + + @Override + public void onConfigurationError(@NonNull OfflineError error) { + Timber.e(error.getMessage()); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/RouteComparator.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/RouteComparator.java new file mode 100644 index 00000000000..e27d96380df --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/RouteComparator.java @@ -0,0 +1,73 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.mapbox.api.directions.v5.models.DirectionsResponse; +import com.mapbox.api.directions.v5.models.DirectionsRoute; +import com.mapbox.api.directions.v5.models.RouteLeg; + +import java.util.List; + +class RouteComparator { + + private static final int FIRST_ROUTE = 0; + private static final int ONE_ROUTE = 1; + private final NavigationViewRouter navigationViewRouter; + + RouteComparator(NavigationViewRouter navigationViewRouter) { + this.navigationViewRouter = navigationViewRouter; + } + + void compare(@NonNull DirectionsResponse response, @Nullable DirectionsRoute chosenRoute) { + if (isValidRoute(response)) { + List routes = response.routes(); + DirectionsRoute bestRoute = routes.get(FIRST_ROUTE); + if (isNavigationRunning(chosenRoute)) { + bestRoute = findMostSimilarRoute(routes, bestRoute, chosenRoute); + } + navigationViewRouter.updateCurrentRoute(bestRoute); + } + } + + private DirectionsRoute findMostSimilarRoute(List routes, DirectionsRoute currentBestRoute, + DirectionsRoute chosenRoute) { + DirectionsRoute mostSimilarRoute = currentBestRoute; + if (routes.size() > ONE_ROUTE) { + mostSimilarRoute = compareRoutes(chosenRoute, routes); + } + return mostSimilarRoute; + } + + private DirectionsRoute compareRoutes(DirectionsRoute chosenRoute, List routes) { + int routeIndex = 0; + String chosenRouteLegDescription = obtainRouteLegDescriptionFrom(chosenRoute); + int minSimilarity = Integer.MAX_VALUE; + for (int index = 0; index < routes.size(); index++) { + String routeLegDescription = obtainRouteLegDescriptionFrom(routes.get(index)); + int currentSimilarity = DamerauLevenshteinAlgorithm.execute(chosenRouteLegDescription, routeLegDescription); + if (currentSimilarity < minSimilarity) { + minSimilarity = currentSimilarity; + routeIndex = index; + } + } + return routes.get(routeIndex); + } + + private String obtainRouteLegDescriptionFrom(DirectionsRoute route) { + List routeLegs = route.legs(); + StringBuilder routeLegDescription = new StringBuilder(); + for (RouteLeg leg : routeLegs) { + routeLegDescription.append(leg.summary()); + } + return routeLegDescription.toString(); + } + + private boolean isValidRoute(DirectionsResponse response) { + return response != null && !response.routes().isEmpty(); + } + + private boolean isNavigationRunning(DirectionsRoute chosenRoute) { + return chosenRoute != null; + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/ViewRouteListener.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/ViewRouteListener.java similarity index 57% rename from libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/ViewRouteListener.java rename to libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/ViewRouteListener.java index 96c0a2de2fe..51fe603b424 100644 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/ViewRouteListener.java +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/ViewRouteListener.java @@ -1,13 +1,13 @@ -package com.mapbox.services.android.navigation.ui.v5.route; +package com.mapbox.services.android.navigation.ui.v5; import com.mapbox.api.directions.v5.models.DirectionsRoute; import com.mapbox.geojson.Point; -public interface ViewRouteListener { +interface ViewRouteListener { void onRouteUpdate(DirectionsRoute directionsRoute); - void onRouteRequestError(Throwable throwable); + void onRouteRequestError(String errorMessage); void onDestinationSet(Point destination); } diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/WifiNetworkChecker.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/WifiNetworkChecker.java new file mode 100644 index 00000000000..0ec09b3f908 --- /dev/null +++ b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/WifiNetworkChecker.java @@ -0,0 +1,33 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.support.annotation.NonNull; + +import java.util.HashMap; + +class WifiNetworkChecker { + + private final HashMap statusMap; + + WifiNetworkChecker(HashMap statusMap) { + this.statusMap = statusMap; + initialize(statusMap); + } + + @NonNull + Boolean isFast(Integer wifiLevel) { + Boolean isConnectionFast = statusMap.get(wifiLevel); + if (isConnectionFast == null) { + isConnectionFast = false; + } + return isConnectionFast; + } + + private void initialize(HashMap statusMap) { + statusMap.put(5, true); + statusMap.put(4, true); + statusMap.put(3, true); + statusMap.put(2, false); + statusMap.put(1, false); + statusMap.put(0, false); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/OffRouteEvent.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/OffRouteEvent.java deleted file mode 100644 index 2539a2e4719..00000000000 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/OffRouteEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.mapbox.services.android.navigation.ui.v5.route; - -import com.mapbox.geojson.Point; -import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; - -public class OffRouteEvent { - - private Point newOrigin; - private RouteProgress routeProgress; - - public OffRouteEvent(Point newOrigin, RouteProgress routeProgress) { - this.newOrigin = newOrigin; - this.routeProgress = routeProgress; - } - - public Point getNewOrigin() { - return newOrigin; - } - - public RouteProgress getRouteProgress() { - return routeProgress; - } - - public static boolean isValid(OffRouteEvent offRouteEvent) { - return offRouteEvent.getNewOrigin() != null && offRouteEvent.getRouteProgress() != null; - } -} diff --git a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/ViewRouteFetcher.java b/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/ViewRouteFetcher.java deleted file mode 100644 index 24fd8cd1b80..00000000000 --- a/libandroid-navigation-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/route/ViewRouteFetcher.java +++ /dev/null @@ -1,164 +0,0 @@ -package com.mapbox.services.android.navigation.ui.v5.route; - -import android.content.Context; -import android.location.Location; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import com.mapbox.api.directions.v5.models.DirectionsResponse; -import com.mapbox.api.directions.v5.models.DirectionsRoute; -import com.mapbox.api.directions.v5.models.RouteLeg; -import com.mapbox.api.directions.v5.models.RouteOptions; -import com.mapbox.geojson.Point; -import com.mapbox.services.android.navigation.ui.v5.NavigationViewOptions; -import com.mapbox.services.android.navigation.v5.route.RouteFetcher; -import com.mapbox.services.android.navigation.v5.route.RouteListener; -import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; - -import java.util.List; - -public class ViewRouteFetcher extends RouteFetcher implements RouteListener { - - private static final int FIRST_ROUTE = 0; - private static final int ONE_ROUTE = 1; - - private final ViewRouteListener listener; - private RouteOptions routeOptions; - private DirectionsRoute currentRoute; - private Location location; - - public ViewRouteFetcher(Context context, String accessToken, ViewRouteListener listener) { - super(context, accessToken); - this.listener = listener; - addRouteListener(this); - } - - @Override - public void onResponseReceived(DirectionsResponse response, @Nullable RouteProgress routeProgress) { - processRoute(response); - } - - @Override - public void onErrorReceived(Throwable throwable) { - listener.onRouteRequestError(throwable); - } - - /** - * Checks the options used to launch this {@link com.mapbox.services.android.navigation.ui.v5.NavigationView}. - *

- * Will launch with a {@link DirectionsRoute}. - * - * @param options holds a {@link DirectionsRoute} - */ - public void extractRouteOptions(NavigationViewOptions options) { - extractRouteFromOptions(options); - } - - /** - * Fetches the route from the off-route event - * - * @param event from which the route progress is extracted - */ - public void fetchRouteFromOffRouteEvent(OffRouteEvent event) { - if (OffRouteEvent.isValid(event)) { - RouteProgress routeProgress = event.getRouteProgress(); - findRouteFromRouteProgress(location, routeProgress); - } - } - - /** - * Updates this object's awareness of the current location. - * - * @param location to set - */ - public void updateLocation(@NonNull Location location) { - this.location = location; - } - - /** - * Call when your {@link android.app.Activity} or {@link android.app.Fragment} is being - * destroyed to cancel any outstanding Directions API calls. - */ - public void onDestroy() { - cancelRouteCall(); - } - - private void extractRouteFromOptions(NavigationViewOptions options) { - DirectionsRoute route = options.directionsRoute(); - cacheRouteOptions(route.routeOptions()); - updateCurrentRoute(route); - } - - private void cacheRouteOptions(RouteOptions routeOptions) { - this.routeOptions = routeOptions; - cacheRouteDestination(); - } - - private void cacheRouteDestination() { - boolean hasValidCoordinates = routeOptions != null && !routeOptions.coordinates().isEmpty(); - if (hasValidCoordinates) { - List coordinates = routeOptions.coordinates(); - int destinationCoordinate = coordinates.size() - 1; - Point destinationPoint = coordinates.get(destinationCoordinate); - listener.onDestinationSet(destinationPoint); - } - } - - private void processRoute(@NonNull DirectionsResponse response) { - if (isValidRoute(response)) { - List routes = response.routes(); - DirectionsRoute bestRoute = routes.get(FIRST_ROUTE); - DirectionsRoute chosenRoute = currentRoute; - if (isNavigationRunning(chosenRoute)) { - bestRoute = obtainMostSimilarRoute(routes, bestRoute, chosenRoute); - } - updateCurrentRoute(bestRoute); - } - } - - private void updateCurrentRoute(DirectionsRoute currentRoute) { - this.currentRoute = currentRoute; - listener.onRouteUpdate(currentRoute); - } - - private boolean isValidRoute(DirectionsResponse response) { - return response != null && !response.routes().isEmpty(); - } - - private boolean isNavigationRunning(DirectionsRoute chosenRoute) { - return chosenRoute != null; - } - - private DirectionsRoute obtainMostSimilarRoute(List routes, DirectionsRoute currentBestRoute, - DirectionsRoute chosenRoute) { - DirectionsRoute mostSimilarRoute = currentBestRoute; - if (routes.size() > ONE_ROUTE) { - mostSimilarRoute = findMostSimilarRoute(chosenRoute, routes); - } - return mostSimilarRoute; - } - - private DirectionsRoute findMostSimilarRoute(DirectionsRoute chosenRoute, List routes) { - int routeIndex = 0; - String chosenRouteLegDescription = obtainRouteLegDescriptionFrom(chosenRoute); - int minSimilarity = Integer.MAX_VALUE; - for (int index = 0; index < routes.size(); index++) { - String routeLegDescription = obtainRouteLegDescriptionFrom(routes.get(index)); - int currentSimilarity = DamerauLevenshteinAlgorithm.execute(chosenRouteLegDescription, routeLegDescription); - if (currentSimilarity < minSimilarity) { - minSimilarity = currentSimilarity; - routeIndex = index; - } - } - return routes.get(routeIndex); - } - - private String obtainRouteLegDescriptionFrom(DirectionsRoute route) { - List routeLegs = route.legs(); - StringBuilder routeLegDescription = new StringBuilder(); - for (RouteLeg leg : routeLegs) { - routeLegDescription.append(leg.summary()); - } - return routeLegDescription.toString(); - } -} diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/ConnectivityStatusProviderTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/ConnectivityStatusProviderTest.java new file mode 100644 index 00000000000..028b3974645 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/ConnectivityStatusProviderTest.java @@ -0,0 +1,119 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.telephony.TelephonyManager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +public class ConnectivityStatusProviderTest { + + @Test + public void isConnected_returnsTrueWithConnectedInfo() { + Context context = buildMockContextForMobile(true); + ConnectivityStatusProvider statusProvider = new ConnectivityStatusProvider(context); + + boolean isConnected = statusProvider.isConnected(); + + assertTrue(isConnected); + } + + @Test + public void isConnected_returnsFalseWithDisconnectedInfo() { + Context context = buildMockContextForMobile(false); + ConnectivityStatusProvider statusProvider = new ConnectivityStatusProvider(context); + + boolean isConnected = statusProvider.isConnected(); + + assertFalse(isConnected); + } + + @Test + public void isConnectedFast_returnsTrueWithWifi() { + Context context = buildMockContextForWifi(ConnectivityManager.TYPE_WIFI, -20); + ConnectivityStatusProvider statusProvider = new ConnectivityStatusProvider(context); + + boolean isFast = statusProvider.isConnectedFast(); + + assertTrue(isFast); + } + + @Test + public void isConnectedFast_returnsTrueWithLTE() { + Context context = buildMockContextForMobile(ConnectivityManager.TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_LTE); + ConnectivityStatusProvider statusProvider = new ConnectivityStatusProvider(context); + + boolean isFast = statusProvider.isConnectedFast(); + + assertTrue(isFast); + } + + @Test + public void isConnectedFast_returnsTrueWithEDGE() { + Context context = buildMockContextForMobile(ConnectivityManager.TYPE_MOBILE, TelephonyManager.NETWORK_TYPE_EDGE); + ConnectivityStatusProvider statusProvider = new ConnectivityStatusProvider(context); + + boolean isFast = statusProvider.isConnectedFast(); + + assertFalse(isFast); + } + + private Context buildMockContextForMobile(boolean isConnected) { + Context context = mock(Context.class); + ConnectivityManager connectivityManager = mock(ConnectivityManager.class); + NetworkInfo networkInfo = mock(NetworkInfo.class); + when(networkInfo.isConnected()).thenReturn(isConnected); + when(connectivityManager.getActiveNetworkInfo()).thenReturn(networkInfo); + when(context.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(connectivityManager); + WifiManager wifiManager = mock(WifiManager.class); + WifiInfo wifiInfo = mock(WifiInfo.class); + when(wifiInfo.getRssi()).thenReturn(-20); + when(wifiManager.getConnectionInfo()).thenReturn(wifiInfo); + when(context.getSystemService(Context.WIFI_SERVICE)).thenReturn(wifiManager); + return context; + } + + private Context buildMockContextForWifi(int type, int rssiLevel) { + Context context = mock(Context.class); + ConnectivityManager connectivityManager = mock(ConnectivityManager.class); + NetworkInfo networkInfo = mock(NetworkInfo.class); + when(networkInfo.isConnected()).thenReturn(true); + when(networkInfo.getType()).thenReturn(type); + when(connectivityManager.getActiveNetworkInfo()).thenReturn(networkInfo); + when(context.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(connectivityManager); + WifiManager wifiManager = mock(WifiManager.class); + WifiInfo wifiInfo = mock(WifiInfo.class); + when(wifiInfo.getRssi()).thenReturn(rssiLevel); + when(wifiManager.getConnectionInfo()).thenReturn(wifiInfo); + when(context.getSystemService(Context.WIFI_SERVICE)).thenReturn(wifiManager); + return context; + } + + private Context buildMockContextForMobile(int type, int subType) { + Context context = mock(Context.class); + ConnectivityManager connectivityManager = mock(ConnectivityManager.class); + NetworkInfo networkInfo = mock(NetworkInfo.class); + when(networkInfo.isConnected()).thenReturn(true); + when(networkInfo.getType()).thenReturn(type); + when(networkInfo.getSubtype()).thenReturn(subType); + when(connectivityManager.getActiveNetworkInfo()).thenReturn(networkInfo); + when(context.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(connectivityManager); + WifiManager wifiManager = mock(WifiManager.class); + WifiInfo wifiInfo = mock(WifiInfo.class); + when(wifiInfo.getRssi()).thenReturn(-20); + when(wifiManager.getConnectionInfo()).thenReturn(wifiInfo); + when(context.getSystemService(Context.WIFI_SERVICE)).thenReturn(wifiManager); + return context; + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOfflineRouterTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOfflineRouterTest.java new file mode 100644 index 00000000000..3ef9310ac99 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewOfflineRouterTest.java @@ -0,0 +1,51 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.services.android.navigation.v5.navigation.MapboxOfflineRouter; +import com.mapbox.services.android.navigation.v5.navigation.NavigationRoute; +import com.mapbox.services.android.navigation.v5.navigation.OfflineRoute; + +import org.junit.Test; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class NavigationViewOfflineRouterTest { + + @Test + public void configure_offlineRouterIsConfigured() { + MapboxOfflineRouter offlineRouter = mock(MapboxOfflineRouter.class); + NavigationViewRouter viewRouter = mock(NavigationViewRouter.class); + NavigationViewOfflineRouter viewOfflineRouter = new NavigationViewOfflineRouter(offlineRouter, viewRouter); + + viewOfflineRouter.configure("some_tile_version"); + + verify(offlineRouter).configure(eq("some_tile_version"), any(OfflineRouterConfiguredCallback.class)); + } + + @Test + public void findRouteWith_notConfiguredIsIgnored() { + MapboxOfflineRouter offlineRouter = mock(MapboxOfflineRouter.class); + NavigationViewRouter viewRouter = mock(NavigationViewRouter.class); + NavigationViewOfflineRouter viewOfflineRouter = new NavigationViewOfflineRouter(offlineRouter, viewRouter); + + viewOfflineRouter.findRouteWith(mock(NavigationRoute.Builder.class)); + + verifyZeroInteractions(offlineRouter); + } + + @Test + public void findRouteWith_offlineRouteIsCalledWhenConfigured() { + MapboxOfflineRouter offlineRouter = mock(MapboxOfflineRouter.class); + NavigationViewRouter viewRouter = mock(NavigationViewRouter.class); + NavigationViewOfflineRouter viewOfflineRouter = new NavigationViewOfflineRouter(offlineRouter, viewRouter); + viewOfflineRouter.setIsConfigured(true); + + viewOfflineRouter.findRouteWith(mock(NavigationRoute.Builder.class)); + + verify(offlineRouter).findRoute(any(OfflineRoute.class), any(OfflineRouteFoundCallback.class)); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouteEngineListenerTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouteEngineListenerTest.java index 34cfb7e7a9e..af08efce19b 100644 --- a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouteEngineListenerTest.java +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouteEngineListenerTest.java @@ -31,13 +31,11 @@ public void onRouteUpdate_checksUpdateRouteCalled() { public void checksSendEventFailedRerouteCalledIfNavigationIsOffRoute() { NavigationViewModel mockedNavigationViewModel = mock(NavigationViewModel.class); when(mockedNavigationViewModel.isOffRoute()).thenReturn(true); - Throwable aThrowable = mock(Throwable.class); String anError = "An error occurred!"; - when(aThrowable.getMessage()).thenReturn(anError); NavigationViewRouteEngineListener theRouteEngineListener = new NavigationViewRouteEngineListener(mockedNavigationViewModel); - theRouteEngineListener.onRouteRequestError(aThrowable); + theRouteEngineListener.onRouteRequestError(anError); verify(mockedNavigationViewModel).sendEventFailedReroute(eq(anError)); } @@ -46,13 +44,11 @@ public void checksSendEventFailedRerouteCalledIfNavigationIsOffRoute() { public void checksSendEventFailedRerouteNotCalledIfNavigationIsNotOffRoute() { NavigationViewModel mockedNavigationViewModel = mock(NavigationViewModel.class); when(mockedNavigationViewModel.isOffRoute()).thenReturn(false); - Throwable aThrowable = mock(Throwable.class); String anError = "An error occurred!"; - when(aThrowable.getMessage()).thenReturn(anError); NavigationViewRouteEngineListener theRouteEngineListener = new NavigationViewRouteEngineListener(mockedNavigationViewModel); - theRouteEngineListener.onRouteRequestError(aThrowable); + theRouteEngineListener.onRouteRequestError(anError); verify(mockedNavigationViewModel, times(0)).sendEventFailedReroute(eq(anError)); } diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouterTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouterTest.java new file mode 100644 index 00000000000..2b9364e7eef --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/NavigationViewRouterTest.java @@ -0,0 +1,261 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import android.location.Location; +import android.support.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.mapbox.api.directions.v5.DirectionsAdapterFactory; +import com.mapbox.api.directions.v5.models.DirectionsResponse; +import com.mapbox.api.directions.v5.models.DirectionsRoute; +import com.mapbox.api.directions.v5.models.DirectionsWaypoint; +import com.mapbox.api.directions.v5.models.RouteOptions; +import com.mapbox.core.constants.Constants; +import com.mapbox.geojson.Point; +import com.mapbox.services.android.navigation.v5.navigation.NavigationRoute; +import com.mapbox.services.android.navigation.v5.route.RouteFetcher; +import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; + +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class NavigationViewRouterTest extends BaseTest { + + private static final String DIRECTIONS_PRECISION_6 = "directions_v5_precision_6.json"; + + @Test + public void sanity() { + ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); + NavigationViewRouter routeEngine = buildRouteEngine(routeEngineListener); + + assertNotNull(routeEngine); + } + + @Test + public void onExtractOptionsWithRoute_routeUpdateCallbackIsCalled() throws Exception { + ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); + NavigationViewRouter routeEngine = buildRouteEngine(routeEngineListener); + NavigationViewOptions options = buildNavigationViewOptionsWithRoute(); + DirectionsRoute directionsRoute = options.directionsRoute(); + + routeEngine.extractRouteOptions(options); + + verify(routeEngineListener).onRouteUpdate(directionsRoute); + } + + @Test + public void onExtractOptionsWithRoute_destinationCallbackIsCalled() throws Exception { + ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); + NavigationViewRouter routeEngine = buildRouteEngine(routeEngineListener); + NavigationViewOptions options = buildNavigationViewOptionsWithRoute(); + Point destination = findDestinationPoint(options); + + routeEngine.extractRouteOptions(options); + + verify(routeEngineListener).onDestinationSet(destination); + } + + @Test + public void onRouteResponseReceived_routeUpdateCallbackIsCalled() throws Exception { + ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); + NavigationViewRouter routeEngine = buildRouteEngine(routeEngineListener); + DirectionsResponse response = buildDirectionsResponse(); + DirectionsRoute route = response.routes().get(0); + RouteProgress routeProgress = mock(RouteProgress.class); + + routeEngine.onResponseReceived(response, routeProgress); + + verify(routeEngineListener).onRouteUpdate(route); + } + + @Test + public void onErrorReceived_errorListenerIsTriggered() { + ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); + NavigationViewRouter routeEngine = buildRouteEngine(routeEngineListener); + Throwable throwable = mock(Throwable.class); + when(throwable.getMessage()).thenReturn("error"); + + routeEngine.onErrorReceived(throwable); + + verify(routeEngineListener).onRouteRequestError(eq("error")); + } + + @Test + public void findRouteFrom_fastConnectionGoesToOnline() { + RouteFetcher onlineRouter = mock(RouteFetcher.class); + NavigationRoute.Builder builder = mock(NavigationRoute.Builder.class); + when(onlineRouter.buildRequestFrom(any(Location.class), any(RouteProgress.class))).thenReturn(builder); + ConnectivityStatusProvider status = mock(ConnectivityStatusProvider.class); + when(status.isConnectedFast()).thenReturn(true); + NavigationViewRouter router = new NavigationViewRouter( + onlineRouter, + null, // Null offline (simulate no data) + status, + mock(RouteComparator.class), + mock(ViewRouteListener.class) + ); + router.updateLocation(mock(Location.class)); + + router.findRouteFrom(mock(RouteProgress.class)); + + verify(onlineRouter).findRouteWith(builder); + } + + @Test + public void findRouteFrom_nullOfflineAndSlowConnectionGoesToOnline() { + RouteFetcher onlineRouter = mock(RouteFetcher.class); + NavigationRoute.Builder builder = mock(NavigationRoute.Builder.class); + when(onlineRouter.buildRequestFrom(any(Location.class), any(RouteProgress.class))).thenReturn(builder); + ConnectivityStatusProvider status = mock(ConnectivityStatusProvider.class); + when(status.isConnectedFast()).thenReturn(false); + when(status.isConnected()).thenReturn(true); + NavigationViewRouter router = new NavigationViewRouter( + onlineRouter, + null, // Null offline (simulate no data) + status, + mock(RouteComparator.class), + mock(ViewRouteListener.class) + ); + router.updateLocation(mock(Location.class)); + + router.findRouteFrom(mock(RouteProgress.class)); + + verify(onlineRouter).findRouteWith(builder); + } + + @Test + public void findRouteFrom_slowConnectionGoesToOffline() { + RouteFetcher onlineRouter = mock(RouteFetcher.class); + NavigationRoute.Builder builder = mock(NavigationRoute.Builder.class); + when(onlineRouter.buildRequestFrom(any(Location.class), any(RouteProgress.class))).thenReturn(builder); + ConnectivityStatusProvider status = mock(ConnectivityStatusProvider.class); + when(status.isConnectedFast()).thenReturn(false); + NavigationViewOfflineRouter offlineRouter = mock(NavigationViewOfflineRouter.class); + when(offlineRouter.isConfigured()).thenReturn(true); + NavigationViewRouter router = new NavigationViewRouter( + onlineRouter, + offlineRouter, + status, + mock(RouteComparator.class), + mock(ViewRouteListener.class) + ); + router.updateLocation(mock(Location.class)); + + router.findRouteFrom(mock(RouteProgress.class)); + + verify(offlineRouter).findRouteWith(builder); + } + + @Test + public void findRouteFrom_secondRequestIgnored() { + RouteFetcher onlineRouter = mock(RouteFetcher.class); + NavigationRoute.Builder builder = mock(NavigationRoute.Builder.class); + when(onlineRouter.buildRequestFrom(any(Location.class), any(RouteProgress.class))).thenReturn(builder); + ConnectivityStatusProvider status = mock(ConnectivityStatusProvider.class); + when(status.isConnectedFast()).thenReturn(false); + NavigationViewOfflineRouter offlineRouter = mock(NavigationViewOfflineRouter.class); + when(offlineRouter.isConfigured()).thenReturn(true); + NavigationViewRouter router = new NavigationViewRouter( + onlineRouter, + offlineRouter, + status, + mock(RouteComparator.class), + mock(ViewRouteListener.class) + ); + router.updateLocation(mock(Location.class)); + + router.findRouteFrom(mock(RouteProgress.class)); + router.findRouteFrom(mock(RouteProgress.class)); + + verify(offlineRouter, times(1)).findRouteWith(builder); + } + + @Test + public void onDestroy_clearsListeners() { + RouteFetcher onlineRouter = mock(RouteFetcher.class); + NavigationViewRouter router = new NavigationViewRouter( + onlineRouter, + mock(NavigationViewOfflineRouter.class), + mock(ConnectivityStatusProvider.class), + mock(RouteComparator.class), + mock(ViewRouteListener.class) + ); + + router.onDestroy(); + + verify(onlineRouter).cancelRouteCall(); + } + + @Test + public void onDestroy_cancelsOnlineRouteCall() { + RouteFetcher onlineRouter = mock(RouteFetcher.class); + NavigationViewRouter router = new NavigationViewRouter( + onlineRouter, + mock(NavigationViewOfflineRouter.class), + mock(ConnectivityStatusProvider.class), + mock(RouteComparator.class), + mock(ViewRouteListener.class) + ); + + router.onDestroy(); + + verify(onlineRouter).clearListeners(); + } + + @NonNull + private NavigationViewRouter buildRouteEngine(ViewRouteListener routeEngineListener) { + return new NavigationViewRouter(mock(RouteFetcher.class), mock(ConnectivityStatusProvider.class), + routeEngineListener); + } + + private NavigationViewOptions buildNavigationViewOptionsWithRoute() throws IOException { + return NavigationViewOptions.builder() + .directionsRoute(buildDirectionsRoute()) + .build(); + } + + private Point findDestinationPoint(NavigationViewOptions options) { + List coordinates = options.directionsRoute().routeOptions().coordinates(); + return coordinates.get(coordinates.size() - 1); + } + + private DirectionsRoute buildDirectionsRoute() throws IOException { + Gson gson = new GsonBuilder().registerTypeAdapterFactory(DirectionsAdapterFactory.create()).create(); + String body = loadJsonFixture(DIRECTIONS_PRECISION_6); + DirectionsResponse response = gson.fromJson(body, DirectionsResponse.class); + RouteOptions options = buildRouteOptionsWithCoordinates(response); + return response.routes().get(0).toBuilder().routeOptions(options).build(); + } + + private DirectionsResponse buildDirectionsResponse() throws IOException { + Gson gson = new GsonBuilder().registerTypeAdapterFactory(DirectionsAdapterFactory.create()).create(); + String body = loadJsonFixture(DIRECTIONS_PRECISION_6); + return gson.fromJson(body, DirectionsResponse.class); + } + + private RouteOptions buildRouteOptionsWithCoordinates(DirectionsResponse response) { + List coordinates = new ArrayList<>(); + for (DirectionsWaypoint waypoint : response.waypoints()) { + coordinates.add(waypoint.location()); + } + return RouteOptions.builder() + .baseUrl(Constants.BASE_API_URL) + .user("user") + .profile("profile") + .accessToken(ACCESS_TOKEN) + .requestUuid("uuid") + .geometries("mocked_geometries") + .coordinates(coordinates).build(); + } +} diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouteFoundCallbackTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouteFoundCallbackTest.java new file mode 100644 index 00000000000..f589fda6193 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouteFoundCallbackTest.java @@ -0,0 +1,38 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import com.mapbox.api.directions.v5.models.DirectionsRoute; +import com.mapbox.services.android.navigation.v5.navigation.OfflineError; + +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class OfflineRouteFoundCallbackTest { + + @Test + public void onRouteFound_routerIsUpdated() { + NavigationViewRouter router = mock(NavigationViewRouter.class); + DirectionsRoute offlineRoute = mock(DirectionsRoute.class); + OfflineRouteFoundCallback callback = new OfflineRouteFoundCallback(router); + + callback.onRouteFound(offlineRoute); + + verify(router).updateCurrentRoute(offlineRoute); + } + + @Test + public void onError_routerReceivesErrorMessage() { + NavigationViewRouter router = mock(NavigationViewRouter.class); + OfflineError error = mock(OfflineError.class); + String errorMessage = "error message"; + when(error.getMessage()).thenReturn(errorMessage); + OfflineRouteFoundCallback callback = new OfflineRouteFoundCallback(router); + + callback.onError(error); + + verify(router).onRequestError(eq(errorMessage)); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouterConfiguredCallbackTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouterConfiguredCallbackTest.java new file mode 100644 index 00000000000..8dc89d14433 --- /dev/null +++ b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/OfflineRouterConfiguredCallbackTest.java @@ -0,0 +1,20 @@ +package com.mapbox.services.android.navigation.ui.v5; + +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class OfflineRouterConfiguredCallbackTest { + + @Test + public void onConfigured_configuredIsSetToTrue() { + NavigationViewOfflineRouter offlineRouter = mock(NavigationViewOfflineRouter.class); + OfflineRouterConfiguredCallback callback = new OfflineRouterConfiguredCallback(offlineRouter); + + callback.onConfigured(122); + + verify(offlineRouter).setIsConfigured(eq(true)); + } +} \ No newline at end of file diff --git a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/ViewRouteFetcherTest.java b/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/ViewRouteFetcherTest.java deleted file mode 100644 index 88a6e2511d4..00000000000 --- a/libandroid-navigation-ui/src/test/java/com/mapbox/services/android/navigation/ui/v5/ViewRouteFetcherTest.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.mapbox.services.android.navigation.ui.v5; - -import android.content.Context; -import android.support.annotation.NonNull; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.mapbox.api.directions.v5.DirectionsAdapterFactory; -import com.mapbox.api.directions.v5.models.DirectionsResponse; -import com.mapbox.api.directions.v5.models.DirectionsRoute; -import com.mapbox.api.directions.v5.models.DirectionsWaypoint; -import com.mapbox.api.directions.v5.models.RouteOptions; -import com.mapbox.core.constants.Constants; -import com.mapbox.geojson.Point; -import com.mapbox.services.android.navigation.ui.v5.route.ViewRouteFetcher; -import com.mapbox.services.android.navigation.ui.v5.route.ViewRouteListener; -import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; - -import org.junit.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import static junit.framework.Assert.assertNotNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -public class ViewRouteFetcherTest extends BaseTest { - - private static final String DIRECTIONS_PRECISION_6 = "directions_v5_precision_6.json"; - - @Test - public void sanity() { - ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); - ViewRouteFetcher routeEngine = buildRouteEngine(routeEngineListener); - - assertNotNull(routeEngine); - } - - @Test - public void onExtractOptionsWithRoute_routeUpdateCallbackIsCalled() throws Exception { - ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); - ViewRouteFetcher routeEngine = buildRouteEngine(routeEngineListener); - NavigationViewOptions options = buildNavigationViewOptionsWithRoute(); - DirectionsRoute directionsRoute = options.directionsRoute(); - - routeEngine.extractRouteOptions(options); - - verify(routeEngineListener).onRouteUpdate(directionsRoute); - } - - @Test - public void onExtractOptionsWithRoute_destinationCallbackIsCalled() throws Exception { - ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); - ViewRouteFetcher routeEngine = buildRouteEngine(routeEngineListener); - NavigationViewOptions options = buildNavigationViewOptionsWithRoute(); - Point destination = findDestinationPoint(options); - - routeEngine.extractRouteOptions(options); - - verify(routeEngineListener).onDestinationSet(destination); - } - - @Test - public void onRouteResponseReceived_routeUpdateCallbackIsCalled() throws Exception { - ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); - ViewRouteFetcher routeEngine = buildRouteEngine(routeEngineListener); - DirectionsResponse response = buildDirectionsResponse(); - DirectionsRoute route = response.routes().get(0); - RouteProgress routeProgress = mock(RouteProgress.class); - - routeEngine.onResponseReceived(response, routeProgress); - - verify(routeEngineListener).onRouteUpdate(route); - } - - @Test - public void onErrorReceived_errorListenerIsTriggered() { - ViewRouteListener routeEngineListener = mock(ViewRouteListener.class); - ViewRouteFetcher routeEngine = buildRouteEngine(routeEngineListener); - Throwable throwable = mock(Throwable.class); - - routeEngine.onErrorReceived(throwable); - - verify(routeEngineListener).onRouteRequestError(throwable); - } - - @NonNull - private ViewRouteFetcher buildRouteEngine(ViewRouteListener routeEngineListener) { - return new ViewRouteFetcher(mock(Context.class), ACCESS_TOKEN, routeEngineListener); - } - - private NavigationViewOptions buildNavigationViewOptionsWithRoute() throws IOException { - return NavigationViewOptions.builder() - .directionsRoute(buildDirectionsRoute()) - .build(); - } - - private Point findDestinationPoint(NavigationViewOptions options) { - List coordinates = options.directionsRoute().routeOptions().coordinates(); - return coordinates.get(coordinates.size() - 1); - } - - private DirectionsRoute buildDirectionsRoute() throws IOException { - Gson gson = new GsonBuilder().registerTypeAdapterFactory(DirectionsAdapterFactory.create()).create(); - String body = loadJsonFixture(DIRECTIONS_PRECISION_6); - DirectionsResponse response = gson.fromJson(body, DirectionsResponse.class); - RouteOptions options = buildRouteOptionsWithCoordinates(response); - return response.routes().get(0).toBuilder().routeOptions(options).build(); - } - - private DirectionsResponse buildDirectionsResponse() throws IOException { - Gson gson = new GsonBuilder().registerTypeAdapterFactory(DirectionsAdapterFactory.create()).create(); - String body = loadJsonFixture(DIRECTIONS_PRECISION_6); - return gson.fromJson(body, DirectionsResponse.class); - } - - private RouteOptions buildRouteOptionsWithCoordinates(DirectionsResponse response) { - List coordinates = new ArrayList<>(); - for (DirectionsWaypoint waypoint : response.waypoints()) { - coordinates.add(waypoint.location()); - } - return RouteOptions.builder() - .baseUrl(Constants.BASE_API_URL) - .user("user") - .profile("profile") - .accessToken(ACCESS_TOKEN) - .requestUuid("uuid") - .geometries("mocked_geometries") - .coordinates(coordinates).build(); - } -} diff --git a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/route/RouteFetcher.java b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/route/RouteFetcher.java index 6c85135fdc5..3759c4a3cb5 100644 --- a/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/route/RouteFetcher.java +++ b/libandroid-navigation/src/main/java/com/mapbox/services/android/navigation/v5/route/RouteFetcher.java @@ -57,6 +57,13 @@ public RouteFetcher(Context context, String accessToken) { this.accessToken = accessToken; } + // Package private (no modifier) for testing purposes + RouteFetcher(Context context, String accessToken, RouteUtils routeUtils) { + this.contextWeakReference = new WeakReference<>(context); + this.accessToken = accessToken; + this.routeUtils = routeUtils; + } + /** * Adds a {@link RouteListener} to this class to be triggered when a route * response has been received. @@ -89,48 +96,69 @@ public void clearListeners() { * @since 0.13.0 */ public void findRouteFromRouteProgress(Location location, RouteProgress routeProgress) { - if (isInvalidProgress(location, routeProgress)) { - return; - } this.routeProgress = routeProgress; - NavigationRoute.Builder builder = buildRequestFromLocation(location, routeProgress); - executeRouteCall(builder); + NavigationRoute.Builder builder = buildRequestFrom(location, routeProgress); + findRouteWith(builder); } /** - * Cancels the Directions API call if it has not been executed yet. + * Build a route request given the passed {@link Location} and {@link RouteProgress}. + *

+ * Uses {@link RouteOptions#coordinates()} and {@link RouteProgress#remainingWaypoints()} + * to determine the amount of remaining waypoints there are along the given route. + * + * @param location current location of the device + * @param routeProgress for remaining waypoints along the route + * @return request reflecting the current progress */ - public void cancelRouteCall() { - if (navigationRoute != null) { - navigationRoute.cancelCall(); - } - } - @Nullable - private NavigationRoute.Builder buildRequestFromLocation(Location location, RouteProgress progress) { + public NavigationRoute.Builder buildRequestFrom(Location location, RouteProgress routeProgress) { Context context = contextWeakReference.get(); - if (context == null) { + if (invalid(context, location, routeProgress)) { return null; } Point origin = Point.fromLngLat(location.getLongitude(), location.getLatitude()); Double bearing = location.hasBearing() ? Float.valueOf(location.getBearing()).doubleValue() : null; - RouteOptions options = progress.directionsRoute().routeOptions(); + RouteOptions options = routeProgress.directionsRoute().routeOptions(); NavigationRoute.Builder builder = NavigationRoute.builder(context) + .accessToken(accessToken) .origin(origin, bearing, BEARING_TOLERANCE) .routeOptions(options); - List remainingWaypoints = routeUtils.calculateRemainingWaypoints(progress); + List remainingWaypoints = routeUtils.calculateRemainingWaypoints(routeProgress); if (remainingWaypoints == null) { Timber.e("An error occurred fetching a new route"); return null; } addDestination(remainingWaypoints, builder); addWaypoints(remainingWaypoints, builder); - addWaypointNames(progress, builder); - addApproaches(progress, builder); + addWaypointNames(routeProgress, builder); + addApproaches(routeProgress, builder); return builder; } + /** + * Executes the given NavigationRoute builder, eventually triggering + * any {@link RouteListener} that has been added via {@link RouteFetcher#addRouteListener(RouteListener)}. + * + * @param builder to be executed + */ + public void findRouteWith(NavigationRoute.Builder builder) { + if (builder != null) { + navigationRoute = builder.build(); + navigationRoute.getRoute(directionsResponseCallback); + } + } + + /** + * Cancels the Directions API call if it has not been executed yet. + */ + public void cancelRouteCall() { + if (navigationRoute != null) { + navigationRoute.cancelCall(); + } + } + private void addDestination(List remainingWaypoints, NavigationRoute.Builder builder) { if (!remainingWaypoints.isEmpty()) { builder.destination(retrieveDestinationWaypoint(remainingWaypoints)); @@ -180,16 +208,8 @@ private String[] calculateRemainingApproaches(RouteProgress routeProgress) { return approaches; } - private void executeRouteCall(NavigationRoute.Builder builder) { - if (builder != null) { - builder.accessToken(accessToken); - navigationRoute = builder.build(); - navigationRoute.getRoute(directionsResponseCallback); - } - } - - private boolean isInvalidProgress(Location location, RouteProgress routeProgress) { - return location == null || routeProgress == null; + private boolean invalid(Context context, Location location, RouteProgress routeProgress) { + return context == null || location == null || routeProgress == null; } private Callback directionsResponseCallback = new Callback() { diff --git a/libandroid-navigation/src/test/java/com/mapbox/services/android/navigation/v5/route/RouteFetcherTest.java b/libandroid-navigation/src/test/java/com/mapbox/services/android/navigation/v5/route/RouteFetcherTest.java index 6e9c91acafa..2660874c24e 100644 --- a/libandroid-navigation/src/test/java/com/mapbox/services/android/navigation/v5/route/RouteFetcherTest.java +++ b/libandroid-navigation/src/test/java/com/mapbox/services/android/navigation/v5/route/RouteFetcherTest.java @@ -1,14 +1,36 @@ package com.mapbox.services.android.navigation.v5.route; import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.location.Location; +import com.mapbox.api.directions.v5.models.DirectionsRoute; +import com.mapbox.api.directions.v5.models.RouteOptions; +import com.mapbox.geojson.Point; import com.mapbox.services.android.navigation.v5.navigation.NavigationRoute; +import com.mapbox.services.android.navigation.v5.routeprogress.RouteProgress; +import com.mapbox.services.android.navigation.v5.utils.RouteUtils; +import org.jetbrains.annotations.NotNull; import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import retrofit2.Callback; + +import static junit.framework.TestCase.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +@RunWith(RobolectricTestRunner.class) public class RouteFetcherTest { @Test @@ -21,4 +43,72 @@ public void cancelRouteCall_cancelsWithNonNullNavigationRoute() { verify(navigationRoute).cancelCall(); } -} + + @Test + public void buildRequestFrom_returnsValidBuilder() { + Context context = buildMockContext(); + Location location = buildMockLocation(); + List remainingCoordinates = buildCoordinateList(); + RouteProgress routeProgress = buildMockProgress(remainingCoordinates); + RouteUtils routeUtils = mock(RouteUtils.class); + when(routeUtils.calculateRemainingWaypoints(eq(routeProgress))).thenReturn(remainingCoordinates); + RouteFetcher routeFetcher = new RouteFetcher(context, "pk.xx", routeUtils); + + NavigationRoute.Builder builder = routeFetcher.buildRequestFrom(location, routeProgress); + + assertNotNull(builder); + } + + @Test + public void findRouteWith_callNavigationRoute() { + Context context = mock(Context.class); + NavigationRoute navigationRoute = mock(NavigationRoute.class); + NavigationRoute.Builder builder = mock(NavigationRoute.Builder.class); + when(builder.build()).thenReturn(navigationRoute); + RouteFetcher routeFetcher = new RouteFetcher(context, "pk.xx", navigationRoute); + + routeFetcher.findRouteWith(builder); + + verify(navigationRoute).getRoute(any(Callback.class)); + } + + @NotNull + private Context buildMockContext() { + Context context = mock(Context.class); + Resources resources = mock(Resources.class); + Configuration configuration = new Configuration(); + configuration.setLocale(Locale.US); + when(resources.getConfiguration()).thenReturn(configuration); + when(context.getResources()).thenReturn(resources); + return context; + } + + @NotNull + private Location buildMockLocation() { + Location location = mock(Location.class); + when(location.getLongitude()).thenReturn(1.23); + when(location.getLatitude()).thenReturn(2.34); + return location; + } + + @NotNull + private RouteProgress buildMockProgress(List remainingCoordinates) { + DirectionsRoute route = mock(DirectionsRoute.class); + RouteOptions routeOptions = mock(RouteOptions.class); + when(routeOptions.coordinates()).thenReturn(remainingCoordinates); + when(route.routeOptions()).thenReturn(routeOptions); + RouteProgress routeProgress = mock(RouteProgress.class); + when(routeProgress.remainingWaypoints()).thenReturn(2); + when(routeProgress.directionsRoute()).thenReturn(route); + return routeProgress; + } + + private List buildCoordinateList() { + List coordinates = new ArrayList<>(); + coordinates.add(Point.fromLngLat(1.234, 5.678)); + coordinates.add(Point.fromLngLat(9.012, 3.456)); + coordinates.add(Point.fromLngLat(7.890, 1.234)); + coordinates.add(Point.fromLngLat(5.678, 9.012)); + return coordinates; + } +} \ No newline at end of file