package com.mapswithme.maps.routing; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.support.annotation.DimenRes; import android.support.annotation.IntRange; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; import android.support.v4.util.Pair; import android.support.v7.app.AlertDialog; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.view.View; import android.widget.TextView; import com.mapswithme.maps.Framework; import com.mapswithme.maps.MwmApplication; import com.mapswithme.maps.R; import com.mapswithme.maps.bookmarks.data.FeatureId; import com.mapswithme.maps.bookmarks.data.MapObject; import com.mapswithme.maps.downloader.MapManager; import com.mapswithme.maps.location.LocationHelper; import com.mapswithme.maps.taxi.TaxiInfo; import com.mapswithme.maps.taxi.TaxiInfoError; import com.mapswithme.maps.taxi.TaxiManager; import com.mapswithme.util.Config; import com.mapswithme.util.ConnectionState; import com.mapswithme.util.NetworkPolicy; import com.mapswithme.util.StringUtils; import com.mapswithme.util.Utils; import com.mapswithme.util.concurrency.UiThread; import com.mapswithme.util.log.Logger; import com.mapswithme.util.log.LoggerFactory; import com.mapswithme.util.statistics.AlohaHelper; import com.mapswithme.util.statistics.Statistics; import java.util.Calendar; import java.util.concurrent.TimeUnit; import static com.mapswithme.util.statistics.Statistics.EventName.ROUTING_POINT_ADD; import static com.mapswithme.util.statistics.Statistics.EventName.ROUTING_POINT_REMOVE; @android.support.annotation.UiThread public class RoutingController implements TaxiManager.TaxiListener { private static final String TAG = RoutingController.class.getSimpleName(); private enum State { NONE, PREPARE, NAVIGATION } enum BuildState { NONE, BUILDING, BUILT, ERROR } public interface Container { FragmentActivity getActivity(); void showSearch(); void showRoutePlan(boolean show, @Nullable Runnable completionListener); void showNavigation(boolean show); void showDownloader(boolean openDownloaded); void updateMenu(); void onTaxiInfoReceived(@NonNull TaxiInfo info); void onTaxiError(@NonNull TaxiManager.ErrorCode code); void onNavigationCancelled(); void onNavigationStarted(); void onAddedStop(); void onRemovedStop(); void onBuiltRoute(); boolean isSubwayEnabled(); /** * @param progress progress to be displayed. * */ void updateBuildProgress(@IntRange(from = 0, to = 100) int progress, @Framework.RouterType int router); } private static final int NO_WAITING_POI_PICK = -1; private static final RoutingController sInstance = new RoutingController(); private final Logger mLogger = LoggerFactory.INSTANCE.getLogger(LoggerFactory.Type.ROUTING); @Nullable private Container mContainer; private BuildState mBuildState = BuildState.NONE; private State mState = State.NONE; @RoutePointInfo.RouteMarkType private int mWaitingPoiPickType = NO_WAITING_POI_PICK; private int mLastBuildProgress; @Framework.RouterType private int mLastRouterType; private boolean mHasContainerSavedState; private boolean mContainsCachedResult; private int mLastResultCode; private String[] mLastMissingMaps; @Nullable private RoutingInfo mCachedRoutingInfo; @Nullable private TransitRouteInfo mCachedTransitRouteInfo; private boolean mTaxiRequestHandled; private boolean mTaxiPlanning; private boolean mInternetConnected; private int mInvalidRoutePointsTransactionId; private int mRemovingIntermediatePointsTransactionId; @SuppressWarnings("FieldCanBeLocal") private final Framework.RoutingListener mRoutingListener = new Framework.RoutingListener() { @MainThread @Override public void onRoutingEvent(final int resultCode, @Nullable final String[] missingMaps) { mLogger.d(TAG, "onRoutingEvent(resultCode: " + resultCode + ")"); mLastResultCode = resultCode; mLastMissingMaps = missingMaps; mContainsCachedResult = true; if (mLastResultCode == ResultCodesHelper.NO_ERROR || ResultCodesHelper.isMoreMapsNeeded(mLastResultCode)) { mCachedRoutingInfo = Framework.nativeGetRouteFollowingInfo(); if (mLastRouterType == Framework.ROUTER_TYPE_TRANSIT) mCachedTransitRouteInfo = Framework.nativeGetTransitRouteInfo(); setBuildState(BuildState.BUILT); mLastBuildProgress = 100; if (mContainer != null) mContainer.onBuiltRoute(); } processRoutingEvent(); } }; @SuppressWarnings("FieldCanBeLocal") private final Framework.RoutingProgressListener mRoutingProgressListener = new Framework.RoutingProgressListener() { @MainThread @Override public void onRouteBuildingProgress(float progress) { mLastBuildProgress = (int) progress; updateProgress(); } }; @SuppressWarnings("FieldCanBeLocal") private final Framework.RoutingRecommendationListener mRoutingRecommendationListener = recommendation -> UiThread.run(() -> { if (recommendation == Framework.ROUTE_REBUILD_AFTER_POINTS_LOADING) setStartPoint(LocationHelper.INSTANCE.getMyPosition()); }); @SuppressWarnings("FieldCanBeLocal") private final Framework.RoutingLoadPointsListener mRoutingLoadPointsListener = success -> { if (success) prepare(getStartPoint(), getEndPoint()); }; public static RoutingController get() { return sInstance; } private void processRoutingEvent() { if (!mContainsCachedResult || mContainer == null || mHasContainerSavedState) return; mContainsCachedResult = false; if (mLastResultCode == ResultCodesHelper.NO_ERROR) { updatePlan(); return; } if (mLastResultCode == ResultCodesHelper.CANCELLED) { setBuildState(BuildState.NONE); updatePlan(); return; } if (!ResultCodesHelper.isMoreMapsNeeded(mLastResultCode)) { setBuildState(BuildState.ERROR); mLastBuildProgress = 0; updateProgress(); } RoutingErrorDialogFragment fragment = RoutingErrorDialogFragment.create(mLastResultCode, mLastMissingMaps); fragment.show(mContainer.getActivity().getSupportFragmentManager(), RoutingErrorDialogFragment.class.getSimpleName()); } private void setState(State newState) { mLogger.d(TAG, "[S] State: " + mState + " -> " + newState + ", BuildState: " + mBuildState); mState = newState; if (mContainer != null) mContainer.updateMenu(); } private void setBuildState(BuildState newState) { mLogger.d(TAG, "[B] State: " + mState + ", BuildState: " + mBuildState + " -> " + newState); mBuildState = newState; if (mBuildState == BuildState.BUILT && !MapObject.isOfType(MapObject.MY_POSITION, getStartPoint())) Framework.nativeDisableFollowing(); if (mContainer != null) mContainer.updateMenu(); } private void updateProgress() { if (isTaxiPlanning()) return; if (mContainer != null) mContainer.updateBuildProgress(mLastBuildProgress, mLastRouterType); } private void showRoutePlan() { if (mContainer != null) mContainer.showRoutePlan(true, new Runnable() { @Override public void run() { updatePlan(); } }); } public void attach(@NonNull Container container) { mContainer = container; } public void initialize() { mLastRouterType = Framework.nativeGetLastUsedRouter(); mInvalidRoutePointsTransactionId = Framework.nativeInvalidRoutePointsTransactionId(); mRemovingIntermediatePointsTransactionId = mInvalidRoutePointsTransactionId; Framework.nativeSetRoutingListener(mRoutingListener); Framework.nativeSetRouteProgressListener(mRoutingProgressListener); Framework.nativeSetRoutingRecommendationListener(mRoutingRecommendationListener); Framework.nativeSetRoutingLoadPointsListener(mRoutingLoadPointsListener); TaxiManager.INSTANCE.setTaxiListener(this); } public void detach() { mContainer = null; } @MainThread public void restore() { mHasContainerSavedState = false; if (isPlanning()) showRoutePlan(); if (mContainer != null) { if (isTaxiPlanning()) mContainer.updateBuildProgress(0, mLastRouterType); mContainer.showNavigation(isNavigating()); mContainer.updateMenu(); } processRoutingEvent(); } public void onSaveState() { mHasContainerSavedState = true; } private void build() { Framework.nativeRemoveRoute(); mLogger.d(TAG, "build"); mTaxiRequestHandled = false; mLastBuildProgress = 0; mInternetConnected = ConnectionState.isConnected(); if (isTaxiRouterType()) { if (!mInternetConnected) { completeTaxiRequest(); return; } MapObject start = getStartPoint(); MapObject end = getEndPoint(); if (start != null && end != null) requestTaxiInfo(start, end); } setBuildState(BuildState.BUILDING); updatePlan(); Statistics.INSTANCE.trackRouteBuild(mLastRouterType, getStartPoint(), getEndPoint()); org.alohalytics.Statistics.logEvent(AlohaHelper.ROUTING_BUILD, new String[]{Statistics.EventParam.FROM, Statistics.getPointType(getStartPoint()), Statistics.EventParam.TO, Statistics.getPointType(getEndPoint())}); Framework.nativeBuildRoute(); } private void completeTaxiRequest() { mTaxiRequestHandled = true; if (mContainer != null) { mContainer.updateBuildProgress(100, mLastRouterType); mContainer.updateMenu(); } } private void showDisclaimer(final MapObject startPoint, final MapObject endPoint, final boolean fromApi) { if (mContainer == null) return; StringBuilder builder = new StringBuilder(); for (int resId : new int[] { R.string.dialog_routing_disclaimer_priority, R.string.dialog_routing_disclaimer_precision, R.string.dialog_routing_disclaimer_recommendations, R.string.dialog_routing_disclaimer_borders, R.string.dialog_routing_disclaimer_beware }) builder.append(MwmApplication.get().getString(resId)).append("\n\n"); new AlertDialog.Builder(mContainer.getActivity()) .setTitle(R.string.dialog_routing_disclaimer_title) .setMessage(builder.toString()) .setCancelable(false) .setNegativeButton(R.string.decline, null) .setPositiveButton(R.string.accept, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dlg, int which) { Config.acceptRoutingDisclaimer(); prepare(startPoint, endPoint, fromApi); } }).show(); } public void restoreRoute() { if (Framework.nativeHasSavedRoutePoints()) Framework.nativeLoadRoutePoints(); } public void saveRoute() { if (isNavigating() || (isPlanning() && isBuilt())) Framework.nativeSaveRoutePoints(); } public void deleteSavedRoute() { Framework.nativeDeleteSavedRoutePoints(); } public void prepare(boolean canUseMyPositionAsStart, @Nullable MapObject endPoint) { prepare(canUseMyPositionAsStart, endPoint, false); } public void prepare(boolean canUseMyPositionAsStart, @Nullable MapObject endPoint, boolean fromApi) { MapObject startPoint = canUseMyPositionAsStart ? LocationHelper.INSTANCE.getMyPosition() : null; prepare(startPoint, endPoint, fromApi); } public void prepare(boolean canUseMyPositionAsStart, @Nullable MapObject endPoint, @Framework.RouterType int type, boolean fromApi) { MapObject startPoint = canUseMyPositionAsStart ? LocationHelper.INSTANCE.getMyPosition() : null; prepare(startPoint, endPoint, type, fromApi); } public void prepare(@Nullable MapObject startPoint, @Nullable MapObject endPoint) { prepare(startPoint, endPoint, false); } public void prepare(@Nullable MapObject startPoint, @Nullable MapObject endPoint, boolean fromApi) { mLogger.d(TAG, "prepare (" + (endPoint == null ? "route)" : "p2p)")); if (!Config.isRoutingDisclaimerAccepted()) { showDisclaimer(startPoint, endPoint, fromApi); return; } initLastRouteType(startPoint, endPoint, fromApi); prepare(startPoint, endPoint, mLastRouterType, fromApi); } private void initLastRouteType(@Nullable MapObject startPoint, @Nullable MapObject endPoint, boolean fromApi) { if (isSubwayEnabled() && !fromApi) { mLastRouterType = Framework.ROUTER_TYPE_TRANSIT; return; } if (startPoint != null && endPoint != null) mLastRouterType = Framework.nativeGetBestRouter(startPoint.getLat(), startPoint.getLon(), endPoint.getLat(), endPoint.getLon()); } private boolean isSubwayEnabled() { return mContainer != null && mContainer.isSubwayEnabled(); } public void prepare(final @Nullable MapObject startPoint, final @Nullable MapObject endPoint, @Framework.RouterType int routerType) { prepare(startPoint, endPoint, routerType, false); } public void prepare(final @Nullable MapObject startPoint, final @Nullable MapObject endPoint, @Framework.RouterType int routerType, boolean fromApi) { cancel(); setState(State.PREPARE); mLastRouterType = routerType; Framework.nativeSetRouter(mLastRouterType); if (startPoint != null || endPoint != null) setPointsInternal(startPoint, endPoint); if (mContainer != null) mContainer.showRoutePlan(true, new Runnable() { @Override public void run() { if (startPoint == null || endPoint == null) updatePlan(); else build(); } }); if (startPoint != null) trackPointAdd(startPoint, RoutePointInfo.ROUTE_MARK_START, false, false, fromApi); if (endPoint != null) trackPointAdd(endPoint, RoutePointInfo.ROUTE_MARK_FINISH, false, false, fromApi); } private static void trackPointAdd(@NonNull MapObject point, @RoutePointInfo.RouteMarkType int type, boolean isPlanning, boolean isNavigating, boolean fromApi) { boolean isMyPosition = point.getMapObjectType() == MapObject.MY_POSITION; Statistics.INSTANCE.trackRoutingPoint(ROUTING_POINT_ADD, type, isPlanning, isNavigating, isMyPosition, fromApi); } private static void trackPointRemove(@NonNull MapObject point, @RoutePointInfo.RouteMarkType int type, boolean isPlanning, boolean isNavigating, boolean fromApi) { boolean isMyPosition = point.getMapObjectType() == MapObject.MY_POSITION; Statistics.INSTANCE.trackRoutingPoint(ROUTING_POINT_REMOVE, type, isPlanning, isNavigating, isMyPosition, fromApi); } public void start() { mLogger.d(TAG, "start"); // This saving is needed just for situation when the user starts navigation // and then app crashes. So, the previous route will be restored on the next app launch. saveRoute(); MapObject my = LocationHelper.INSTANCE.getMyPosition(); if (my == null || !MapObject.isOfType(MapObject.MY_POSITION, getStartPoint())) { Statistics.INSTANCE.trackEvent(Statistics.EventName.ROUTING_START_SUGGEST_REBUILD); AlohaHelper.logClick(AlohaHelper.ROUTING_START_SUGGEST_REBUILD); suggestRebuildRoute(); return; } setState(State.NAVIGATION); if (mContainer != null) { mContainer.showRoutePlan(false, null); mContainer.showNavigation(true); mContainer.onNavigationStarted(); } Framework.nativeFollowRoute(); LocationHelper.INSTANCE.restart(); } public void addStop(@NonNull MapObject mapObject) { addRoutePoint(RoutePointInfo.ROUTE_MARK_INTERMEDIATE, mapObject); trackPointAdd(mapObject, RoutePointInfo.ROUTE_MARK_INTERMEDIATE, isPlanning(), isNavigating(), false); build(); if (mContainer != null) mContainer.onAddedStop(); backToPlaningStateIfNavigating(); } public void removeStop(@NonNull MapObject mapObject) { RoutePointInfo info = mapObject.getRoutePointInfo(); if (info == null) throw new AssertionError("A stop point must have the route point info!"); applyRemovingIntermediatePointsTransaction(); Framework.nativeRemoveRoutePoint(info.mMarkType, info.mIntermediateIndex); trackPointRemove(mapObject, info.mMarkType, isPlanning(), isNavigating(), false); build(); if (mContainer != null) mContainer.onRemovedStop(); backToPlaningStateIfNavigating(); } private void backToPlaningStateIfNavigating() { if (!isNavigating()) return; setState(State.PREPARE); if (mContainer != null) { mContainer.showNavigation(false); mContainer.showRoutePlan(true, null); mContainer.updateMenu(); mContainer.onNavigationCancelled(); } } private void removeIntermediatePoints() { Framework.nativeRemoveIntermediateRoutePoints(); } @NonNull private MapObject toMapObject(@NonNull RouteMarkData point) { return MapObject.createMapObject(FeatureId.EMPTY, point.mIsMyPosition ? MapObject.MY_POSITION : MapObject.POI, point.mTitle == null ? "" : point.mTitle, point.mSubtitle == null ? "" : point.mSubtitle, point.mLat, point.mLon); } public boolean isStopPointAllowed() { return Framework.nativeCouldAddIntermediatePoint() && !isTaxiRouterType(); } public boolean isRoutePoint(@NonNull MapObject mapObject) { return mapObject.getRoutePointInfo() != null; } private void suggestRebuildRoute() { if (mContainer == null) return; final AlertDialog.Builder builder = new AlertDialog.Builder(mContainer.getActivity()) .setMessage(R.string.p2p_reroute_from_current) .setCancelable(false) .setNegativeButton(R.string.cancel, null); TextView titleView = (TextView)View.inflate(mContainer.getActivity(), R.layout.dialog_suggest_reroute_title, null); titleView.setText(R.string.p2p_only_from_current); builder.setCustomTitle(titleView); if (MapObject.isOfType(MapObject.MY_POSITION, getEndPoint())) { builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { swapPoints(); } }); } else { if (LocationHelper.INSTANCE.getMyPosition() == null) builder.setMessage(null).setNegativeButton(null, null); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { setStartFromMyPosition(); } }); } builder.show(); } private void updatePlan() { updateProgress(); } private void cancelInternal() { mLogger.d(TAG, "cancelInternal"); //noinspection WrongConstant mWaitingPoiPickType = NO_WAITING_POI_PICK; mTaxiRequestHandled = false; setBuildState(BuildState.NONE); setState(State.NONE); applyRemovingIntermediatePointsTransaction(); Framework.nativeDeleteSavedRoutePoints(); Framework.nativeCloseRouting(); } public boolean cancel() { if (isPlanning()) { mLogger.d(TAG, "cancel: planning"); cancelInternal(); if (mContainer != null) mContainer.showRoutePlan(false, null); return true; } if (isNavigating()) { mLogger.d(TAG, "cancel: navigating"); cancelInternal(); if (mContainer != null) { mContainer.showNavigation(false); mContainer.updateMenu(); } if (mContainer != null) mContainer.onNavigationCancelled(); return true; } mLogger.d(TAG, "cancel: none"); return false; } public boolean isPlanning() { return mState == State.PREPARE; } boolean isTaxiPlanning() { return isTaxiRouterType() && mTaxiPlanning; } boolean isTaxiRouterType() { return mLastRouterType == Framework.ROUTER_TYPE_TAXI; } boolean isTransitType() { return mLastRouterType == Framework.ROUTER_TYPE_TRANSIT; } boolean isVehicleRouterType() { return mLastRouterType == Framework.ROUTER_TYPE_VEHICLE; } public boolean isNavigating() { return mState == State.NAVIGATION; } public boolean isVehicleNavigation() { return isNavigating() && isVehicleRouterType(); } public boolean isBuilding() { return mState == State.PREPARE && mBuildState == BuildState.BUILDING; } public boolean isErrorEncountered() { return mBuildState == BuildState.ERROR; } public boolean isBuilt() { return mBuildState == BuildState.BUILT; } public void waitForPoiPick(@RoutePointInfo.RouteMarkType int pointType){ mWaitingPoiPickType = pointType; } public boolean isWaitingPoiPick() { return mWaitingPoiPickType != NO_WAITING_POI_PICK; } public boolean isTaxiRequestHandled() { return mTaxiRequestHandled; } boolean isInternetConnected() { return mInternetConnected; } BuildState getBuildState() { return mBuildState; } @Nullable public MapObject getStartPoint() { return getStartOrEndPointByType(RoutePointInfo.ROUTE_MARK_START); } @Nullable public MapObject getEndPoint() { return getStartOrEndPointByType(RoutePointInfo.ROUTE_MARK_FINISH); } @Nullable private MapObject getStartOrEndPointByType(@RoutePointInfo.RouteMarkType int type) { RouteMarkData[] points = Framework.nativeGetRoutePoints(); int size = points.length; if (size == 0) return null; if (size == 1) { RouteMarkData point = points[0]; return point.mPointType == type ? toMapObject(point) : null; } if (type == RoutePointInfo.ROUTE_MARK_START) return toMapObject(points[0]); if (type == RoutePointInfo.ROUTE_MARK_FINISH) return toMapObject(points[size - 1]); return null; } public boolean hasStartPoint() { return getStartPoint() != null; } public boolean hasEndPoint() { return getEndPoint() != null; } @Nullable RoutingInfo getCachedRoutingInfo() { return mCachedRoutingInfo; } @Nullable TransitRouteInfo getCachedTransitInfo() { return mCachedTransitRouteInfo; } private void setPointsInternal(@Nullable MapObject startPoint, @Nullable MapObject endPoint) { if (startPoint != null) { applyRemovingIntermediatePointsTransaction(); addRoutePoint(RoutePointInfo.ROUTE_MARK_START, startPoint); if (mContainer != null) mContainer.updateMenu(); } if (endPoint != null) { applyRemovingIntermediatePointsTransaction(); addRoutePoint(RoutePointInfo.ROUTE_MARK_FINISH, endPoint); if (mContainer != null) mContainer.updateMenu(); } } void checkAndBuildRoute() { if (isWaitingPoiPick()) showRoutePlan(); if (getStartPoint() != null && getEndPoint() != null) build(); } private boolean setStartFromMyPosition() { mLogger.d(TAG, "setStartFromMyPosition"); MapObject my = LocationHelper.INSTANCE.getMyPosition(); if (my == null) { mLogger.d(TAG, "setStartFromMyPosition: no my position - skip"); return false; } return setStartPoint(my); } /** * Sets starting point. *