[android][sdk] Move java files into sdk module

Signed-off-by: Andrei Shkrob <github@shkrob.dev>
This commit is contained in:
Andrei Shkrob
2025-08-15 12:59:03 +02:00
committed by Konstantin Pastbin
parent 447266c328
commit 6a85526ac9
147 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
package app.organicmaps.sdk;
import androidx.annotation.NonNull;
public enum ChoosePositionMode
{
None(0),
Editor(1),
Api(2);
ChoosePositionMode(int mode)
{
this.mode = mode;
}
/**
* @param isBusiness selection area will be bounded by building borders, if its true (eg. true for businesses in
* buildings).
* @param applyPosition if true, map will be animated to currently selected object.
*/
public static void set(@NonNull ChoosePositionMode mode, boolean isBusiness, boolean applyPosition)
{
nativeSet(mode.mode, isBusiness, applyPosition);
}
public static ChoosePositionMode get()
{
return ChoosePositionMode.values()[nativeGet()];
}
private final int mode;
private static native void nativeSet(int mode, boolean isBusiness, boolean applyPosition);
private static native int nativeGet();
}

View File

@@ -0,0 +1,30 @@
package app.organicmaps.sdk;
import androidx.annotation.Keep;
public class DownloadResourcesLegacyActivity
{
// Error codes, should match the same codes in JNI
public static final int ERR_DOWNLOAD_SUCCESS = 0;
public static final int ERR_DISK_ERROR = -1;
public static final int ERR_NOT_ENOUGH_FREE_SPACE = -2;
public static final int ERR_STORAGE_DISCONNECTED = -3;
public static final int ERR_DOWNLOAD_ERROR = -4;
public static final int ERR_NO_MORE_FILES = -5;
public static final int ERR_FILE_IN_PROGRESS = -6;
public interface Listener
{
// Called by JNI.
@Keep
void onProgress(int bytesDownloaded);
// Called by JNI.
@Keep
void onFinish(int errorCode);
}
public static native int nativeGetBytesToDownload();
public static native int nativeStartNextFileDownload(Listener listener);
public static native void nativeCancelCurrentFile();
}

View File

@@ -0,0 +1,347 @@
package app.organicmaps.sdk;
import android.graphics.Bitmap;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
import app.organicmaps.sdk.api.ParsedRoutingData;
import app.organicmaps.sdk.api.ParsedSearchRequest;
import app.organicmaps.sdk.api.RequestType;
import app.organicmaps.sdk.bookmarks.data.DistanceAndAzimut;
import app.organicmaps.sdk.bookmarks.data.FeatureId;
import app.organicmaps.sdk.bookmarks.data.MapObject;
import app.organicmaps.sdk.routing.JunctionInfo;
import app.organicmaps.sdk.routing.RouteMarkData;
import app.organicmaps.sdk.routing.RouteMarkType;
import app.organicmaps.sdk.routing.RoutingInfo;
import app.organicmaps.sdk.routing.RoutingListener;
import app.organicmaps.sdk.routing.RoutingLoadPointsListener;
import app.organicmaps.sdk.routing.RoutingProgressListener;
import app.organicmaps.sdk.routing.RoutingRecommendationListener;
import app.organicmaps.sdk.routing.TransitRouteInfo;
import app.organicmaps.sdk.settings.SpeedCameraMode;
import app.organicmaps.sdk.util.Constants;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
* This class wraps android::Framework.cpp class
* via static methods
*/
public class Framework
{
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public static class Params3dMode
{
public boolean enabled;
public boolean buildings;
}
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public static class RouteAltitudeLimits
{
public int totalAscent;
public int totalDescent;
public String totalAscentString;
public String totalDescentString;
public boolean isMetricUnits;
}
// this class is just bridge between Java and C++ worlds, we must not create it
private Framework() {}
public static String getHttpGe0Url(double lat, double lon, double zoomLevel, String name)
{
return nativeGetGe0Url(lat, lon, zoomLevel, name)
.replaceFirst(Constants.Url.SHORT_SHARE_PREFIX, Constants.Url.HTTP_SHARE_PREFIX);
}
/**
* Generates Bitmap with route altitude image chart taking into account current map style.
* @param width is width of the image.
* @param height is height of the image.
* @return Bitmap if there's pedestrian or bicycle route and null otherwise.
*/
@Nullable
public static Bitmap generateRouteAltitudeChart(int width, int height, @NonNull RouteAltitudeLimits limits)
{
if (width <= 0 || height <= 0)
return null;
final int[] altitudeChartBits = Framework.nativeGenerateRouteAltitudeChartBits(width, height, limits);
if (altitudeChartBits == null)
return null;
return Bitmap.createBitmap(altitudeChartBits, width, height, Bitmap.Config.ARGB_8888);
}
public static void setSpeedCamerasMode(@NonNull SpeedCameraMode mode)
{
nativeSetSpeedCamManagerMode(mode.ordinal());
}
public static native void nativeShowTrackRect(long track);
public static native int nativeGetDrawScale();
public static native void nativePokeSearchInViewport();
@Size(2)
public static native double[] nativeGetScreenRectCenter();
public static native DistanceAndAzimut nativeGetDistanceAndAzimuth(double dstMerX, double dstMerY, double srcLat,
double srcLon, double north);
public static native DistanceAndAzimut nativeGetDistanceAndAzimuthFromLatLon(double dstLat, double dstLon,
double srcLat, double srcLon,
double north);
public static native String nativeFormatLatLon(double lat, double lon, int coordFormat);
public static native String nativeFormatAltitude(double alt);
public static native String nativeFormatSpeed(double speed);
public static native String nativeGetGe0Url(double lat, double lon, double zoomLevel, String name);
public static native String nativeGetGeoUri(double lat, double lon, double zoomLevel, String name);
public static native String nativeGetAddress(double lat, double lon);
public static native void nativePlacePageActivationListener(@NonNull PlacePageActivationListener listener);
public static native void nativeRemovePlacePageActivationListener(@NonNull PlacePageActivationListener listener);
// @UiThread
// public static native String nativeGetOutdatedCountriesString();
//
// @UiThread
// @NonNull
// public static native String[] nativeGetOutdatedCountries();
//
// @UiThread
// @DoAfterUpdate
// public static native int nativeToDoAfterUpdate();
//
// public static native boolean nativeIsDataVersionChanged();
//
// public static native void nativeUpdateSavedDataVersion();
private static native long nativeGetDataVersion();
public static Date getDataVersion()
{
long dataVersion = nativeGetDataVersion();
final SimpleDateFormat format = new SimpleDateFormat("yyMMdd", Locale.ENGLISH);
try
{
return format.parse(String.valueOf(dataVersion));
}
catch (ParseException e)
{
throw new AssertionError("Invalid data version code: " + dataVersion);
}
}
public static native void nativeClearApiPoints();
@NonNull
public static native @RequestType int nativeParseAndSetApiUrl(String url);
public static native ParsedRoutingData nativeGetParsedRoutingData();
public static native ParsedSearchRequest nativeGetParsedSearchRequest();
public static native @Nullable String nativeGetParsedAppName();
public static native @Nullable String nativeGetParsedOAuth2Code();
@Nullable
@Size(2)
public static native double[] nativeGetParsedCenterLatLon();
public static native @Nullable String nativeGetParsedBackUrl();
public static native void nativeDeactivatePopup();
public static native void nativeDeactivateMapSelectionCircle(boolean restoreViewport);
public static native String nativeGetDataFileExt();
public static native String[] nativeGetMovableFilesExts();
public static native String[] nativeGetBookmarksFilesExts();
public static native String nativeGetBookmarkDir();
public static native String nativeGetSettingsDir();
public static native String nativeGetWritableDir();
public static native void nativeChangeWritableDir(String newPath);
// Routing.
public static native boolean nativeIsRoutingActive();
public static native boolean nativeIsRouteBuilt();
public static native boolean nativeIsRouteBuilding();
public static native void nativeCloseRouting();
public static native void nativeBuildRoute();
public static native void nativeRemoveRoute();
public static native void nativeFollowRoute();
public static native void nativeDisableFollowing();
@Nullable
public static native RoutingInfo nativeGetRouteFollowingInfo();
@Nullable
public static native JunctionInfo[] nativeGetRouteJunctionPoints();
@Nullable
public static native final int[] nativeGenerateRouteAltitudeChartBits(int width, int height,
RouteAltitudeLimits routeAltitudeLimits);
// When an end user is going to a turn he gets sound turn instructions.
// If C++ part wants the client to pronounce an instruction nativeGenerateTurnNotifications returns
// an array of one of more strings. C++ part assumes that all these strings shall be pronounced by the client's TTS.
// For example if C++ part wants the client to pronounce "Make a right turn." this method returns
// an array with one string "Make a right turn.". The next call of the method returns nothing.
// nativeGenerateTurnNotifications shall be called by the client when a new position is available.
@Nullable
public static native String[] nativeGenerateNotifications(boolean announceStreets);
private static native void nativeSetSpeedCamManagerMode(int mode);
public static native void nativeSetRoutingListener(@NonNull RoutingListener listener);
public static native void nativeSetRouteProgressListener(@NonNull RoutingProgressListener listener);
public static native void nativeSetRoutingRecommendationListener(@NonNull RoutingRecommendationListener listener);
public static native void nativeSetRoutingLoadPointsListener(@NonNull RoutingLoadPointsListener listener);
public static native void nativeShowCountry(String countryId, boolean zoomToDownloadButton);
public static void addRoutePoint(RouteMarkData point)
{
addRoutePoint(point, true);
}
public static void addRoutePoint(RouteMarkData point, boolean reorderIntermediatePoints)
{
Framework.nativeAddRoutePoint(point.mTitle, point.mSubtitle, point.mPointType, point.mIntermediateIndex,
point.mIsMyPosition, point.mLat, point.mLon, reorderIntermediatePoints);
}
public static native void nativeAddRoutePoint(String title, String subtitle, @NonNull RouteMarkType markType,
int intermediateIndex, boolean isMyPosition, double lat, double lon,
boolean reorderIntermediatePoints);
public static native void nativeRemoveRoutePoints();
public static native void nativeRemoveRoutePoint(@NonNull RouteMarkType markType, int intermediateIndex);
public static native void nativeRemoveIntermediateRoutePoints();
public static native boolean nativeCouldAddIntermediatePoint();
@NonNull
public static native RouteMarkData[] nativeGetRoutePoints();
public static native void nativeMoveRoutePoint(int currentIndex, int targetIndex);
@NonNull
public static native TransitRouteInfo nativeGetTransitRouteInfo();
/**
* Registers all maps(.mwms). Adds them to the models, generates indexes and does all necessary stuff.
*/
public static native void nativeReloadWorldMaps();
/**
* Determines if currently is day or night at the given location. Used to switch day/night styles.
* @param utcTimeSeconds Unix time in seconds.
* @param lat latitude of the current location.
* @param lon longitude of the current location.
* @return {@code true} if it is day now or {@code false} otherwise.
*/
public static native boolean nativeIsDayTime(long utcTimeSeconds, double lat, double lon);
public static native void nativeGet3dMode(Params3dMode result);
public static native void nativeSet3dMode(boolean allow3d, boolean allow3dBuildings);
public static native boolean nativeGetAutoZoomEnabled();
public static native void nativeSetAutoZoomEnabled(boolean enabled);
public static native void nativeSetTransitSchemeEnabled(boolean enabled);
public static native void nativeSaveSettingSchemeEnabled(boolean enabled);
public static native boolean nativeIsTransitSchemeEnabled();
public static native void nativeSetIsolinesLayerEnabled(boolean enabled);
public static native boolean nativeIsIsolinesLayerEnabled();
public static native void nativeSetOutdoorsLayerEnabled(boolean enabled);
public static native boolean nativeIsOutdoorsLayerEnabled();
@NonNull
public static native MapObject nativeDeleteBookmarkFromMapObject();
@NonNull
public static native String nativeGetPoiContactUrl(int metadataType);
public static native void nativeZoomToPoint(double lat, double lon, int zoom, boolean animate);
public static native boolean nativeIsDownloadedMapAtScreenCenter();
public static native String nativeGetActiveObjectFormattedCuisine();
public static native void nativeSetVisibleRect(int left, int top, int right, int bottom);
// Navigation.
public static native boolean nativeIsRouteFinished();
public static native void nativeRunFirstLaunchAnimation();
public static native int nativeOpenRoutePointsTransaction();
public static native void nativeApplyRoutePointsTransaction(int transactionId);
public static native void nativeCancelRoutePointsTransaction(int transactionId);
public static native int nativeInvalidRoutePointsTransactionId();
public static native boolean nativeHasSavedRoutePoints();
public static native void nativeLoadRoutePoints();
public static native void nativeSaveRoutePoints();
public static native void nativeDeleteSavedRoutePoints();
public static native void nativeShowFeature(@NonNull FeatureId featureId);
public static native void nativeMakeCrash();
public static native void nativeSetPowerManagerFacility(int facilityType, boolean state);
public static native int nativeGetPowerManagerScheme();
public static native void nativeSetPowerManagerScheme(int schemeType);
public static native void nativeSetViewportCenter(double lat, double lon, int zoom);
public static native void nativeStopLocationFollow();
public static native void nativeSetSearchViewport(double lat, double lon, int zoom);
/**
* In case of the app was dumped by system to the hard drive, Java map object can be
* restored from parcelable, but c++ framework is created from scratch and internal
* place page object is not initialized. So, do not restore place page in this case.
*
* @return true if c++ framework has initialized internal place page object, otherwise - false.
*/
public static native boolean nativeHasPlacePageInfo();
public static native void nativeMemoryWarning();
public static native void nativeSaveRoute();
}

View File

@@ -0,0 +1,425 @@
package app.organicmaps.sdk;
import android.content.Context;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.BuildConfig;
import app.organicmaps.R;
import app.organicmaps.sdk.display.DisplayType;
import app.organicmaps.sdk.location.LocationHelper;
import app.organicmaps.sdk.util.Config;
import app.organicmaps.sdk.util.ROMUtils;
import app.organicmaps.sdk.util.Utils;
import app.organicmaps.sdk.util.concurrency.UiThread;
import app.organicmaps.sdk.util.log.Logger;
public final class Map
{
public interface CallbackUnsupported
{
void report();
}
public static final String ARG_LAUNCH_BY_DEEP_LINK = "launch_by_deep_link";
private static final String TAG = Map.class.getSimpleName();
// Should correspond to android::MultiTouchAction from Framework.cpp
public static final int NATIVE_ACTION_UP = 0x01;
public static final int NATIVE_ACTION_DOWN = 0x02;
public static final int NATIVE_ACTION_MOVE = 0x03;
public static final int NATIVE_ACTION_CANCEL = 0x04;
// Should correspond to gui::EWidget from skin.hpp
public static final int WIDGET_RULER = 0x01;
public static final int WIDGET_COMPASS = 0x02;
public static final int WIDGET_COPYRIGHT = 0x04;
public static final int WIDGET_SCALE_FPS_LABEL = 0x08;
// Should correspond to dp::Anchor from drape_global.hpp
public static final int ANCHOR_CENTER = 0x00;
public static final int ANCHOR_LEFT = 0x01;
public static final int ANCHOR_RIGHT = (ANCHOR_LEFT << 1);
public static final int ANCHOR_TOP = (ANCHOR_RIGHT << 1);
public static final int ANCHOR_BOTTOM = (ANCHOR_TOP << 1);
public static final int ANCHOR_LEFT_TOP = (ANCHOR_LEFT | ANCHOR_TOP);
public static final int ANCHOR_RIGHT_TOP = (ANCHOR_RIGHT | ANCHOR_TOP);
public static final int ANCHOR_LEFT_BOTTOM = (ANCHOR_LEFT | ANCHOR_BOTTOM);
public static final int ANCHOR_RIGHT_BOTTOM = (ANCHOR_RIGHT | ANCHOR_BOTTOM);
// Should correspond to df::TouchEvent::INVALID_MASKED_POINTER from user_event_stream.cpp
public static final int INVALID_POINTER_MASK = 0xFF;
public static final int INVALID_TOUCH_ID = -1;
@NonNull
private final DisplayType mDisplayType;
@Nullable
private LocationHelper mLocationHelper;
private int mCurrentCompassOffsetX;
private int mCurrentCompassOffsetY;
private int mBottomWidgetOffsetX;
private int mBottomWidgetOffsetY;
private int mHeight;
private int mWidth;
private boolean mRequireResize;
private boolean mSurfaceCreated;
private boolean mSurfaceAttached;
private boolean mLaunchByDeepLink;
@Nullable
private String mUiThemeOnPause;
@Nullable
private MapRenderingListener mMapRenderingListener;
@Nullable
private CallbackUnsupported mCallbackUnsupported;
private static int sCurrentDpi = 0;
public Map(@NonNull DisplayType mapType)
{
mDisplayType = mapType;
onCreate(false);
}
public void setLocationHelper(@NonNull LocationHelper locationHelper)
{
mLocationHelper = locationHelper;
}
/**
* Moves the map compass using the given offsets.
*
* @param context Context.
* @param offsetX Pixel offset from the top. -1 to keep the previous value.
* @param offsetY Pixel offset from the right. -1 to keep the previous value.
* @param forceRedraw True to force the compass to redraw
*/
public void updateCompassOffset(final Context context, int offsetX, int offsetY, boolean forceRedraw)
{
final int x = offsetX < 0 ? mCurrentCompassOffsetX : offsetX;
final int y = offsetY < 0 ? mCurrentCompassOffsetY : offsetY;
final int navPadding = Utils.dimen(context, R.dimen.nav_frame_padding);
final int marginX = Utils.dimen(context, R.dimen.margin_compass) + navPadding;
final int marginY = Utils.dimen(context, R.dimen.margin_compass_top) + navPadding;
nativeSetupWidget(WIDGET_COMPASS, mWidth - x - marginX, y + marginY, ANCHOR_CENTER);
if (forceRedraw && mSurfaceCreated)
nativeApplyWidgets();
mCurrentCompassOffsetX = x;
mCurrentCompassOffsetY = y;
}
public static void onCompassUpdated(double north, boolean forceRedraw)
{
nativeCompassUpdated(north, forceRedraw);
}
/**
* Moves the ruler and copyright using the given offsets.
*
* @param context Context.
* @param offsetX Pixel offset from the left. -1 to keep the previous value.
* @param offsetY Pixel offset from the bottom. -1 to keep the previous value.
*/
public void updateBottomWidgetsOffset(final Context context, int offsetX, int offsetY)
{
final int x = offsetX < 0 ? mBottomWidgetOffsetX : offsetX;
final int y = offsetY < 0 ? mBottomWidgetOffsetY : offsetY;
updateRulerOffset(context, x, y);
updateAttributionOffset(context, x, y);
mBottomWidgetOffsetX = x;
mBottomWidgetOffsetY = y;
}
/**
* Moves my position arrow to the given offset.
*
* @param offsetY Pixel offset from the bottom.
*/
public void updateMyPositionRoutingOffset(int offsetY)
{
nativeUpdateMyPositionRoutingOffset(offsetY);
}
public void onSurfaceCreated(final Context context, final Surface surface, Rect surfaceFrame, int surfaceDpi)
{
assert mLocationHelper != null : "LocationHelper must be initialized before calling onSurfaceCreated";
if (isThemeChangingProcess())
{
Logger.d(TAG, "Theme changing process, skip 'onSurfaceCreated' callback");
return;
}
Logger.d(TAG, "mSurfaceCreated = " + mSurfaceCreated);
if (nativeIsEngineCreated())
{
if (sCurrentDpi != surfaceDpi)
{
nativeUpdateEngineDpi(surfaceDpi);
sCurrentDpi = surfaceDpi;
setupWidgets(context, surfaceFrame.width(), surfaceFrame.height());
}
if (!nativeAttachSurface(surface))
{
if (mCallbackUnsupported != null)
mCallbackUnsupported.report();
return;
}
mSurfaceCreated = true;
mSurfaceAttached = true;
mRequireResize = true;
nativeResumeSurfaceRendering();
return;
}
mRequireResize = false;
setupWidgets(context, surfaceFrame.width(), surfaceFrame.height());
final boolean firstStart = mLocationHelper.isInFirstRun();
if (!nativeCreateEngine(surface, surfaceDpi, firstStart, mLaunchByDeepLink, BuildConfig.VERSION_CODE,
ROMUtils.isCustomROM()))
{
if (mCallbackUnsupported != null)
mCallbackUnsupported.report();
return;
}
sCurrentDpi = surfaceDpi;
if (firstStart)
UiThread.runLater(mLocationHelper::onExitFromFirstRun);
mSurfaceCreated = true;
mSurfaceAttached = true;
nativeResumeSurfaceRendering();
if (mMapRenderingListener != null)
mMapRenderingListener.onRenderingCreated();
}
public void onSurfaceChanged(final Context context, final Surface surface, Rect surfaceFrame,
boolean isSurfaceCreating)
{
if (isThemeChangingProcess())
{
Logger.d(TAG, "Theme changing process, skip 'onSurfaceChanged' callback");
return;
}
Logger.d(TAG, "mSurfaceCreated = " + mSurfaceCreated);
if (!mSurfaceCreated || (!mRequireResize && isSurfaceCreating))
return;
nativeSurfaceChanged(surface, surfaceFrame.width(), surfaceFrame.height());
mRequireResize = false;
setupWidgets(context, surfaceFrame.width(), surfaceFrame.height());
nativeApplyWidgets();
if (mMapRenderingListener != null)
mMapRenderingListener.onRenderingRestored();
}
public void onSurfaceDestroyed(boolean activityIsChangingConfigurations, boolean isAdded)
{
Logger.d(TAG, "mSurfaceCreated = " + mSurfaceCreated + ", mSurfaceAttached = " + mSurfaceAttached
+ ", isAdded = " + isAdded);
if (!mSurfaceCreated || !mSurfaceAttached || !isAdded)
return;
nativeDetachSurface(!activityIsChangingConfigurations);
mSurfaceCreated = !nativeDestroySurfaceOnDetach();
mSurfaceAttached = false;
}
public void setMapRenderingListener(MapRenderingListener mapRenderingListener)
{
mMapRenderingListener = mapRenderingListener;
}
public void setCallbackUnsupported(CallbackUnsupported callback)
{
mCallbackUnsupported = callback;
}
public void onCreate(boolean launchByDeeplink)
{
mLaunchByDeepLink = launchByDeeplink;
mCurrentCompassOffsetX = 0;
mCurrentCompassOffsetY = 0;
mBottomWidgetOffsetX = 0;
mBottomWidgetOffsetY = 0;
}
public void onStart()
{
nativeSetRenderingInitializationFinishedListener(mMapRenderingListener);
}
public void onStop()
{
nativeSetRenderingInitializationFinishedListener(null);
}
public void onPause(final Context context)
{
mUiThemeOnPause = Config.UiTheme.getCurrent();
// Pause/Resume can be called without surface creation/destroy.
if (mSurfaceAttached)
nativePauseSurfaceRendering();
}
public void onResume()
{
// Pause/Resume can be called without surface creation/destroy.
if (mSurfaceAttached)
nativeResumeSurfaceRendering();
}
public boolean isContextCreated()
{
return mSurfaceCreated;
}
public void onScroll(double distanceX, double distanceY)
{
Map.nativeOnScroll(distanceX, distanceY);
}
public static void zoomIn()
{
nativeScalePlus();
}
public static void zoomOut()
{
nativeScaleMinus();
}
public static void onScale(double factor, double focusX, double focusY, boolean isAnim)
{
nativeOnScale(factor, focusX, focusY, isAnim);
}
public static void onTouch(int actionType, MotionEvent event, int pointerIndex)
{
if (event.getPointerCount() == 1)
{
nativeOnTouch(actionType, event.getPointerId(0), event.getX(), event.getY(), Map.INVALID_TOUCH_ID, 0, 0, 0);
}
else
{
nativeOnTouch(actionType, event.getPointerId(0), event.getX(0), event.getY(0), event.getPointerId(1),
event.getX(1), event.getY(1), pointerIndex);
}
}
public static void onClick(float x, float y)
{
nativeOnTouch(NATIVE_ACTION_DOWN, 0, x, y, Map.INVALID_TOUCH_ID, 0, 0, 0);
nativeOnTouch(NATIVE_ACTION_UP, 0, x, y, Map.INVALID_TOUCH_ID, 0, 0, 0);
}
public static boolean isEngineCreated()
{
return nativeIsEngineCreated();
}
public static void executeMapApiRequest()
{
nativeExecuteMapApiRequest();
}
private void setupWidgets(final Context context, int width, int height)
{
mHeight = height;
mWidth = width;
nativeCleanWidgets();
updateBottomWidgetsOffset(context, mBottomWidgetOffsetX, mBottomWidgetOffsetY);
if (mDisplayType == DisplayType.Device)
{
nativeSetupWidget(WIDGET_SCALE_FPS_LABEL, Utils.dimen(context, R.dimen.margin_base),
Utils.dimen(context, R.dimen.margin_base) * 2, ANCHOR_LEFT_TOP);
updateCompassOffset(context, mCurrentCompassOffsetX, mCurrentCompassOffsetY, false);
}
else
{
nativeSetupWidget(WIDGET_SCALE_FPS_LABEL, (float) mWidth / 2 + Utils.dimen(context, R.dimen.margin_base) * 2,
Utils.dimen(context, R.dimen.margin_base), ANCHOR_LEFT_TOP);
updateCompassOffset(context, mWidth, mCurrentCompassOffsetY, true);
}
}
private void updateRulerOffset(final Context context, int offsetX, int offsetY)
{
nativeSetupWidget(WIDGET_RULER, Utils.dimen(context, R.dimen.margin_ruler) + offsetX,
mHeight - Utils.dimen(context, R.dimen.margin_ruler) - offsetY, ANCHOR_LEFT_BOTTOM);
if (mSurfaceCreated)
nativeApplyWidgets();
}
private void updateAttributionOffset(final Context context, int offsetX, int offsetY)
{
nativeSetupWidget(WIDGET_COPYRIGHT, Utils.dimen(context, R.dimen.margin_ruler) + offsetX,
mHeight - Utils.dimen(context, R.dimen.margin_ruler) - offsetY, ANCHOR_LEFT_BOTTOM);
if (mSurfaceCreated)
nativeApplyWidgets();
}
private boolean isThemeChangingProcess()
{
return mUiThemeOnPause != null && !mUiThemeOnPause.equals(Config.UiTheme.getCurrent());
}
// Engine
private static native boolean nativeCreateEngine(Surface surface, int density, boolean firstLaunch,
boolean isLaunchByDeepLink, int appVersionCode, boolean isCustomROM);
private static native boolean nativeIsEngineCreated();
private static native void nativeUpdateEngineDpi(int dpi);
private static native void nativeSetRenderingInitializationFinishedListener(@Nullable MapRenderingListener listener);
private static native void nativeExecuteMapApiRequest();
// Surface
private static native boolean nativeAttachSurface(Surface surface);
private static native void nativeDetachSurface(boolean destroySurface);
private static native void nativeSurfaceChanged(Surface surface, int w, int h);
private static native boolean nativeDestroySurfaceOnDetach();
private static native void nativePauseSurfaceRendering();
private static native void nativeResumeSurfaceRendering();
// Widgets
private static native void nativeApplyWidgets();
private static native void nativeCleanWidgets();
private static native void nativeUpdateMyPositionRoutingOffset(int offsetY);
private static native void nativeSetupWidget(int widget, float x, float y, int anchor);
private static native void nativeCompassUpdated(double north, boolean forceRedraw);
// Events
private static native void nativeScalePlus();
private static native void nativeScaleMinus();
private static native void nativeOnScroll(double distanceX, double distanceY);
private static native void nativeOnScale(double factor, double focusX, double focusY, boolean isAnim);
private static native void nativeOnTouch(int actionType, int id1, float x1, float y1, int id2, float x2, float y2,
int maskedPointer);
}

View File

@@ -0,0 +1,16 @@
package app.organicmaps.sdk;
import androidx.annotation.Keep;
public interface MapRenderingListener
{
default void onRenderingCreated() {}
default void onRenderingRestored() {}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
default void onRenderingInitializationFinished()
{}
}

View File

@@ -0,0 +1,59 @@
package app.organicmaps.sdk;
import androidx.annotation.NonNull;
public enum MapStyle
{
Clear(0),
Dark(1),
VehicleClear(3),
VehicleDark(4),
OutdoorsClear(5),
OutdoorsDark(6);
MapStyle(int value)
{
this.value = value;
}
@NonNull
public static MapStyle get()
{
return valueOf(nativeGet());
}
public static void set(@NonNull MapStyle mapStyle)
{
nativeSet(mapStyle.value);
}
/**
* This method allows to set new map style without immediate applying. It can be used before
* engine recreation instead of nativeSetMapStyle to avoid huge flow of OpenGL invocations.
*
* @param mapStyle style index
*/
public static void mark(@NonNull MapStyle mapStyle)
{
nativeMark(mapStyle.value);
}
@NonNull
public static MapStyle valueOf(int value)
{
for (MapStyle mapStyle : MapStyle.values())
{
if (mapStyle.value == value)
return mapStyle;
}
throw new IllegalArgumentException("Unknown map style value: " + value);
}
private final int value;
private static native void nativeSet(int mapStyle);
private static native int nativeGet();
private static native void nativeMark(int mapStyle);
}

View File

@@ -0,0 +1,230 @@
package app.organicmaps.sdk;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import app.organicmaps.R;
import app.organicmaps.sdk.bookmarks.data.BookmarkManager;
import app.organicmaps.sdk.bookmarks.data.Icon;
import app.organicmaps.sdk.downloader.Android7RootCertificateWorkaround;
import app.organicmaps.sdk.editor.OsmOAuth;
import app.organicmaps.sdk.location.LocationHelper;
import app.organicmaps.sdk.location.SensorHelper;
import app.organicmaps.sdk.maplayer.isolines.IsolinesManager;
import app.organicmaps.sdk.maplayer.subway.SubwayManager;
import app.organicmaps.sdk.maplayer.traffic.TrafficManager;
import app.organicmaps.sdk.routing.RoutingController;
import app.organicmaps.sdk.search.SearchEngine;
import app.organicmaps.sdk.settings.StoragePathManager;
import app.organicmaps.sdk.sound.TtsPlayer;
import app.organicmaps.sdk.util.Config;
import app.organicmaps.sdk.util.SharedPropertiesUtils;
import app.organicmaps.sdk.util.StorageUtils;
import app.organicmaps.sdk.util.log.Logger;
import app.organicmaps.sdk.util.log.LogsManager;
import java.io.IOException;
public final class OrganicMaps implements DefaultLifecycleObserver
{
private static final String TAG = OrganicMaps.class.getSimpleName();
@NonNull
private final Context mContext;
@NonNull
private final SharedPreferences mPreferences;
@NonNull
private final IsolinesManager mIsolinesManager;
@NonNull
private final SubwayManager mSubwayManager;
@NonNull
private final LocationHelper mLocationHelper;
@NonNull
private final SensorHelper mSensorHelper;
private volatile boolean mFrameworkInitialized;
private volatile boolean mPlatformInitialized;
@NonNull
public LocationHelper getLocationHelper()
{
return mLocationHelper;
}
@NonNull
public SensorHelper getSensorHelper()
{
return mSensorHelper;
}
@NonNull
public SubwayManager getSubwayManager()
{
return mSubwayManager;
}
@NonNull
public IsolinesManager getIsolinesManager()
{
return mIsolinesManager;
}
public OrganicMaps(@NonNull Context context)
{
mContext = context.getApplicationContext();
mPreferences = mContext.getSharedPreferences(context.getString(app.organicmaps.sdk.R.string.pref_file_name),
Context.MODE_PRIVATE);
// Set configuration directory as early as possible.
// Other methods may explicitly use Config, which requires settingsDir to be set.
final String settingsPath = StorageUtils.getSettingsPath(mContext);
if (!StorageUtils.createDirectory(settingsPath))
throw new AssertionError("Can't create settingsDir " + settingsPath);
Logger.d(TAG, "Settings path = " + settingsPath);
nativeSetSettingsDir(settingsPath);
Config.init(mContext, mPreferences);
OsmOAuth.init(mPreferences);
SharedPropertiesUtils.init(mPreferences);
LogsManager.INSTANCE.initFileLogging(mContext, mPreferences);
Android7RootCertificateWorkaround.initializeIfNeeded(mContext);
Icon.loadDefaultIcons(mContext.getResources(), mContext.getPackageName());
mSensorHelper = new SensorHelper(mContext);
mLocationHelper = new LocationHelper(mContext, mSensorHelper);
mIsolinesManager = new IsolinesManager();
mSubwayManager = new SubwayManager(mContext);
}
/**
* Initialize native core of application: platform and framework.
*
* @throws IOException - if failed to create directories. Caller must handle
* the exception and do nothing with native code if initialization is failed.
*/
public boolean init(@NonNull Runnable onComplete) throws IOException
{
initNativePlatform();
return initNativeFramework(onComplete);
}
public boolean arePlatformAndCoreInitialized()
{
return mFrameworkInitialized && mPlatformInitialized;
}
@Override
public void onStart(@NonNull LifecycleOwner owner)
{
nativeOnTransit(true);
}
@Override
public void onStop(@NonNull LifecycleOwner owner)
{
nativeOnTransit(false);
}
@NonNull
public SharedPreferences getPreferences()
{
return mPreferences;
}
private void initNativePlatform() throws IOException
{
if (mPlatformInitialized)
return;
final String apkPath = StorageUtils.getApkPath(mContext);
Logger.d(TAG, "Apk path = " + apkPath);
// Note: StoragePathManager uses Config, which requires SettingsDir to be set.
final String writablePath = StoragePathManager.findMapsStorage(mContext);
Logger.d(TAG, "Writable path = " + writablePath);
final String privatePath = StorageUtils.getPrivatePath(mContext);
Logger.d(TAG, "Private path = " + privatePath);
final String tempPath = StorageUtils.getTempPath(mContext);
Logger.d(TAG, "Temp path = " + tempPath);
// If platform directories are not created it means that native part of app will not be able
// to work at all. So, we just ignore native part initialization in this case, e.g. when the
// external storage is damaged or not available (read-only).
createPlatformDirectories(writablePath, privatePath, tempPath);
nativeInitPlatform(mContext, apkPath, writablePath, privatePath, tempPath, app.organicmaps.BuildConfig.FLAVOR,
app.organicmaps.BuildConfig.BUILD_TYPE,
/* isTablet */ false);
Config.setStoragePath(writablePath);
Config.setStatisticsEnabled(SharedPropertiesUtils.isStatisticsEnabled());
mPlatformInitialized = true;
Logger.i(TAG, "Platform initialized");
}
private boolean initNativeFramework(@NonNull Runnable onComplete)
{
if (mFrameworkInitialized)
return false;
nativeInitFramework(onComplete);
initNativeStrings();
SearchEngine.INSTANCE.initialize();
BookmarkManager.loadBookmarks();
TtsPlayer.INSTANCE.initialize(mContext);
RoutingController.get().initialize(mLocationHelper);
TrafficManager.INSTANCE.initialize();
mSubwayManager.initialize();
mIsolinesManager.initialize();
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
Logger.i(TAG, "Framework initialized");
mFrameworkInitialized = true;
return true;
}
private void createPlatformDirectories(@NonNull String writablePath, @NonNull String privatePath,
@NonNull String tempPath) throws IOException
{
SharedPropertiesUtils.emulateBadExternalStorage(mContext);
StorageUtils.requireDirectory(writablePath);
StorageUtils.requireDirectory(privatePath);
StorageUtils.requireDirectory(tempPath);
}
private void initNativeStrings()
{
nativeAddLocalization("core_entrance", mContext.getString(R.string.core_entrance));
nativeAddLocalization("core_exit", mContext.getString(R.string.core_exit));
nativeAddLocalization("core_my_places", mContext.getString(R.string.core_my_places));
nativeAddLocalization("core_my_position", mContext.getString(R.string.core_my_position));
nativeAddLocalization("core_placepage_unknown_place", mContext.getString(R.string.core_placepage_unknown_place));
nativeAddLocalization("postal_code", mContext.getString(R.string.postal_code));
nativeAddLocalization("wifi", mContext.getString(R.string.category_wifi));
}
private static native void nativeSetSettingsDir(String settingsPath);
private static native void nativeInitPlatform(Context context, String apkPath, String writablePath,
String privatePath, String tmpPath, String flavorName, String buildType,
boolean isTablet);
private static native void nativeInitFramework(@NonNull Runnable onComplete);
private static native void nativeAddLocalization(String name, String value);
private static native void nativeOnTransit(boolean foreground);
static
{
System.loadLibrary("organicmaps");
}
}

View File

@@ -0,0 +1,23 @@
package app.organicmaps.sdk;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.widget.placepage.PlacePageData;
public interface PlacePageActivationListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
void onPlacePageActivated(@NonNull PlacePageData data);
// Called from JNI
@Keep
@SuppressWarnings("unused")
void onPlacePageDeactivated();
// Called from JNI
@Keep
@SuppressWarnings("unused")
void onSwitchFullScreenMode();
}

View File

@@ -0,0 +1,52 @@
package app.organicmaps.sdk;
import androidx.annotation.NonNull;
public enum Router
{
Vehicle(0),
Pedestrian(1),
Bicycle(2),
Transit(3),
Ruler(4);
Router(int type)
{
this.type = type;
}
public static void set(@NonNull Router routerType)
{
nativeSet(routerType.type);
}
public static Router get()
{
return valueOf(nativeGet());
}
public static Router getLastUsed()
{
return valueOf(nativeGetLastUsed());
}
public static Router getBest(double srcLat, double srcLon, double dstLat, double dstLon)
{
return Router.values()[nativeGetBest(srcLat, srcLon, dstLat, dstLon)];
}
public static Router valueOf(int type)
{
return Router.values()[type];
}
private final int type;
private static native void nativeSet(int routerType);
private static native int nativeGet();
private static native int nativeGetLastUsed();
private static native int nativeGetBest(double srcLat, double srcLon, double dstLat, double dstLon);
}

View File

@@ -0,0 +1,22 @@
package app.organicmaps.sdk.api;
import androidx.annotation.Keep;
import app.organicmaps.sdk.Router;
/**
* Represents Framework::ParsedRoutingData from core.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class ParsedRoutingData
{
public final RoutePoint[] mPoints;
public final Router mRouterType;
public ParsedRoutingData(RoutePoint[] points, int routerType)
{
this.mPoints = points;
this.mRouterType = Router.valueOf(routerType);
}
}

View File

@@ -0,0 +1,32 @@
package app.organicmaps.sdk.api;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Represents url_scheme::SearchRequest from core.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public final class ParsedSearchRequest
{
@NonNull
public final String mQuery;
@Nullable
public final String mLocale;
public final double mLat;
public final double mLon;
public final boolean mIsSearchOnMap;
public ParsedSearchRequest(@NonNull String query, @Nullable String locale, double lat, double lon,
boolean isSearchOnMap)
{
this.mQuery = query;
this.mLocale = locale;
this.mLat = lat;
this.mLon = lon;
this.mIsSearchOnMap = isSearchOnMap;
}
}

View File

@@ -0,0 +1,21 @@
package app.organicmaps.sdk.api;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE)
@IntDef({RequestType.INCORRECT, RequestType.MAP, RequestType.ROUTE, RequestType.SEARCH, RequestType.CROSSHAIR,
RequestType.OAUTH2, RequestType.MENU, RequestType.SETTINGS})
public @interface RequestType
{
// Represents url_scheme::ParsedMapApi::UrlType from c++ part.
int INCORRECT = 0;
int MAP = 1;
int ROUTE = 2;
int SEARCH = 3;
int CROSSHAIR = 4;
int OAUTH2 = 5;
int MENU = 6;
int SETTINGS = 7;
}

View File

@@ -0,0 +1,23 @@
package app.organicmaps.sdk.api;
import androidx.annotation.Keep;
/**
* Represents url_scheme::RoutePoint from core.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class RoutePoint
{
public final double mLat;
public final double mLon;
public final String mName;
public RoutePoint(double lat, double lon, String name)
{
mLat = lat;
mLon = lon;
mName = name;
}
}

View File

@@ -0,0 +1,158 @@
package app.organicmaps.sdk.bookmarks.data;
import android.annotation.SuppressLint;
import android.os.Parcel;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.ParcelCompat;
import app.organicmaps.sdk.Framework;
import app.organicmaps.sdk.routing.RoutePointInfo;
import app.organicmaps.sdk.search.Popularity;
import app.organicmaps.sdk.util.Constants;
// TODO consider refactoring to remove hack with MapObject unmarshalling itself and Bookmark at the same time.
// Used by JNI.
@Keep
@SuppressWarnings("unused")
@SuppressLint("ParcelCreator")
public class Bookmark extends MapObject
{
private Icon mIcon;
private long mCategoryId;
private final long mBookmarkId;
private final double mMerX;
private final double mMerY;
public Bookmark(@NonNull FeatureId featureId, @IntRange(from = 0) long categoryId,
@IntRange(from = 0) long bookmarkId, String title, @Nullable String secondaryTitle,
@Nullable String subtitle, @Nullable String address, @Nullable RoutePointInfo routePointInfo,
@OpeningMode int openingMode, @NonNull Popularity popularity, @NonNull String description,
@Nullable String[] rawTypes)
{
super(featureId, BOOKMARK, title, secondaryTitle, subtitle, address, 0, 0, "", routePointInfo, openingMode,
popularity, description, RoadWarningMarkType.UNKNOWN.ordinal(), rawTypes);
mCategoryId = categoryId;
mBookmarkId = bookmarkId;
mIcon = getIconInternal();
final ParcelablePointD ll = BookmarkManager.INSTANCE.getBookmarkXY(mBookmarkId);
mMerX = ll.x;
mMerY = ll.y;
initXY();
}
private void initXY()
{
setLat(Math.toDegrees(2.0 * Math.atan(Math.exp(Math.toRadians(mMerY))) - Math.PI / 2.0));
setLon(mMerX);
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
super.writeToParcel(dest, flags);
dest.writeLong(mCategoryId);
dest.writeLong(mBookmarkId);
dest.writeParcelable(mIcon, flags);
dest.writeDouble(mMerX);
dest.writeDouble(mMerY);
}
// Do not use Core while restoring from Parcel! In some cases this constructor is called before
// the App is completely initialized.
// TODO: Method restoreHasCurrentPermission causes this strange behaviour, needs to be investigated.
protected Bookmark(@MapObjectType int type, Parcel source)
{
super(type, source);
mCategoryId = source.readLong();
mBookmarkId = source.readLong();
mIcon = ParcelCompat.readParcelable(source, Icon.class.getClassLoader(), Icon.class);
mMerX = source.readDouble();
mMerY = source.readDouble();
initXY();
}
@Override
public double getScale()
{
return BookmarkManager.INSTANCE.getBookmarkScale(mBookmarkId);
}
public DistanceAndAzimut getDistanceAndAzimuth(double cLat, double cLon, double north)
{
return Framework.nativeGetDistanceAndAzimuth(mMerX, mMerY, cLat, cLon, north);
}
private Icon getIconInternal()
{
return new Icon(BookmarkManager.INSTANCE.getBookmarkColor(mBookmarkId),
BookmarkManager.INSTANCE.getBookmarkIcon(mBookmarkId));
}
@Nullable
public Icon getIcon()
{
return mIcon;
}
public String getCategoryName()
{
return BookmarkManager.INSTANCE.getCategoryById(mCategoryId).getName();
}
public void setCategoryId(@IntRange(from = 0) long catId)
{
BookmarkManager.INSTANCE.notifyCategoryChanging(this, catId);
mCategoryId = catId;
}
public void setIconColor(@ColorInt int color)
{
Icon icon = new Icon(PredefinedColors.getPredefinedColorIndex(color),
BookmarkManager.INSTANCE.getBookmarkIcon(mBookmarkId));
BookmarkManager.INSTANCE.notifyParametersUpdating(this, getName(), icon, getBookmarkDescription());
mIcon = icon;
}
public void setParams(@NonNull String title, @Nullable Icon icon, @NonNull String description)
{
BookmarkManager.INSTANCE.notifyParametersUpdating(this, title, icon, description);
if (icon != null)
mIcon = icon;
setTitle(title);
setDescription(description);
}
public long getCategoryId()
{
return mCategoryId;
}
public long getBookmarkId()
{
return mBookmarkId;
}
@NonNull
public String getBookmarkDescription()
{
return BookmarkManager.INSTANCE.getBookmarkDescription(mBookmarkId);
}
@NonNull
public String getGe0Url(boolean addName)
{
return BookmarkManager.INSTANCE.encode2Ge0Url(mBookmarkId, addName);
}
@NonNull
public String getHttpGe0Url(boolean addName)
{
return getGe0Url(addName).replaceFirst(Constants.Url.SHORT_SHARE_PREFIX, Constants.Url.HTTP_SHARE_PREFIX);
}
}

View File

@@ -0,0 +1,15 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.NonNull;
import java.util.List;
public interface BookmarkCategoriesDataProvider
{
@NonNull
List<BookmarkCategory> getCategories();
int getCategoriesCount();
@NonNull
List<BookmarkCategory> getChildrenCategories(long parentId);
@NonNull
BookmarkCategory getCategoryById(long categoryId);
}

View File

@@ -0,0 +1,191 @@
package app.organicmaps.sdk.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.DrawableRes;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import app.organicmaps.sdk.R;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class BookmarkCategory implements Parcelable
{
private final long mId;
@NonNull
private final String mName;
@NonNull
private final String mAnnotation;
@NonNull
private final String mDescription;
private final int mTracksCount;
private final int mBookmarksCount;
private boolean mIsVisible;
public BookmarkCategory(long id, @NonNull String name, @NonNull String annotation, @NonNull String description,
int tracksCount, int bookmarksCount, boolean isVisible)
{
mId = id;
mName = name;
mAnnotation = annotation;
mDescription = description;
mTracksCount = tracksCount;
mBookmarksCount = bookmarksCount;
mIsVisible = isVisible;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
BookmarkCategory that = (BookmarkCategory) o;
return mId == that.mId;
}
@Override
public int hashCode()
{
return (int) (mId ^ (mId >>> 32));
}
public long getId()
{
return mId;
}
@NonNull
public String getName()
{
return mName;
}
public int getTracksCount()
{
return mTracksCount;
}
public int getBookmarksCount()
{
return mBookmarksCount;
}
public boolean isVisible()
{
return mIsVisible;
}
public void setVisible(boolean isVisible)
{
mIsVisible = isVisible;
}
public int size()
{
return getBookmarksCount() + getTracksCount();
}
@NonNull
public String getAnnotation()
{
return mAnnotation;
}
@NonNull
public String getDescription()
{
return mDescription;
}
@Override
public String toString()
{
final StringBuilder sb = new StringBuilder("BookmarkCategory{");
sb.append("mId=").append(mId);
sb.append(", mName='").append(mName).append('\'');
sb.append(", mAnnotation='").append(mAnnotation).append('\'');
sb.append(", mDescription='").append(mDescription).append('\'');
sb.append(", mTracksCount=").append(mTracksCount);
sb.append(", mBookmarksCount=").append(mBookmarksCount);
sb.append(", mIsVisible=").append(mIsVisible);
sb.append('}');
return sb.toString();
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeLong(this.mId);
dest.writeString(this.mName);
dest.writeString(this.mAnnotation);
dest.writeString(this.mDescription);
dest.writeInt(this.mTracksCount);
dest.writeInt(this.mBookmarksCount);
dest.writeByte(this.mIsVisible ? (byte) 1 : (byte) 0);
}
protected BookmarkCategory(Parcel in)
{
this.mId = in.readLong();
this.mName = in.readString();
this.mAnnotation = in.readString();
this.mDescription = in.readString();
this.mTracksCount = in.readInt();
this.mBookmarksCount = in.readInt();
this.mIsVisible = in.readByte() != 0;
}
public static final Creator<BookmarkCategory> CREATOR = new Creator<>() {
@Override
public BookmarkCategory createFromParcel(Parcel source)
{
return new BookmarkCategory(source);
}
@Override
public BookmarkCategory[] newArray(int size)
{
return new BookmarkCategory[size];
}
};
public enum AccessRules
{
ACCESS_RULES_LOCAL(app.organicmaps.R.string.not_shared, R.drawable.ic_lock),
ACCESS_RULES_PUBLIC(app.organicmaps.R.string.public_access, R.drawable.ic_public_inline),
ACCESS_RULES_DIRECT_LINK(app.organicmaps.R.string.limited_access, R.drawable.ic_link_inline),
ACCESS_RULES_AUTHOR_ONLY(app.organicmaps.R.string.access_rules_author_only, R.drawable.ic_lock);
private final int mResId;
private final int mDrawableResId;
AccessRules(int resId, int drawableResId)
{
mResId = resId;
mDrawableResId = drawableResId;
}
@DrawableRes
public int getDrawableResId()
{
return mDrawableResId;
}
@StringRes
public int getNameResId()
{
return mResId;
}
}
}

View File

@@ -0,0 +1,106 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.Framework;
import app.organicmaps.sdk.util.Distance;
import app.organicmaps.sdk.util.GeoUtils;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class BookmarkInfo
{
private final long mCategoryId;
private final long mBookmarkId;
@NonNull
private final String mTitle;
@NonNull
private final String mFeatureType;
@NonNull
private final Icon mIcon;
private final double mMerX;
private final double mMerY;
private final double mScale;
@NonNull
private final String mAddress;
@NonNull
private final ParcelablePointD mLatLonPoint;
public BookmarkInfo(@IntRange(from = 0) long categoryId, @IntRange(from = 0) long bookmarkId)
{
mCategoryId = categoryId;
mBookmarkId = bookmarkId;
mTitle = BookmarkManager.INSTANCE.getBookmarkName(mBookmarkId);
mFeatureType = BookmarkManager.INSTANCE.getBookmarkFeatureType(mBookmarkId);
mIcon = new Icon(BookmarkManager.INSTANCE.getBookmarkColor(mBookmarkId),
BookmarkManager.INSTANCE.getBookmarkIcon(mBookmarkId));
final ParcelablePointD ll = BookmarkManager.INSTANCE.getBookmarkXY(mBookmarkId);
mMerX = ll.x;
mMerY = ll.y;
mScale = BookmarkManager.INSTANCE.getBookmarkScale(mBookmarkId);
mAddress = BookmarkManager.INSTANCE.getBookmarkAddress(mBookmarkId);
mLatLonPoint = GeoUtils.toLatLon(mMerX, mMerY);
}
public long getCategoryId()
{
return mCategoryId;
}
public long getBookmarkId()
{
return mBookmarkId;
}
public DistanceAndAzimut getDistanceAndAzimuth(double cLat, double cLon, double north)
{
return Framework.nativeGetDistanceAndAzimuth(mMerX, mMerY, cLat, cLon, north);
}
@NonNull
public String getFeatureType()
{
return mFeatureType;
}
@NonNull
public String getName()
{
return mTitle;
}
@NonNull
public Icon getIcon()
{
return mIcon;
}
@NonNull
public Distance getDistance(double latitude, double longitude, double v)
{
return getDistanceAndAzimuth(latitude, longitude, v).getDistance();
}
public double getLat()
{
return mLatLonPoint.x;
}
public double getLon()
{
return mLatLonPoint.y;
}
public double getScale()
{
return mScale;
}
@NonNull
public String getAddress()
{
return mAddress;
}
}

View File

@@ -0,0 +1,997 @@
package app.organicmaps.sdk.bookmarks.data;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import app.organicmaps.sdk.Framework;
import app.organicmaps.sdk.util.KeyValue;
import app.organicmaps.sdk.util.StorageUtils;
import app.organicmaps.sdk.util.concurrency.UiThread;
import app.organicmaps.sdk.util.log.Logger;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@MainThread
public enum BookmarkManager {
INSTANCE;
@Retention(RetentionPolicy.SOURCE)
@IntDef({SORT_BY_TYPE, SORT_BY_DISTANCE, SORT_BY_TIME, SORT_BY_NAME})
public @interface SortingType
{}
public static final int SORT_BY_TYPE = 0;
public static final int SORT_BY_DISTANCE = 1;
public static final int SORT_BY_TIME = 2;
public static final int SORT_BY_NAME = 3;
// These values have to match the values of kml::CompilationType from kml/types.hpp
public static final int CATEGORY = 0;
private static final String[] BOOKMARKS_EXTENSIONS = Framework.nativeGetBookmarksFilesExts();
private static final String TAG = BookmarkManager.class.getSimpleName();
@NonNull
private final BookmarkCategoriesDataProvider mCategoriesCoreDataProvider = new CoreBookmarkCategoriesDataProvider();
@NonNull
private BookmarkCategoriesDataProvider mCurrentDataProvider = mCategoriesCoreDataProvider;
private final BookmarkCategoriesCache mBookmarkCategoriesCache = new BookmarkCategoriesCache();
@NonNull
private final List<BookmarksLoadingListener> mListeners = new ArrayList<>();
@NonNull
private final List<BookmarksSortingListener> mSortingListeners = new ArrayList<>();
@NonNull
private final List<BookmarksSharingListener> mSharingListeners = new ArrayList<>();
@Nullable
private OnElevationCurrentPositionChangedListener mOnElevationCurrentPositionChangedListener;
@Nullable
private OnElevationActivePointChangedListener mOnElevationActivePointChangedListener;
public void toggleCategoryVisibility(@NonNull BookmarkCategory category)
{
boolean isVisible = isVisible(category.getId());
setVisibility(category.getId(), !isVisible);
}
@Nullable
public Bookmark addNewBookmark(double lat, double lon)
{
return nativeAddBookmarkToLastEditedCategory(lat, lon);
}
public void addLoadingListener(@NonNull BookmarksLoadingListener listener)
{
mListeners.add(listener);
}
public void removeLoadingListener(@NonNull BookmarksLoadingListener listener)
{
mListeners.remove(listener);
}
public void addSortingListener(@NonNull BookmarksSortingListener listener)
{
mSortingListeners.add(listener);
}
public void removeSortingListener(@NonNull BookmarksSortingListener listener)
{
mSortingListeners.remove(listener);
}
public void addSharingListener(@NonNull BookmarksSharingListener listener)
{
mSharingListeners.add(listener);
}
public void removeSharingListener(@NonNull BookmarksSharingListener listener)
{
mSharingListeners.remove(listener);
}
public void setElevationActivePointChangedListener(@Nullable OnElevationActivePointChangedListener listener)
{
if (listener != null)
nativeSetElevationActiveChangedListener();
else
nativeRemoveElevationActiveChangedListener();
mOnElevationActivePointChangedListener = listener;
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksChanged()
{
updateCache();
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksLoadingStarted()
{
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksLoadingStarted();
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksLoadingFinished()
{
updateCache();
mCurrentDataProvider = new CacheBookmarkCategoriesDataProvider();
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksLoadingFinished();
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksSortingCompleted(@NonNull SortedBlock[] sortedBlocks, long timestamp)
{
for (BookmarksSortingListener listener : mSortingListeners)
listener.onBookmarksSortingCompleted(sortedBlocks, timestamp);
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksSortingCancelled(long timestamp)
{
for (BookmarksSortingListener listener : mSortingListeners)
listener.onBookmarksSortingCancelled(timestamp);
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onBookmarksFileLoaded(boolean success, @NonNull String fileName, boolean isTemporaryFile)
{
// Android could create temporary file with bookmarks in some cases (KML/KMZ file is a blob
// in the intent, so we have to create a temporary file on the disk). Here we can delete it.
if (isTemporaryFile)
{
File tmpFile = new File(fileName);
tmpFile.delete();
}
if (success)
{
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksFileImportSuccessful();
}
else
{
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksFileImportFailed();
}
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onPreparedFileForSharing(BookmarkSharingResult result)
{
for (BookmarksSharingListener listener : mSharingListeners)
listener.onPreparedFileForSharing(result);
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onElevationCurrentPositionChanged()
{
if (mOnElevationCurrentPositionChangedListener != null)
mOnElevationCurrentPositionChangedListener.onCurrentPositionChanged();
}
public void setElevationCurrentPositionChangedListener(@Nullable OnElevationCurrentPositionChangedListener listener)
{
if (listener != null)
nativeSetElevationCurrentPositionChangedListener();
else
nativeRemoveElevationCurrentPositionChangedListener();
mOnElevationCurrentPositionChangedListener = listener;
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
public void onElevationActivePointChanged()
{
if (mOnElevationActivePointChangedListener != null)
mOnElevationActivePointChangedListener.onElevationActivePointChanged();
}
public boolean isVisible(long catId)
{
return nativeIsVisible(catId);
}
public void setVisibility(long catId, boolean visible)
{
nativeSetVisibility(catId, visible);
}
public void setCategoryName(long catId, @NonNull String name)
{
nativeSetCategoryName(catId, name);
}
public void setCategoryDescription(long id, @NonNull String categoryDesc)
{
nativeSetCategoryDescription(id, categoryDesc);
}
@Nullable
public Bookmark updateBookmarkPlacePage(long bmkId)
{
return nativeUpdateBookmarkPlacePage(bmkId);
}
@Nullable
public void updateTrackPlacePage()
{
nativeUpdateTrackPlacePage();
}
@Nullable
public BookmarkInfo getBookmarkInfo(long bmkId)
{
return nativeGetBookmarkInfo(bmkId);
}
public long getBookmarkIdByPosition(long catId, int positionInCategory)
{
return nativeGetBookmarkIdByPosition(catId, positionInCategory);
}
@NonNull
public Track getTrack(long trackId)
{
return nativeGetTrack(trackId, Track.class);
}
public long getTrackIdByPosition(long catId, int positionInCategory)
{
return nativeGetTrackIdByPosition(catId, positionInCategory);
}
public static void loadBookmarks()
{
nativeLoadBookmarks();
}
public void deleteCategory(long catId)
{
nativeDeleteCategory(catId);
}
public void deleteTrack(long trackId)
{
nativeDeleteTrack(trackId);
}
public void deleteBookmark(long bmkId)
{
nativeDeleteBookmark(bmkId);
}
public long createCategory(@NonNull String name)
{
return nativeCreateCategory(name);
}
public void showBookmarkOnMap(long bmkId)
{
nativeShowBookmarkOnMap(bmkId);
}
public void showBookmarkCategoryOnMap(long catId)
{
nativeShowBookmarkCategoryOnMap(catId);
}
@PredefinedColors.Color
public int getLastEditedColor()
{
return nativeGetLastEditedColor();
}
@MainThread
public void loadBookmarksFile(@NonNull String path, boolean isTemporaryFile)
{
Logger.d(TAG, "Loading bookmarks file from: " + path);
nativeLoadBookmarksFile(path, isTemporaryFile);
}
static @Nullable String getBookmarksFilenameFromUri(@NonNull ContentResolver resolver, @NonNull Uri uri)
{
String filename = null;
final String scheme = uri.getScheme();
if (scheme.equals("content"))
{
try (Cursor cursor = resolver.query(uri, null, null, null, null))
{
if (cursor != null && cursor.moveToFirst())
{
final int columnIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (columnIndex >= 0)
filename = cursor.getString(columnIndex);
}
}
}
if (filename == null)
{
filename = uri.getPath();
if (filename == null)
return null;
final int cut = filename.lastIndexOf('/');
if (cut != -1)
filename = filename.substring(cut + 1);
}
// See IsBadCharForPath()
filename = filename.replaceAll("[:/\\\\<>\"|?*]", "");
final String lowerCaseFilename = filename.toLowerCase(java.util.Locale.ROOT);
// Check that filename contains bookmarks extension.
for (String ext : BOOKMARKS_EXTENSIONS)
{
if (lowerCaseFilename.endsWith(ext))
return filename;
}
// Samsung browser adds .xml extension to downloaded gpx files.
// Duplicate files have " (1).xml", " (2).xml" suffixes added.
final String gpxExt = ".gpx";
final int gpxStart = lowerCaseFilename.lastIndexOf(gpxExt);
if (gpxStart != -1)
return filename.substring(0, gpxStart + gpxExt.length());
// Try get guess extension from the mime type.
final String mime = resolver.getType(uri);
if (mime != null)
{
final int i = mime.lastIndexOf('.');
if (i != -1)
{
final String type = mime.substring(i + 1);
if (type.equalsIgnoreCase("kmz"))
return filename + ".kmz";
else if (type.equalsIgnoreCase("kml+xml"))
return filename + ".kml";
}
if (mime.endsWith("gpx+xml") || mime.endsWith("gpx")) // match application/gpx, application/gpx+xml
return filename + ".gpx";
}
// WhatsApp doesn't provide correct mime type and extension for GPX files.
if (uri.getHost().contains("com.whatsapp.provider.media"))
return filename + ".gpx";
return null;
}
@WorkerThread
public boolean importBookmarksFile(@NonNull ContentResolver resolver, @NonNull Uri uri, @NonNull File tempDir)
{
Logger.w(TAG, "Importing bookmarks from " + uri);
try
{
String filename = getBookmarksFilenameFromUri(resolver, uri);
if (filename == null)
{
Logger.w(TAG, "Could not find a supported file type in " + uri);
UiThread.run(() -> {
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksFileUnsupported(uri);
});
return false;
}
Logger.d(TAG, "Downloading bookmarks file from " + uri + " into " + filename);
final File tempFile = new File(tempDir, filename);
StorageUtils.copyFile(resolver, uri, tempFile);
Logger.d(TAG, "Downloaded bookmarks file from " + uri + " into " + filename);
UiThread.run(() -> loadBookmarksFile(tempFile.getAbsolutePath(), true));
return true;
}
catch (IOException | SecurityException e)
{
Logger.e(TAG, "Could not download bookmarks file from " + uri, e);
UiThread.run(() -> {
for (BookmarksLoadingListener listener : mListeners)
listener.onBookmarksFileDownloadFailed(uri, e.toString());
});
return false;
}
}
@WorkerThread
public void importBookmarksFiles(@NonNull ContentResolver resolver, @NonNull List<Uri> uris, @NonNull File tempDir)
{
for (Uri uri : uris)
importBookmarksFile(resolver, uri, tempDir);
}
public boolean isAsyncBookmarksLoadingInProgress()
{
return nativeIsAsyncBookmarksLoadingInProgress();
}
@NonNull
public List<BookmarkCategory> getCategories()
{
return mCurrentDataProvider.getCategories();
}
public int getCategoriesCount()
{
return mCurrentDataProvider.getCategoriesCount();
}
@NonNull
BookmarkCategoriesCache getBookmarkCategoriesCache()
{
return mBookmarkCategoriesCache;
}
private void updateCache()
{
getBookmarkCategoriesCache().update(mCategoriesCoreDataProvider.getCategories());
}
public void addCategoriesUpdatesListener(@NonNull DataChangedListener listener)
{
getBookmarkCategoriesCache().registerListener(listener);
}
public void removeCategoriesUpdatesListener(@NonNull DataChangedListener listener)
{
getBookmarkCategoriesCache().unregisterListener(listener);
}
@NonNull
public BookmarkCategory getCategoryById(long categoryId)
{
return mCurrentDataProvider.getCategoryById(categoryId);
}
public boolean isUsedCategoryName(@NonNull String name)
{
return nativeIsUsedCategoryName(name);
}
public void prepareForSearch(long catId)
{
nativePrepareForSearch(catId);
}
public boolean areAllCategoriesVisible()
{
return nativeAreAllCategoriesVisible();
}
public boolean areAllCategoriesInvisible()
{
return nativeAreAllCategoriesInvisible();
}
public void setAllCategoriesVisibility(boolean visible)
{
nativeSetAllCategoriesVisibility(visible);
}
public void setChildCategoriesVisibility(long catId, boolean visible)
{
nativeSetChildCategoriesVisibility(catId, visible);
}
public void prepareCategoriesForSharing(long[] catIds, KmlFileType kmlFileType)
{
nativePrepareFileForSharing(catIds, kmlFileType.ordinal());
}
public void prepareTrackForSharing(long trackId, KmlFileType kmlFileType)
{
nativePrepareTrackFileForSharing(trackId, kmlFileType.ordinal());
}
public void setNotificationsEnabled(boolean enabled)
{
nativeSetNotificationsEnabled(enabled);
}
public boolean hasLastSortingType(long catId)
{
return nativeHasLastSortingType(catId);
}
@SortingType
public int getLastSortingType(long catId)
{
return nativeGetLastSortingType(catId);
}
public void setLastSortingType(long catId, @SortingType int sortingType)
{
nativeSetLastSortingType(catId, sortingType);
}
public void resetLastSortingType(long catId)
{
nativeResetLastSortingType(catId);
}
@NonNull
@SortingType
public int[] getAvailableSortingTypes(long catId, boolean hasMyPosition)
{
return nativeGetAvailableSortingTypes(catId, hasMyPosition);
}
public void getSortedCategory(long catId, @SortingType int sortingType, boolean hasMyPosition, double lat, double lon,
long timestamp)
{
nativeGetSortedCategory(catId, sortingType, hasMyPosition, lat, lon, timestamp);
}
@NonNull
public List<BookmarkCategory> getChildrenCategories(long catId)
{
return mCurrentDataProvider.getChildrenCategories(catId);
}
@NonNull
native BookmarkCategory nativeGetBookmarkCategory(long catId);
@NonNull
native BookmarkCategory[] nativeGetBookmarkCategories();
native int nativeGetBookmarkCategoriesCount();
@NonNull
native BookmarkCategory[] nativeGetChildrenCategories(long catId);
@NonNull
public String getBookmarkName(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkName(bookmarkId);
}
@NonNull
public String getBookmarkFeatureType(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkFeatureType(bookmarkId);
}
@NonNull
public ParcelablePointD getBookmarkXY(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkXY(bookmarkId);
}
@PredefinedColors.Color
public int getBookmarkColor(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkColor(bookmarkId);
}
public int getBookmarkIcon(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkIcon(bookmarkId);
}
@NonNull
public String getBookmarkDescription(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkDescription(bookmarkId);
}
public String getTrackDescription(@IntRange(from = 0) long trackId)
{
return nativeGetTrackDescription(trackId);
}
public double getBookmarkScale(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkScale(bookmarkId);
}
@NonNull
public String encode2Ge0Url(@IntRange(from = 0) long bookmarkId, boolean addName)
{
return nativeEncode2Ge0Url(bookmarkId, addName);
}
public void setBookmarkParams(@IntRange(from = 0) long bookmarkId, @NonNull String name,
@PredefinedColors.Color int color, @NonNull String descr)
{
nativeSetBookmarkParams(bookmarkId, name, color, descr);
}
public void setTrackParams(@IntRange(from = 0) long trackId, @NonNull String name, int color, @NonNull String descr)
{
nativeSetTrackParams(trackId, name, color, descr);
}
public void changeTrackColor(@IntRange(from = 0) long trackId, int color)
{
nativeChangeTrackColor(trackId, color);
}
public void changeBookmarkCategory(@IntRange(from = 0) long oldCatId, @IntRange(from = 0) long newCatId,
@IntRange(from = 0) long bookmarkId)
{
nativeChangeBookmarkCategory(oldCatId, newCatId, bookmarkId);
}
public void changeTrackCategory(@IntRange(from = 0) long oldCatId, @IntRange(from = 0) long newCatId,
@IntRange(from = 0) long trackId)
{
nativeChangeTrackCategory(oldCatId, newCatId, trackId);
}
@NonNull
public String getBookmarkAddress(@IntRange(from = 0) long bookmarkId)
{
return nativeGetBookmarkAddress(bookmarkId);
}
public void notifyCategoryChanging(@NonNull BookmarkInfo bookmarkInfo, @IntRange(from = 0) long catId)
{
if (catId == bookmarkInfo.getCategoryId())
return;
changeBookmarkCategory(bookmarkInfo.getCategoryId(), catId, bookmarkInfo.getBookmarkId());
}
public void notifyCategoryChanging(@NonNull Track track, @IntRange(from = 0) long catId)
{
if (catId == track.getCategoryId())
return;
changeTrackCategory(track.getCategoryId(), catId, track.getTrackId());
}
public void notifyCategoryChanging(@NonNull Bookmark bookmark, @IntRange(from = 0) long catId)
{
if (catId == bookmark.getCategoryId())
return;
changeBookmarkCategory(bookmark.getCategoryId(), catId, bookmark.getBookmarkId());
}
public void notifyParametersUpdating(@NonNull BookmarkInfo bookmarkInfo, @NonNull String name, @Nullable Icon icon,
@NonNull String description)
{
if (icon == null)
icon = bookmarkInfo.getIcon();
if (!name.equals(bookmarkInfo.getName()) || !icon.equals(bookmarkInfo.getIcon())
|| !description.equals(getBookmarkDescription(bookmarkInfo.getBookmarkId())))
{
setBookmarkParams(bookmarkInfo.getBookmarkId(), name, icon.getColor(), description);
}
}
public void notifyParametersUpdating(@NonNull Bookmark bookmark, @NonNull String name, @Nullable Icon icon,
@NonNull String description)
{
if (icon == null)
icon = bookmark.getIcon();
if (!name.equals(bookmark.getName()) || !icon.equals(bookmark.getIcon())
|| !description.equals(getBookmarkDescription(bookmark.getBookmarkId())))
{
setBookmarkParams(bookmark.getBookmarkId(), name, icon != null ? icon.getColor() : getLastEditedColor(),
description);
}
}
public void notifyParametersUpdating(@NonNull Track track, @NonNull String name, @Nullable int color,
@NonNull String description)
{
if (!name.equals(track.getName()) || !(color == track.getColor())
|| !description.equals(getTrackDescription(track.getTrackId())))
{
setTrackParams(track.getTrackId(), name, color, description);
}
}
public double getElevationCurPositionDistance(long trackId)
{
return nativeGetElevationCurPositionDistance(trackId);
}
public void setElevationActivePoint(long trackId, double distance, ElevationInfo.Point point)
{
nativeSetElevationActivePoint(trackId, distance, point.getLatitude(), point.getLongitude());
}
public double getElevationActivePointDistance(long trackId)
{
return nativeGetElevationActivePointDistance(trackId);
}
private static native ElevationInfo.Point nativeGetElevationActivePointCoordinates(long trackId);
@Nullable
private native Bookmark nativeUpdateBookmarkPlacePage(long bmkId);
@Nullable
private native void nativeUpdateTrackPlacePage();
@Nullable
private native BookmarkInfo nativeGetBookmarkInfo(long bmkId);
private native long nativeGetBookmarkIdByPosition(long catId, int position);
@NonNull
private native Track nativeGetTrack(long trackId, Class<Track> trackClazz);
private native long nativeGetTrackIdByPosition(long catId, int position);
private native boolean nativeIsVisible(long catId);
private native void nativeSetVisibility(long catId, boolean visible);
private native void nativeSetCategoryName(long catId, @NonNull String n);
private native void nativeSetCategoryDescription(long catId, @NonNull String desc);
private native void nativeSetCategoryTags(long catId, @NonNull String[] tagsIds);
private native void nativeSetCategoryAccessRules(long catId, int accessRules);
private native void nativeSetCategoryCustomProperty(long catId, String key, String value);
private static native void nativeLoadBookmarks();
private native boolean nativeDeleteCategory(long catId);
private native void nativeDeleteTrack(long trackId);
private native void nativeDeleteBookmark(long bmkId);
/**
* @return category Id
*/
private native long nativeCreateCategory(@NonNull String name);
private native void nativeShowBookmarkOnMap(long bmkId);
private native void nativeShowBookmarkCategoryOnMap(long catId);
@Nullable
private native Bookmark nativeAddBookmarkToLastEditedCategory(double lat, double lon);
@PredefinedColors.Color
private native int nativeGetLastEditedColor();
private static native void nativeLoadBookmarksFile(@NonNull String path, boolean isTemporaryFile);
private static native boolean nativeIsAsyncBookmarksLoadingInProgress();
private static native boolean nativeIsUsedCategoryName(@NonNull String name);
private static native void nativePrepareForSearch(long catId);
private static native boolean nativeAreAllCategoriesVisible();
private static native boolean nativeAreAllCategoriesInvisible();
private static native void nativeSetChildCategoriesVisibility(long catId, boolean visible);
private static native void nativeSetAllCategoriesVisibility(boolean visible);
private static native void nativePrepareFileForSharing(long[] catIds, int kmlFileType);
private static native void nativePrepareTrackFileForSharing(long trackId, int kmlFileType);
private static native boolean nativeIsCategoryEmpty(long catId);
private static native void nativeSetNotificationsEnabled(boolean enabled);
@NonNull
private static native String nativeGetCatalogDeeplink(long catId);
@NonNull
private static native String nativeGetCatalogPublicLink(long catId);
@NonNull
private static native String nativeGetWebEditorUrl(@NonNull String serverId);
@NonNull
private static native KeyValue[] nativeGetCatalogHeaders();
private static native void nativeRequestCatalogCustomProperties();
private native boolean nativeHasLastSortingType(long catId);
@SortingType
private native int nativeGetLastSortingType(long catId);
private native void nativeSetLastSortingType(long catId, @SortingType int sortingType);
private native void nativeResetLastSortingType(long catId);
@NonNull
@SortingType
private native int[] nativeGetAvailableSortingTypes(long catId, boolean hasMyPosition);
private native void nativeGetSortedCategory(long catId, @SortingType int sortingType, boolean hasMyPosition,
double lat, double lon, long timestamp);
@NonNull
private static native String nativeGetBookmarkName(@IntRange(from = 0) long bookmarkId);
@NonNull
private static native String nativeGetBookmarkFeatureType(@IntRange(from = 0) long bookmarkId);
@NonNull
private static native ParcelablePointD nativeGetBookmarkXY(@IntRange(from = 0) long bookmarkId);
@PredefinedColors.Color
private static native int nativeGetBookmarkColor(@IntRange(from = 0) long bookmarkId);
private static native int nativeGetBookmarkIcon(@IntRange(from = 0) long bookmarkId);
@NonNull
private static native String nativeGetBookmarkDescription(@IntRange(from = 0) long bookmarkId);
private static native String nativeGetTrackDescription(@IntRange(from = 0) long trackId);
private static native double nativeGetBookmarkScale(@IntRange(from = 0) long bookmarkId);
@NonNull
private static native String nativeEncode2Ge0Url(@IntRange(from = 0) long bookmarkId, boolean addName);
private static native void nativeSetBookmarkParams(@IntRange(from = 0) long bookmarkId, @NonNull String name,
@PredefinedColors.Color int color, @NonNull String descr);
private static native void nativeChangeTrackColor(@IntRange(from = 0) long trackId,
@PredefinedColors.Color int color);
private static native void nativeSetTrackParams(@IntRange(from = 0) long trackId, @NonNull String name,
@PredefinedColors.Color int color, @NonNull String descr);
private static native void nativeChangeBookmarkCategory(@IntRange(from = 0) long oldCatId,
@IntRange(from = 0) long newCatId,
@IntRange(from = 0) long bookmarkId);
private static native void nativeChangeTrackCategory(@IntRange(from = 0) long oldCatId,
@IntRange(from = 0) long newCatId,
@IntRange(from = 0) long trackId);
@NonNull
private static native String nativeGetBookmarkAddress(@IntRange(from = 0) long bookmarkId);
private static native double nativeGetElevationCurPositionDistance(long trackId);
private static native void nativeSetElevationCurrentPositionChangedListener();
public static native void nativeRemoveElevationCurrentPositionChangedListener();
private static native void nativeSetElevationActivePoint(long trackId, double distanceInMeters, double latitude,
double longitude);
private static native double nativeGetElevationActivePointDistance(long trackId);
public ElevationInfo.Point getElevationActivePointCoordinates(long trackId)
{
return nativeGetElevationActivePointCoordinates(trackId);
}
private static native void nativeSetElevationActiveChangedListener();
public static native void nativeRemoveElevationActiveChangedListener();
public static native ElevationInfo nativeGetTrackElevationInfo(long trackId);
public static native TrackStatistics nativeGetTrackStatistics(long trackId);
public interface BookmarksLoadingListener
{
default void onBookmarksLoadingStarted() {}
default void onBookmarksLoadingFinished() {}
default void onBookmarksFileUnsupported(@NonNull Uri uri) {}
default void onBookmarksFileDownloadFailed(@NonNull Uri uri, @NonNull String string) {}
default void onBookmarksFileImportSuccessful() {}
default void onBookmarksFileImportFailed() {}
}
public interface BookmarksSortingListener
{
void onBookmarksSortingCompleted(@NonNull SortedBlock[] sortedBlocks, long timestamp);
default void onBookmarksSortingCancelled(long timestamp) {}
}
public interface BookmarksSharingListener
{
void onPreparedFileForSharing(@NonNull BookmarkSharingResult result);
}
public interface OnElevationActivePointChangedListener
{
void onElevationActivePointChanged();
}
public interface OnElevationCurrentPositionChangedListener
{
void onCurrentPositionChanged();
}
static class BookmarkCategoriesCache
{
@NonNull
private final List<BookmarkCategory> mCategories = new ArrayList<>();
@NonNull
private final List<DataChangedListener> mListeners = new ArrayList<>();
void update(@NonNull List<BookmarkCategory> categories)
{
mCategories.clear();
mCategories.addAll(categories);
notifyChanged();
}
@NonNull
public List<BookmarkCategory> getCategories()
{
return Collections.unmodifiableList(mCategories);
}
public void registerListener(@NonNull DataChangedListener listener)
{
if (mListeners.contains(listener))
throw new IllegalStateException("Observer " + listener + " is already registered.");
mListeners.add(listener);
}
public void unregisterListener(@NonNull DataChangedListener listener)
{
int index = mListeners.indexOf(listener);
if (index == -1)
throw new IllegalStateException("Observer " + listener + " was not registered.");
mListeners.remove(index);
}
protected void notifyChanged()
{
for (DataChangedListener item : mListeners)
item.onChanged();
}
}
}

View File

@@ -0,0 +1,72 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class BookmarkSharingResult
{
@Retention(RetentionPolicy.SOURCE)
@IntDef({SUCCESS, EMPTY_CATEGORY, ARCHIVE_ERROR, FILE_ERROR})
public @interface Code
{}
public static final int SUCCESS = 0;
public static final int EMPTY_CATEGORY = 1;
public static final int ARCHIVE_ERROR = 2;
public static final int FILE_ERROR = 3;
private final long[] mCategoriesIds;
@Code
private final int mCode;
@NonNull
private final String mSharingPath;
@NonNull
@SuppressWarnings("unused")
private final String mErrorString;
@NonNull
@SuppressWarnings("unused")
private final String mMimeType;
public BookmarkSharingResult(long[] categoriesIds, @Code int code, @NonNull String sharingPath,
@NonNull String mimeType, @NonNull String errorString)
{
mCategoriesIds = categoriesIds;
mCode = code;
mSharingPath = sharingPath;
mErrorString = errorString;
mMimeType = mimeType;
}
public long[] getCategoriesIds()
{
return mCategoriesIds;
}
public int getCode()
{
return mCode;
}
@NonNull
public String getSharingPath()
{
return mSharingPath;
}
@NonNull
public String getMimeType()
{
return mMimeType;
}
@NonNull
public String getErrorString()
{
return mErrorString;
}
}

View File

@@ -0,0 +1,43 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
class CacheBookmarkCategoriesDataProvider implements BookmarkCategoriesDataProvider
{
@NonNull
@Override
public BookmarkCategory getCategoryById(long categoryId)
{
BookmarkManager.BookmarkCategoriesCache cache = BookmarkManager.INSTANCE.getBookmarkCategoriesCache();
List<BookmarkCategory> categories = cache.getCategories();
for (BookmarkCategory category : categories)
if (category.getId() == categoryId)
return category;
return BookmarkManager.INSTANCE.nativeGetBookmarkCategory(categoryId);
}
@NonNull
@Override
public List<BookmarkCategory> getCategories()
{
return BookmarkManager.INSTANCE.getBookmarkCategoriesCache().getCategories();
}
@Override
public int getCategoriesCount()
{
return BookmarkManager.INSTANCE.nativeGetBookmarkCategoriesCount();
}
@NonNull
@Override
public List<BookmarkCategory> getChildrenCategories(long parentId)
{
BookmarkCategory[] categories = BookmarkManager.INSTANCE.nativeGetChildrenCategories(parentId);
return Arrays.asList(categories);
}
}

View File

@@ -0,0 +1,40 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import app.organicmaps.sdk.content.DataSource;
import java.util.List;
public class CategoryDataSource extends RecyclerView.AdapterDataObserver implements DataSource<BookmarkCategory>
{
@NonNull
private BookmarkCategory mCategory;
public CategoryDataSource(@NonNull BookmarkCategory category)
{
mCategory = category;
}
@NonNull
@Override
public BookmarkCategory getData()
{
return mCategory;
}
@Override
public void onChanged()
{
super.onChanged();
List<BookmarkCategory> categories = BookmarkManager.INSTANCE.getCategories();
int index = categories.indexOf(mCategory);
if (index >= 0)
mCategory = categories.get(index);
}
@Override
public void invalidate()
{
onChanged();
}
}

View File

@@ -0,0 +1,37 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
class CoreBookmarkCategoriesDataProvider implements BookmarkCategoriesDataProvider
{
@NonNull
@Override
public BookmarkCategory getCategoryById(long categoryId)
{
return BookmarkManager.INSTANCE.nativeGetBookmarkCategory(categoryId);
}
@NonNull
@Override
public List<BookmarkCategory> getCategories()
{
BookmarkCategory[] categories = BookmarkManager.INSTANCE.nativeGetBookmarkCategories();
return Arrays.asList(categories);
}
@Override
public int getCategoriesCount()
{
return BookmarkManager.INSTANCE.nativeGetBookmarkCategoriesCount();
}
@NonNull
@Override
public List<BookmarkCategory> getChildrenCategories(long parentId)
{
BookmarkCategory[] categories = BookmarkManager.INSTANCE.nativeGetChildrenCategories(parentId);
return Arrays.asList(categories);
}
}

View File

@@ -0,0 +1,6 @@
package app.organicmaps.sdk.bookmarks.data;
public interface DataChangedListener
{
void onChanged();
}

View File

@@ -0,0 +1,29 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.Keep;
import app.organicmaps.sdk.util.Distance;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class DistanceAndAzimut
{
private final Distance mDistance;
private final double mAzimuth;
public Distance getDistance()
{
return mDistance;
}
public double getAzimuth()
{
return mAzimuth;
}
public DistanceAndAzimut(Distance distance, double azimuth)
{
mDistance = distance;
mAzimuth = azimuth;
}
}

View File

@@ -0,0 +1,155 @@
package app.organicmaps.sdk.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.widget.placepage.PlacePageData;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class ElevationInfo implements PlacePageData
{
@NonNull
private final List<Point> mPoints;
private final int mDifficulty;
public ElevationInfo(@NonNull Point[] points, int difficulty)
{
mPoints = Arrays.asList(points);
mDifficulty = difficulty;
}
protected ElevationInfo(Parcel in)
{
mDifficulty = in.readInt();
mPoints = readPoints(in);
}
@NonNull
private static List<Point> readPoints(@NonNull Parcel in)
{
List<Point> points = new ArrayList<>();
in.readTypedList(points, Point.CREATOR);
return points;
}
@NonNull
public List<Point> getPoints()
{
return Collections.unmodifiableList(mPoints);
}
public int getDifficulty()
{
return mDifficulty;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeInt(mDifficulty);
// All collections are deserialized AFTER non-collection and primitive type objects,
// so collections must be always serialized at the end.
dest.writeTypedList(mPoints);
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public static class Point implements Parcelable
{
private final double mDistance;
private final int mAltitude;
private final double mLatitude;
private final double mLongitude;
public Point(double distance, int altitude, double latitude, double longitude)
{
mDistance = distance;
mAltitude = altitude;
mLatitude = latitude;
mLongitude = longitude;
}
protected Point(Parcel in)
{
mDistance = in.readDouble();
mAltitude = in.readInt();
mLatitude = in.readDouble();
mLongitude = in.readDouble();
}
public static final Creator<Point> CREATOR = new Creator<>() {
@Override
public Point createFromParcel(Parcel in)
{
return new Point(in);
}
@Override
public Point[] newArray(int size)
{
return new Point[size];
}
};
public double getDistance()
{
return mDistance;
}
public int getAltitude()
{
return mAltitude;
}
public double getLatitude()
{
return mLatitude;
}
public double getLongitude()
{
return mLongitude;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeDouble(mDistance);
dest.writeInt(mAltitude);
}
}
public static final Creator<ElevationInfo> CREATOR = new Creator<>() {
@Override
public ElevationInfo createFromParcel(Parcel in)
{
return new ElevationInfo(in);
}
@Override
public ElevationInfo[] newArray(int size)
{
return new ElevationInfo[size];
}
};
}

View File

@@ -0,0 +1,64 @@
package app.organicmaps.sdk.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import java.net.HttpURLConnection;
public class Error implements Parcelable
{
private final int mHttpCode;
@Nullable
private final String mMessage;
public Error(int httpCode, @Nullable String message)
{
mHttpCode = httpCode;
mMessage = message;
}
public Error(@Nullable String message)
{
this(HttpURLConnection.HTTP_UNAVAILABLE, message);
}
protected Error(Parcel in)
{
mHttpCode = in.readInt();
mMessage = in.readString();
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeInt(mHttpCode);
dest.writeString(mMessage);
}
@Override
public String toString()
{
return "Error{"
+ "mHttpCode=" + mHttpCode + ", mMessage='" + mMessage + '\'' + '}';
}
public static final Creator<Error> CREATOR = new Creator<>() {
@Override
public Error createFromParcel(Parcel in)
{
return new Error(in);
}
@Override
public Error[] newArray(int size)
{
return new Error[size];
}
};
}

View File

@@ -0,0 +1,131 @@
package app.organicmaps.sdk.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
// Used by JNI.
@Keep
/// @todo Review using of this class, because seems like it has no any useful purpose.
/// Just creating in JNI and assigning ..
public class FeatureId implements Parcelable
{
public static final Creator<FeatureId> CREATOR = new Creator<>() {
@Override
public FeatureId createFromParcel(Parcel in)
{
return new FeatureId(in);
}
@Override
public FeatureId[] newArray(int size)
{
return new FeatureId[size];
}
};
@NonNull
public static final FeatureId EMPTY = new FeatureId("", -1L, 0);
@NonNull
private final String mMwmName;
private final long mMwmVersion;
private final int mFeatureIndex;
@NonNull
public static FeatureId fromFeatureIdString(@NonNull String id)
{
if (TextUtils.isEmpty(id))
throw new AssertionError("Feature id string is empty");
String[] parts = id.split(":");
if (parts.length != 3)
throw new AssertionError("Wrong feature id string format");
return new FeatureId(parts[1], Long.parseLong(parts[0]), Integer.parseInt(parts[2]));
}
public FeatureId(@NonNull String mwmName, long mwmVersion, int featureIndex)
{
mMwmName = mwmName;
mMwmVersion = mwmVersion;
mFeatureIndex = featureIndex;
}
private FeatureId(Parcel in)
{
mMwmName = in.readString();
mMwmVersion = in.readLong();
mFeatureIndex = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeString(mMwmName);
dest.writeLong(mMwmVersion);
dest.writeInt(mFeatureIndex);
}
@Override
public int describeContents()
{
return 0;
}
@NonNull
public String getMwmName()
{
return mMwmName;
}
public long getMwmVersion()
{
return mMwmVersion;
}
public int getFeatureIndex()
{
return mFeatureIndex;
}
public boolean isRealId()
{
return !TextUtils.isEmpty(mMwmName) && mMwmVersion >= 0 && mFeatureIndex > 0;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
FeatureId featureId = (FeatureId) o;
if (mMwmVersion != featureId.mMwmVersion)
return false;
if (mFeatureIndex != featureId.mFeatureIndex)
return false;
return mMwmName.equals(featureId.mMwmName);
}
@Override
public int hashCode()
{
int result = mMwmName.hashCode();
result = 31 * result + (int) (mMwmVersion ^ (mMwmVersion >>> 32));
result = 31 * result + mFeatureIndex;
return result;
}
@Override
public String toString()
{
return "FeatureId{"
+ "mMwmName='" + mMwmName + '\'' + ", mMwmVersion=" + mMwmVersion + ", mFeatureIndex=" + mFeatureIndex + '}';
}
}

View File

@@ -0,0 +1,128 @@
package app.organicmaps.sdk.bookmarks.data;
import android.content.res.Resources;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import app.organicmaps.BuildConfig;
import app.organicmaps.sdk.util.StringUtils;
import app.organicmaps.sdk.util.log.Logger;
import com.google.common.base.Objects;
import dalvik.annotation.optimization.FastNative;
public class Icon implements Parcelable
{
private static final String TAG = Icon.class.getSimpleName();
@DrawableRes
private static int[] sTypeIcons = null;
@PredefinedColors.Color
private final int mColor;
private final int mType;
public Icon(@PredefinedColors.Color int color, int type)
{
mColor = color;
mType = type;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeInt(mColor);
dest.writeInt(mType);
}
private Icon(Parcel in)
{
mColor = in.readInt();
mType = in.readInt();
}
@PredefinedColors.Color
public int getColor()
{
return mColor;
}
@ColorInt
public int argb()
{
return PredefinedColors.getColor(mColor);
}
@DrawableRes
public int getResId()
{
// loadDefaultIcons should be called
assert (sTypeIcons != null);
return sTypeIcons[mType];
}
public int getType()
{
return mType;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o instanceof Icon comparedIcon)
return mColor == comparedIcon.mColor && mType == comparedIcon.mType;
return false;
}
@Override
public int hashCode()
{
return Objects.hashCode(mColor, mType);
}
public static final Parcelable.Creator<Icon> CREATOR = new Parcelable.Creator<>() {
public Icon createFromParcel(Parcel in)
{
return new Icon(in);
}
public Icon[] newArray(int size)
{
return new Icon[size];
}
};
static public void loadDefaultIcons(@NonNull Resources resources, @NonNull String packageName)
{
final String[] names = nativeGetBookmarkIconNames();
int[] icons = new int[names.length];
for (int i = 0; i < names.length; i++)
{
final String name = StringUtils.toSnakeCase(names[i]);
icons[i] = resources.getIdentifier("ic_bookmark_" + name, "drawable", packageName);
if (icons[i] == 0)
{
Logger.e(TAG, "Error getting icon for " + name);
// Force devs to add an icon for each bookmark type.
if (BuildConfig.DEBUG)
throw new RuntimeException("Error getting icon for " + name);
icons[i] = app.organicmaps.sdk.R.drawable.ic_bookmark_none; // Fallback icon
}
}
sTypeIcons = icons;
}
@FastNative
@NonNull
private static native String[] nativeGetBookmarkIconNames();
}

View File

@@ -0,0 +1,8 @@
package app.organicmaps.sdk.bookmarks.data;
import com.google.android.material.imageview.ShapeableImageView;
public interface IconClickListener
{
void onItemClick(ShapeableImageView v, int position);
}

View File

@@ -0,0 +1,9 @@
package app.organicmaps.sdk.bookmarks.data;
// Need to be in sync with KmlFileType (map/bookmark_helpers.hpp)
public enum KmlFileType
{
Text,
Binary,
Gpx
}

View File

@@ -0,0 +1,414 @@
package app.organicmaps.sdk.bookmarks.data;
import android.net.Uri;
import android.os.Parcel;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.ParcelCompat;
import app.organicmaps.sdk.routing.RoutePointInfo;
import app.organicmaps.sdk.search.Popularity;
import app.organicmaps.sdk.widget.placepage.PlacePageData;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
// TODO(yunikkk): Refactor. Displayed information is different from edited information, and it's better to
// separate them. Simple getters from jni place_page::Info and osm::EditableFeature should be enough.
// Used from JNI.
@Keep
public class MapObject implements PlacePageData
{
@Retention(RetentionPolicy.SOURCE)
@IntDef({POI, API_POINT, BOOKMARK, MY_POSITION, SEARCH, TRACK})
public @interface MapObjectType
{}
public static final int POI = 0;
public static final int API_POINT = 1;
public static final int BOOKMARK = 2;
public static final int MY_POSITION = 3;
public static final int SEARCH = 4;
public static final int TRACK = 5;
@Retention(RetentionPolicy.SOURCE)
@IntDef({OPENING_MODE_PREVIEW, OPENING_MODE_PREVIEW_PLUS, OPENING_MODE_DETAILS, OPENING_MODE_FULL})
public @interface OpeningMode
{}
public static final int OPENING_MODE_PREVIEW = 0;
public static final int OPENING_MODE_PREVIEW_PLUS = 1;
public static final int OPENING_MODE_DETAILS = 2;
public static final int OPENING_MODE_FULL = 3;
private static final String kHttp = "http://";
private static final String kHttps = "https://";
@NonNull
private final FeatureId mFeatureId;
@MapObjectType
private final int mMapObjectType;
private String mTitle;
@Nullable
private final String mSecondaryTitle;
private final String mSubtitle;
private double mLat;
private double mLon;
private final String mAddress;
@NonNull
private final Metadata mMetadata;
private final String mApiId;
private final RoutePointInfo mRoutePointInfo;
@OpeningMode
private final int mOpeningMode;
// @NonNull
// private final Popularity mPopularity;
@NonNull
private final RoadWarningMarkType mRoadWarningMarkType;
@NonNull
private String mDescription;
@Nullable
private List<String> mRawTypes;
public MapObject(@NonNull FeatureId featureId, @MapObjectType int mapObjectType, String title,
@Nullable String secondaryTitle, String subtitle, String address, double lat, double lon,
String apiId, @Nullable RoutePointInfo routePointInfo, @OpeningMode int openingMode,
Popularity popularity, @NonNull String description, int roadWarningType, @Nullable String[] rawTypes)
{
this(featureId, mapObjectType, title, secondaryTitle, subtitle, address, lat, lon, new Metadata(), apiId,
routePointInfo, openingMode, popularity, description, roadWarningType, rawTypes);
}
public MapObject(@NonNull FeatureId featureId, @MapObjectType int mapObjectType, String title,
@Nullable String secondaryTitle, String subtitle, String address, double lat, double lon,
Metadata metadata, String apiId, @Nullable RoutePointInfo routePointInfo,
@OpeningMode int openingMode, Popularity popularity, @NonNull String description,
int roadWarningType, @Nullable String[] rawTypes)
{
mFeatureId = featureId;
mMapObjectType = mapObjectType;
mTitle = title;
mSecondaryTitle = secondaryTitle;
mSubtitle = subtitle;
mAddress = address;
mLat = lat;
mLon = lon;
mMetadata = metadata != null ? metadata : new Metadata();
mApiId = apiId;
mRoutePointInfo = routePointInfo;
mOpeningMode = openingMode;
// mPopularity = popularity;
mDescription = description;
mRoadWarningMarkType = RoadWarningMarkType.values()[roadWarningType];
if (rawTypes != null)
mRawTypes = new ArrayList<>(Arrays.asList(rawTypes));
}
protected MapObject(@MapObjectType int type, Parcel source)
{
this(Objects.requireNonNull(
ParcelCompat.readParcelable(source, FeatureId.class.getClassLoader(), FeatureId.class)), // FeatureId
type, // MapObjectType
source.readString(), // Title
source.readString(), // SecondaryTitle
source.readString(), // Subtitle
source.readString(), // Address
source.readDouble(), // Lat
source.readDouble(), // Lon
ParcelCompat.readParcelable(source, Metadata.class.getClassLoader(), Metadata.class),
source.readString(), // ApiId;
ParcelCompat.readParcelable(source, RoutePointInfo.class.getClassLoader(),
RoutePointInfo.class), // RoutePointInfo
source.readInt(), // mOpeningMode
Objects.requireNonNull(
ParcelCompat.readParcelable(source, Popularity.class.getClassLoader(), Popularity.class)),
Objects.requireNonNull(source.readString()), source.readInt(),
null // mRawTypes
);
mRawTypes = readRawTypes(source);
}
@NonNull
public static MapObject createMapObject(@NonNull FeatureId featureId, @MapObjectType int mapObjectType,
@NonNull String title, @NonNull String subtitle, double lat, double lon)
{
return new MapObject(featureId, mapObjectType, title, "", subtitle, "", lat, lon, null, "", null,
OPENING_MODE_PREVIEW, Popularity.defaultInstance(), "", RoadWarningMarkType.UNKNOWN.ordinal(),
new String[0]);
}
@NonNull
private static List<String> readRawTypes(@NonNull Parcel source)
{
List<String> types = new ArrayList<>();
source.readStringList(types);
return types;
}
/**
* If you override {@link #equals(Object)} it is also required to override {@link #hashCode()}.
* MapObject does not participate in any sets or other collections that need {@code hashCode()}.
* So {@code sameAs()} serves as {@code equals()} but does not break the equals+hashCode contract.
*/
public boolean sameAs(@Nullable MapObject other)
{
if (other == null)
return false;
if (this == other)
return true;
// noinspection SimplifiableIfStatement
if (getClass() != other.getClass())
return false;
if (mFeatureId.isRealId() && other.getFeatureId().isRealId())
return mFeatureId.equals(other.getFeatureId());
return Double.doubleToLongBits(mLon) == Double.doubleToLongBits(other.mLon)
&& Double.doubleToLongBits(mLat) == Double.doubleToLongBits(other.mLat);
}
public static boolean same(@Nullable MapObject one, @Nullable MapObject another)
{
// noinspection SimplifiableIfStatement
if (one == null && another == null)
return true;
return (one != null && one.sameAs(another));
}
public double getScale()
{
return 0;
}
@NonNull
public String getTitle()
{
return mTitle;
}
public void setTitle(@NonNull String title)
{
mTitle = title;
}
@NonNull
public String getName()
{
return getTitle();
}
@Nullable
public String getSecondaryTitle()
{
return mSecondaryTitle;
}
@NonNull
public String getSubtitle()
{
return mSubtitle;
}
public double getLat()
{
return mLat;
}
public double getLon()
{
return mLon;
}
@NonNull
public String getAddress()
{
return mAddress;
}
@NonNull
public String getDescription()
{
return mDescription;
}
public void setDescription(@NonNull String description)
{
mDescription = description;
}
@NonNull
public RoadWarningMarkType getRoadWarningMarkType()
{
return mRoadWarningMarkType;
}
@NonNull
public String getMetadata(Metadata.MetadataType type)
{
final String res = mMetadata.getMetadata(type);
return res == null ? "" : res;
}
@NonNull
public String getWebsiteUrl(boolean strip, @NonNull Metadata.MetadataType type)
{
final String website = Uri.decode(getMetadata(type));
final int len = website.length();
if (strip && len > 1)
{
final int start = website.startsWith(kHttps) ? kHttps.length() : (website.startsWith(kHttp) ? kHttp.length() : 0);
final int end = website.endsWith("/") ? len - 1 : len;
return website.substring(start, end);
}
return website;
}
public String getApiId()
{
return mApiId;
}
public void setLat(double lat)
{
mLat = lat;
}
public void setLon(double lon)
{
mLon = lon;
}
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public void addMetadata(int type, String value)
{
mMetadata.addMetadata(type, value);
}
public boolean hasPhoneNumber()
{
return !TextUtils.isEmpty(getMetadata(Metadata.MetadataType.FMD_PHONE_NUMBER));
}
public boolean hasAtm()
{
return mRawTypes.contains("amenity-atm");
}
public final boolean isMyPosition()
{
return mMapObjectType == MY_POSITION;
}
public final boolean isBookmark()
{
return mMapObjectType == BOOKMARK;
}
public final boolean isTrack()
{
return mMapObjectType == TRACK;
}
@Nullable
public RoutePointInfo getRoutePointInfo()
{
return mRoutePointInfo;
}
@OpeningMode
public int getOpeningMode()
{
return mOpeningMode;
}
@NonNull
public FeatureId getFeatureId()
{
return mFeatureId;
}
private static MapObject readFromParcel(Parcel source)
{
@MapObjectType
int type = source.readInt();
if (type == BOOKMARK)
return new Bookmark(type, source);
return new MapObject(type, source);
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
// A map object type must be written first, since it's used in readParcel method to distinguish
// what type of object should be read from the parcel.
dest.writeInt(mMapObjectType);
dest.writeParcelable(mFeatureId, 0);
dest.writeString(mTitle);
dest.writeString(mSecondaryTitle);
dest.writeString(mSubtitle);
dest.writeString(mAddress);
dest.writeDouble(mLat);
dest.writeDouble(mLon);
dest.writeParcelable(mMetadata, 0);
dest.writeString(mApiId);
dest.writeParcelable(mRoutePointInfo, 0);
dest.writeInt(mOpeningMode);
// dest.writeParcelable(mPopularity, 0);
dest.writeString(mDescription);
dest.writeInt(getRoadWarningMarkType().ordinal());
// All collections are deserialized AFTER non-collection and primitive type objects,
// so collections must be always serialized at the end.
dest.writeStringList(mRawTypes);
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
MapObject mapObject = (MapObject) o;
return mFeatureId.equals(mapObject.mFeatureId);
}
@Override
public int hashCode()
{
return mFeatureId.hashCode();
}
public static final Creator<MapObject> CREATOR = new Creator<>() {
@Override
public MapObject createFromParcel(Parcel source)
{
return readFromParcel(source);
}
@Override
public MapObject[] newArray(int size)
{
return new MapObject[size];
}
};
}

View File

@@ -0,0 +1,148 @@
package app.organicmaps.sdk.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
public class Metadata implements Parcelable
{
// Values must correspond to the Metadata definition from indexer/feature_meta.hpp.
public enum MetadataType
{
// Defined by classifier types now.
FMD_CUISINE(1),
FMD_OPEN_HOURS(2),
FMD_PHONE_NUMBER(3),
FMD_FAX_NUMBER(4),
FMD_STARS(5),
FMD_OPERATOR(6),
// Removed and is not used in the core. Use FMD_WEBSITE instead.
// FMD_URL(7),
FMD_WEBSITE(8),
FMD_INTERNET(9),
FMD_ELE(10),
FMD_TURN_LANES(11),
FMD_TURN_LANES_FORWARD(12),
FMD_TURN_LANES_BACKWARD(13),
FMD_EMAIL(14),
FMD_POSTCODE(15),
// TODO: It is hacked in jni and returns full Wikipedia url. Should use separate getter instead.
FMD_WIKIPEDIA(16),
// TODO: Skipped now.
FMD_DESCRIPTION(17),
FMD_FLATS(18),
FMD_HEIGHT(19),
FMD_MIN_HEIGHT(20),
FMD_DENOMINATION(21),
FMD_BUILDING_LEVELS(22),
FWD_TEST_ID(23),
FMD_CUSTOM_IDS(24),
FMD_PRICE_RATES(25),
FMD_RATINGS(26),
FMD_EXTERNAL_URI(27),
FMD_LEVEL(28),
FMD_AIRPORT_IATA(29),
FMD_BRAND(30),
FMD_DURATION(31),
FMD_CONTACT_FACEBOOK(32),
FMD_CONTACT_INSTAGRAM(33),
FMD_CONTACT_TWITTER(34),
FMD_CONTACT_VK(35),
FMD_CONTACT_LINE(36),
FMD_DESTINATION(37),
FMD_DESTINATION_REF(38),
FMD_JUNCTION_REF(39),
FMD_BUILDING_MIN_LEVEL(40),
FMD_WIKIMEDIA_COMMONS(41),
FMD_CAPACITY(42),
FMD_WHEELCHAIR(43),
FMD_LOCAL_REF(44),
FMD_DRIVE_THROUGH(45),
FMD_WEBSITE_MENU(46),
FMD_SELF_SERVICE(47),
FMD_OUTDOOR_SEATING(48),
FMD_NETWORK(49),
FMD_CONTACT_FEDIVERSE(50),
FMD_CONTACT_BLUESKY(51),
FMD_PANORAMAX(52);
private final int mMetaType;
MetadataType(int metadataType)
{
mMetaType = metadataType;
}
@NonNull
public static MetadataType fromInt(@IntRange(from = 1, to = 49) int metaType)
{
for (MetadataType type : values())
if (type.mMetaType == metaType)
return type;
throw new IllegalArgumentException("Illegal metaType: " + metaType);
}
public int toInt()
{
return mMetaType;
}
}
private final Map<MetadataType, String> mMetadataMap = new HashMap<>();
public void addMetadata(int metaType, String metaValue)
{
final MetadataType type = MetadataType.fromInt(metaType);
mMetadataMap.put(type, metaValue);
}
@Nullable
String getMetadata(MetadataType type)
{
return mMetadataMap.get(type);
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeInt(mMetadataMap.size());
for (Map.Entry<MetadataType, String> metaEntry : mMetadataMap.entrySet())
{
dest.writeInt(metaEntry.getKey().mMetaType);
dest.writeString(metaEntry.getValue());
}
}
public static Metadata readFromParcel(Parcel source)
{
final Metadata metadata = new Metadata();
final int size = source.readInt();
for (int i = 0; i < size; i++)
metadata.addMetadata(source.readInt(), source.readString());
return metadata;
}
public static final Creator<Metadata> CREATOR = new Creator<>() {
@Override
public Metadata createFromParcel(Parcel source)
{
return readFromParcel(source);
}
@Override
public Metadata[] newArray(int size)
{
return new Metadata[size];
}
};
}

View File

@@ -0,0 +1,53 @@
package app.organicmaps.sdk.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Keep;
// TODO consider removal and usage of platform PointF
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class ParcelablePointD implements Parcelable
{
public final double x;
public final double y;
@Override
public int describeContents()
{
// TODO Auto-generated method stub
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeDouble(x);
dest.writeDouble(y);
}
private ParcelablePointD(Parcel in)
{
x = in.readDouble();
y = in.readDouble();
}
public ParcelablePointD(double x, double y)
{
this.x = x;
this.y = y;
}
public static final Parcelable.Creator<ParcelablePointD> CREATOR = new Parcelable.Creator<>() {
public ParcelablePointD createFromParcel(Parcel in)
{
return new ParcelablePointD(in);
}
public ParcelablePointD[] newArray(int size)
{
return new ParcelablePointD[size];
}
};
}

View File

@@ -0,0 +1,50 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import dalvik.annotation.optimization.FastNative;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.stream.IntStream;
public class PredefinedColors
{
@Retention(RetentionPolicy.SOURCE)
@IntRange(from = 0)
public @interface Color
{}
/// @note Color format: ARGB
@ColorInt
private static final int[] PREDEFINED_COLORS = nativeGetPredefinedColors();
@ColorInt
public static int getColor(int index)
{
return PREDEFINED_COLORS[index];
}
@PredefinedColors.Color
public static List<Integer> getAllPredefinedColors()
{
// 0 is reserved for "no color" option.
return IntStream.range(1, PREDEFINED_COLORS.length).boxed().toList();
}
public static int getPredefinedColorIndex(@ColorInt int color)
{
// 0 is reserved for "no color" option.
for (int index = 1; index < PREDEFINED_COLORS.length; index++)
{
if (PREDEFINED_COLORS[index] == color)
return index;
}
return -1;
}
@FastNative
@NonNull
private static native int[] nativeGetPredefinedColors();
}

View File

@@ -0,0 +1,59 @@
package app.organicmaps.sdk.bookmarks.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
public class Result implements Parcelable
{
@Nullable
private final String mFilePath;
@Nullable
private final String mArchiveId;
public Result(@Nullable String filePath, @Nullable String archiveId)
{
mFilePath = filePath;
mArchiveId = archiveId;
}
protected Result(Parcel in)
{
mFilePath = in.readString();
mArchiveId = in.readString();
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeString(mFilePath);
dest.writeString(mArchiveId);
}
@Override
public String toString()
{
return "Result{"
+ "mFilePath='" + mFilePath + '\'' + ", mArchiveId='" + mArchiveId + '\'' + '}';
}
public static final Creator<Result> CREATOR = new Creator<>() {
@Override
public Result createFromParcel(Parcel in)
{
return new Result(in);
}
@Override
public Result[] newArray(int size)
{
return new Result[size];
}
};
}

View File

@@ -0,0 +1,9 @@
package app.organicmaps.sdk.bookmarks.data;
public enum RoadWarningMarkType
{
TOLL,
FERRY,
DIRTY,
UNKNOWN
}

View File

@@ -0,0 +1,51 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public class SortedBlock
{
@NonNull
private final String mName;
@NonNull
private final List<Long> mBookmarkIds;
@NonNull
private final List<Long> mTrackIds;
public SortedBlock(@NonNull String name, @NonNull Long[] bookmarkIds, @NonNull Long[] trackIds)
{
mName = name;
mBookmarkIds = new ArrayList<>(Arrays.asList(bookmarkIds));
mTrackIds = new ArrayList<>(Arrays.asList(trackIds));
}
public boolean isBookmarksBlock()
{
return !mBookmarkIds.isEmpty();
}
@NonNull
public String getName()
{
return mName;
}
@SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
@NonNull
public List<Long> getBookmarkIds()
{
return mBookmarkIds;
}
@SuppressWarnings("AssignmentOrReturnOfFieldWithMutableType")
@NonNull
public List<Long> getTrackIds()
{
return mTrackIds;
}
}

View File

@@ -0,0 +1,107 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.sdk.routing.RoutePointInfo;
import app.organicmaps.sdk.search.Popularity;
import app.organicmaps.sdk.util.Distance;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class Track extends MapObject
{
private final long mTrackId;
private long mCategoryId;
private final String mName;
private final Distance mLength;
private int mColor;
@Nullable
private ElevationInfo mElevationInfo;
@Nullable
private TrackStatistics mTrackStatistics;
Track(long trackId, long categoryId, String name, Distance length, int color)
{
super(FeatureId.EMPTY, TRACK, name, "", "", "", 0, 0, "", null, OPENING_MODE_PREVIEW_PLUS, null, "",
RoadWarningMarkType.UNKNOWN.ordinal(), null);
mTrackId = trackId;
mCategoryId = categoryId;
mName = name;
mLength = length;
mColor = color;
}
// used by JNI
Track(@NonNull FeatureId featureId, @IntRange(from = 0) long categoryId, @IntRange(from = 0) long trackId,
String title, @Nullable String secondaryTitle, @Nullable String subtitle, @Nullable String address,
@Nullable RoutePointInfo routePointInfo, @OpeningMode int openingMode, @NonNull Popularity popularity,
@NonNull String description, @Nullable String[] rawTypes, int color, Distance length, double lat, double lon)
{
super(featureId, TRACK, title, secondaryTitle, subtitle, address, lat, lon, "", routePointInfo, openingMode,
popularity, description, RoadWarningMarkType.UNKNOWN.ordinal(), rawTypes);
mTrackId = trackId;
mCategoryId = categoryId;
mColor = color;
mName = title;
mLength = length;
}
// Change of the category in the core is done in PlacePageView::onCategoryChanged().
public void setCategoryId(@NonNull long categoryId)
{
mCategoryId = categoryId;
}
public void setColor(@NonNull int color)
{
mColor = color;
BookmarkManager.INSTANCE.changeTrackColor(getTrackId(), color);
}
public String getName()
{
return mName;
}
public Distance getLength()
{
return mLength;
}
public int getColor()
{
return mColor;
}
public long getTrackId()
{
return mTrackId;
}
public long getCategoryId()
{
return mCategoryId;
}
public String getTrackDescription()
{
return BookmarkManager.INSTANCE.getTrackDescription(mTrackId);
}
public ElevationInfo getElevationInfo()
{
if (mElevationInfo == null)
mElevationInfo = BookmarkManager.nativeGetTrackElevationInfo(mTrackId);
return mElevationInfo;
}
public TrackStatistics getTrackStatistics()
{
if (mTrackStatistics == null)
mTrackStatistics = BookmarkManager.nativeGetTrackStatistics(mTrackId);
return mTrackStatistics;
}
}

View File

@@ -0,0 +1,57 @@
package app.organicmaps.sdk.bookmarks.data;
import androidx.annotation.Keep;
// Used by JNI
@Keep
public class TrackStatistics
{
private final double m_length;
private final double m_duration;
private final double m_ascent;
private final double m_descent;
private final int m_minElevation;
private final int m_maxElevation;
@Keep
public TrackStatistics(double length, double duration, double ascent, double descent, int minElevation,
int maxElevation)
{
m_length = length;
m_duration = duration;
m_ascent = ascent;
m_descent = descent;
m_minElevation = minElevation;
m_maxElevation = maxElevation;
}
public double getLength()
{
return m_length;
}
public double getDuration()
{
return m_duration;
}
public double getAscent()
{
return m_ascent;
}
public double getDescent()
{
return m_descent;
}
public int getMinElevation()
{
return m_minElevation;
}
public int getMaxElevation()
{
return m_maxElevation;
}
}

View File

@@ -0,0 +1,11 @@
package app.organicmaps.sdk.content;
import androidx.annotation.NonNull;
public interface DataSource<D>
{
@NonNull
D getData();
void invalidate();
}

View File

@@ -0,0 +1,10 @@
package app.organicmaps.sdk.display;
import androidx.annotation.NonNull;
public interface DisplayChangedListener
{
default void onDisplayChangedToDevice(@NonNull Runnable onTaskFinishedCallback) {}
default void onDisplayChangedToCar(@NonNull Runnable onTaskFinishedCallback) {}
}

View File

@@ -0,0 +1,176 @@
package app.organicmaps.sdk.display;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.sdk.util.log.Logger;
import java.util.Objects;
public class DisplayManager
{
private static final String TAG = DisplayManager.class.getSimpleName();
private interface TaskWithCallback
{
void start(@NonNull Runnable onTaskFinishedCallback);
}
private static class DisplayHolder
{
boolean notify = false;
DisplayChangedListener listener;
public void destroy()
{
notify = false;
listener = null;
}
}
private final Handler mHandler = new Handler(Looper.getMainLooper());
@NonNull
private DisplayType mCurrentDisplayType = DisplayType.Device;
@Nullable
private DisplayHolder mDevice;
@Nullable
private DisplayHolder mCar;
public boolean isCarConnected()
{
return mCar != null;
}
public boolean isDeviceConnected()
{
return mDevice != null;
}
public boolean isCarDisplayUsed()
{
return mCurrentDisplayType == DisplayType.Car;
}
public boolean isDeviceDisplayUsed()
{
return mCurrentDisplayType == DisplayType.Device;
}
public void addListener(@NonNull final DisplayType displayType, @NonNull final DisplayChangedListener listener)
{
Logger.d(TAG, "displayType = " + displayType + ", listener = " + listener);
if (displayType == DisplayType.Device)
{
mDevice = new DisplayHolder();
mDevice.notify = true;
mDevice.listener = listener;
}
else if (displayType == DisplayType.Car && mCar == null)
{
mCar = new DisplayHolder();
mCar.notify = true;
mCar.listener = listener;
}
if (isCarConnected() && !isDeviceConnected())
mCurrentDisplayType = displayType;
}
public void removeListener(@NonNull final DisplayType displayType)
{
Logger.d(TAG, "displayType = " + displayType);
if (displayType == DisplayType.Device && mDevice != null)
{
mDevice.destroy();
mDevice = null;
if (isCarConnected() && !isCarDisplayUsed())
changeDisplay(DisplayType.Car);
}
else if (displayType == DisplayType.Car && mCar != null)
{
mCar.destroy();
mCar = null;
if (isDeviceConnected() && !isDeviceDisplayUsed())
changeDisplay(DisplayType.Device);
}
}
public void changeDisplay(@NonNull final DisplayType newDisplayType)
{
Logger.d(TAG, "newDisplayType = " + newDisplayType);
if (mCurrentDisplayType == newDisplayType)
return;
if (mCar != null)
mCar.notify = true;
if (mDevice != null)
mDevice.notify = true;
mCurrentDisplayType = newDisplayType;
if (mCurrentDisplayType == DisplayType.Device)
onDisplayTypeChangedToDevice();
else if (mCurrentDisplayType == DisplayType.Car)
onDisplayTypeChangedToCar();
}
private void onDisplayTypeChangedToDevice()
{
Logger.d(TAG);
TaskWithCallback firstTask = null;
TaskWithCallback secondTask = null;
if (mCar != null && mCar.notify)
{
firstTask = mCar.listener::onDisplayChangedToDevice;
mCar.notify = false;
}
if (mDevice != null && mDevice.notify)
{
if (firstTask == null)
firstTask = mDevice.listener::onDisplayChangedToDevice;
else
secondTask = mDevice.listener::onDisplayChangedToDevice;
mDevice.notify = false;
}
postTask(Objects.requireNonNull(firstTask), secondTask);
}
private void onDisplayTypeChangedToCar()
{
Logger.d(TAG);
TaskWithCallback firstTask = null;
TaskWithCallback secondTask = null;
if (mDevice != null && mDevice.notify)
{
firstTask = mDevice.listener::onDisplayChangedToCar;
mDevice.notify = false;
}
if (mCar != null && mCar.notify)
{
if (firstTask == null)
firstTask = mCar.listener::onDisplayChangedToCar;
else
secondTask = mCar.listener::onDisplayChangedToCar;
mCar.notify = false;
}
postTask(Objects.requireNonNull(firstTask), secondTask);
}
private void postTask(@NonNull TaskWithCallback firstTask, @Nullable TaskWithCallback secondTask)
{
mHandler.post(() -> firstTask.start(() -> {
if (secondTask != null)
mHandler.post(() -> secondTask.start(() -> {}));
}));
}
}

View File

@@ -0,0 +1,7 @@
package app.organicmaps.sdk.display;
public enum DisplayType
{
Device,
Car
}

View File

@@ -0,0 +1,72 @@
package app.organicmaps.sdk.downloader;
import android.annotation.TargetApi;
import android.content.Context;
import app.organicmaps.sdk.R;
import app.organicmaps.sdk.util.log.Logger;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
// Fix missing root certificates for HTTPS connections on Android 7 and below:
// https://community.letsencrypt.org/t/letsencrypt-certificates-fails-on-android-phones-running-android-7-or-older/205686
@TargetApi(24)
public class Android7RootCertificateWorkaround
{
private static final String TAG = Android7RootCertificateWorkaround.class.getSimpleName();
@TargetApi(24)
private static SSLSocketFactory mSslSocketFactory;
public static void applyFixIfNeeded(HttpURLConnection connection)
{
// Deliberately not checking for null to have an exception from setSSLSocketFactory.
if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.N
&& connection.getURL().getProtocol().equals("https"))
((HttpsURLConnection) connection).setSSLSocketFactory(mSslSocketFactory);
}
public static void initializeIfNeeded(Context context)
{
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.N)
return;
final int[] certificates = new int[] {R.raw.isrgrootx1, R.raw.globalsignr4, R.raw.gtsrootr1,
R.raw.gtsrootr2, R.raw.gtsrootr3, R.raw.gtsrootr4};
try
{
final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
// Load PEM certificates from raw resources.
for (final int rawCertificateId : certificates)
{
try (final InputStream caInput = context.getResources().openRawResource(rawCertificateId))
{
final CertificateFactory cf = CertificateFactory.getInstance("X.509");
final Certificate ca = cf.generateCertificate(caInput);
keyStore.setCertificateEntry("ca" + rawCertificateId, ca);
}
}
final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
final SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
mSslSocketFactory = sslContext.getSocketFactory();
}
catch (Exception e)
{
e.printStackTrace();
Logger.e(TAG, "Failed to load certificates: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,276 @@
package app.organicmaps.sdk.downloader;
import android.os.AsyncTask;
import android.util.Base64;
import androidx.annotation.Keep;
import app.organicmaps.sdk.util.Constants;
import app.organicmaps.sdk.util.StringUtils;
import app.organicmaps.sdk.util.Utils;
import app.organicmaps.sdk.util.log.Logger;
import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
// Used from JNI.
@Keep
@SuppressWarnings({"unused", "deprecation"}) // https://github.com/organicmaps/organicmaps/issues/3632
class ChunkTask extends AsyncTask<Void, byte[], Integer>
{
private static final String TAG = ChunkTask.class.getSimpleName();
private static final int TIMEOUT_IN_SECONDS = 10;
private final long mHttpCallbackID;
private final String mUrl;
private final long mBeg;
private final long mEnd;
private final long mExpectedFileSize;
private byte[] mPostBody;
private static final int IO_EXCEPTION = -1;
private static final int WRITE_EXCEPTION = -2;
private static final int INCONSISTENT_FILE_SIZE = -3;
private static final int NON_HTTP_RESPONSE = -4;
private static final int INVALID_URL = -5;
private static final int CANCELLED = -6;
private long mDownloadedBytes;
private static final Executor sExecutors = Executors.newFixedThreadPool(4);
public ChunkTask(long httpCallbackID, String url, long beg, long end, long expectedFileSize, byte[] postBody)
{
mHttpCallbackID = httpCallbackID;
mUrl = url;
mBeg = beg;
mEnd = end;
mExpectedFileSize = expectedFileSize;
mPostBody = postBody;
}
@Override
protected void onPreExecute()
{}
@Override
protected void onPostExecute(Integer httpOrErrorCode)
{
// It seems like onPostExecute can be called (from GUI thread queue)
// after the task was cancelled in destructor of HttpThread.
// Reproduced by Samsung testers: touch Try Again for many times from
// start activity when no connection is present.
if (!isCancelled())
nativeOnFinish(mHttpCallbackID, httpOrErrorCode, mBeg, mEnd);
}
@Override
protected void onProgressUpdate(byte[]... data)
{
if (!isCancelled())
{
// Use progress event to save downloaded bytes.
if (nativeOnWrite(mHttpCallbackID, mBeg + mDownloadedBytes, data[0], data[0].length))
mDownloadedBytes += data[0].length;
else
{
// Cancel downloading and notify about error.
cancel(false);
nativeOnFinish(mHttpCallbackID, WRITE_EXCEPTION, mBeg, mEnd);
}
}
}
void start()
{
executeOnExecutor(sExecutors, (Void[]) null);
}
private static long parseContentRange(String contentRangeValue)
{
if (contentRangeValue != null)
{
final int slashIndex = contentRangeValue.lastIndexOf('/');
if (slashIndex >= 0)
{
try
{
return Long.parseLong(contentRangeValue.substring(slashIndex + 1));
}
catch (final NumberFormatException ex)
{
// Return -1 at the end of function
}
}
}
return -1;
}
@Override
protected Integer doInBackground(Void... p)
{
HttpURLConnection urlConnection = null;
/*
* TODO improve reliability of connections & handle EOF errors.
* <a href="http://stackoverflow.com/questions/19258518/android-httpurlconnection-eofexception">asd</a>
*/
try
{
final URL url = new URL(mUrl);
urlConnection = (HttpURLConnection) url.openConnection();
if (isCancelled())
return CANCELLED;
Android7RootCertificateWorkaround.applyFixIfNeeded(urlConnection);
urlConnection.setUseCaches(false);
urlConnection.setConnectTimeout(TIMEOUT_IN_SECONDS * 1000);
urlConnection.setReadTimeout(TIMEOUT_IN_SECONDS * 1000);
// Provide authorization credentials
String creds = url.getUserInfo();
if (creds != null)
{
String value = "Basic " + Base64.encodeToString(creds.getBytes(), Base64.DEFAULT);
urlConnection.setRequestProperty("Authorization", value);
}
// use Range header only if we don't download whole file from start
if (!(mBeg == 0 && mEnd < 0))
{
if (mEnd > 0)
urlConnection.setRequestProperty("Range", StringUtils.formatUsingUsLocale("bytes=%d-%d", mBeg, mEnd));
else
urlConnection.setRequestProperty("Range", StringUtils.formatUsingUsLocale("bytes=%d-", mBeg));
}
final Map<?, ?> requestParams = urlConnection.getRequestProperties();
if (mPostBody != null)
{
urlConnection.setDoOutput(true);
urlConnection.setFixedLengthStreamingMode(mPostBody.length);
final DataOutputStream os = new DataOutputStream(urlConnection.getOutputStream());
os.write(mPostBody);
os.flush();
mPostBody = null;
Utils.closeSafely(os);
}
if (isCancelled())
return CANCELLED;
final int err = urlConnection.getResponseCode();
if (err == HttpURLConnection.HTTP_NOT_FOUND)
return err;
// @TODO We can handle redirect (301, 302 and 307) here and display redirected page to user,
// to avoid situation when downloading is always failed by "unknown" reason
// When we didn't ask for chunks, code should be 200
// When we asked for a chunk, code should be 206
final boolean isChunk = !(mBeg == 0 && mEnd < 0);
if ((isChunk && err != HttpURLConnection.HTTP_PARTIAL) || (!isChunk && err != HttpURLConnection.HTTP_OK))
{
// we've set error code so client should be notified about the error
Logger.w(TAG, "Error for " + urlConnection.getURL() + ": Server replied with code " + err
+ ", aborting download. " + Utils.mapPrettyPrint(requestParams));
return INCONSISTENT_FILE_SIZE;
}
// Check for content size - are we downloading requested file or some router's garbage?
if (mExpectedFileSize > 0)
{
long contentLength = parseContentRange(urlConnection.getHeaderField("Content-Range"));
if (contentLength < 0)
contentLength = urlConnection.getContentLength();
// Check even if contentLength is invalid (-1), in this case it's not our server!
if (contentLength != mExpectedFileSize)
{
// we've set error code so client should be notified about the error
Logger.w(TAG, "Error for " + urlConnection.getURL() + ": Invalid file size received (" + contentLength
+ ") while expecting " + mExpectedFileSize + ". Aborting download.");
return INCONSISTENT_FILE_SIZE;
}
// @TODO Else display received web page to user - router is redirecting us to some page
}
return downloadFromStream(new BufferedInputStream(urlConnection.getInputStream(), 128 * Constants.KB));
}
catch (final MalformedURLException ex)
{
Logger.e(TAG, "Invalid url: " + mUrl, ex);
return INVALID_URL;
}
catch (final IOException ex)
{
Logger.d(TAG, "IOException in doInBackground for URL: " + mUrl, ex);
return IO_EXCEPTION;
}
finally
{
if (urlConnection != null)
urlConnection.disconnect();
}
}
private Integer downloadFromStream(InputStream stream)
{
// Because of timeouts in InputStream.read (for bad connection),
// try to introduce dynamic buffer size to read in one query.
final int[] arrSize = {128, 32, 1};
int ret = IO_EXCEPTION;
for (int size : arrSize)
{
try
{
ret = downloadFromStreamImpl(stream, size * Constants.KB);
break;
}
catch (final IOException ex)
{
Logger.e(TAG, "IOException in downloadFromStream for buffer size: " + size, ex);
}
}
Utils.closeSafely(stream);
return ret;
}
/**
* @throws IOException
*/
private int downloadFromStreamImpl(InputStream stream, int bufferSize) throws IOException
{
final byte[] tempBuf = new byte[bufferSize];
int readBytes;
while ((readBytes = stream.read(tempBuf)) > 0)
{
if (isCancelled())
return CANCELLED;
final byte[] chunk = new byte[readBytes];
System.arraycopy(tempBuf, 0, chunk, 0, readBytes);
publishProgress(chunk);
}
// -1 - means the end of the stream (success), else - some error occurred
return (readBytes == -1 ? HttpURLConnection.HTTP_OK : IO_EXCEPTION);
}
private static native boolean nativeOnWrite(long httpCallbackID, long beg, byte[] data, long size);
private static native void nativeOnFinish(long httpCallbackID, long httpCode, long beg, long end);
}

View File

@@ -0,0 +1,161 @@
package app.organicmaps.sdk.downloader;
import android.text.TextUtils;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.sdk.util.StringUtils;
/**
* Class representing a single item in countries hierarchy.
* Fields are filled by native code.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public final class CountryItem implements Comparable<CountryItem>
{
private static String sRootId;
// Must correspond to ItemCategory in MapManager.cpp
public static final int CATEGORY_NEAR_ME = 0;
public static final int CATEGORY_DOWNLOADED = 1;
public static final int CATEGORY_AVAILABLE = 2;
public static final int CATEGORY__LAST = CATEGORY_AVAILABLE;
// Must correspond to NodeStatus in storage_defines.hpp
public static final int STATUS_UNKNOWN = 0;
public static final int STATUS_PROGRESS = 1; // Downloading a new mwm or updating an old one.
public static final int STATUS_APPLYING = 2; // Applying downloaded diff for an old mwm.
public static final int STATUS_ENQUEUED = 3; // An mwm is waiting for downloading in the queue.
public static final int STATUS_FAILED = 4; // An error happened while downloading
public static final int STATUS_UPDATABLE = 5; // An update for a downloaded mwm is ready according to counties.txt.
public static final int STATUS_DONE = 6; // Downloaded mwm(s) is up to date. No need to update it.
public static final int STATUS_DOWNLOADABLE = 7; // An mwm can be downloaded but not downloaded yet.
public static final int STATUS_PARTLY = 8; // Leafs of group node has a mix of STATUS_DONE and STATUS_DOWNLOADABLE.
// Must correspond to NodeErrorCode in storage_defines.hpp
public static final int ERROR_NONE = 0;
public static final int ERROR_UNKNOWN = 1;
public static final int ERROR_OOM = 2;
public static final int ERROR_NO_INTERNET = 3;
public final String id;
public String directParentId;
public String topmostParentId;
public String name;
public String directParentName;
public String topmostParentName;
public String description;
public long size;
public long enqueuedSize;
public long totalSize;
public int childCount;
public int totalChildCount;
public int category;
public int status;
public int errorCode;
public boolean present;
/**
* This value represents the percentage of download (values span from 0 to 100)
*/
public float progress;
public long downloadedBytes;
public long bytesToDownload;
// Internal ID for grouping under headers in the list
public int headerId;
// Internal field to store search result name
@Nullable
public String searchResultName;
private static void ensureRootIdKnown()
{
if (sRootId == null)
sRootId = MapManager.nativeGetRoot();
}
public CountryItem(String id)
{
this.id = id;
}
@Override
public int hashCode()
{
return id.hashCode();
}
@SuppressWarnings("SimplifiableIfStatement")
@Override
public boolean equals(Object other)
{
if (this == other)
return true;
if (other == null || getClass() != other.getClass())
return false;
return id.equals(((CountryItem) other).id);
}
@Override
public int compareTo(@NonNull CountryItem another)
{
int catDiff = (category - another.category);
if (catDiff != 0)
return catDiff;
return name.compareTo(another.name);
}
public void update()
{
MapManager.nativeGetAttributes(this);
ensureRootIdKnown();
if (TextUtils.equals(sRootId, directParentId))
directParentId = "";
}
@NonNull
public static CountryItem fill(String countryId)
{
CountryItem res = new CountryItem(countryId);
res.update();
return res;
}
public static boolean isRoot(String id)
{
ensureRootIdKnown();
return sRootId.equals(id);
}
public static String getRootId()
{
ensureRootIdKnown();
return sRootId;
}
public boolean isExpandable()
{
return (totalChildCount > 1);
}
@Override
public String toString()
{
return "{ id: \"" + id + "\", directParentId: \"" + directParentId + "\", topmostParentId: \"" + topmostParentId
+ "\", category: \"" + category + "\", name: \"" + name + "\", directParentName: \"" + directParentName
+ "\", topmostParentName: \"" + topmostParentName + "\", present: " + present + ", status: " + status
+ ", errorCode: " + errorCode + ", headerId: " + headerId + ", size: " + size + ", enqueuedSize: " + enqueuedSize
+ ", totalSize: " + totalSize + ", childCount: " + childCount + ", totalChildCount: " + totalChildCount
+ ", progress: " + StringUtils.formatUsingUsLocale("%.2f", progress) + "% }";
}
}

View File

@@ -0,0 +1,23 @@
package app.organicmaps.sdk.downloader;
import androidx.annotation.Nullable;
import androidx.core.util.Consumer;
class ExpandRetryConfirmationListener implements Runnable
{
@Nullable
private final Consumer<Boolean> mDialogClickListener;
ExpandRetryConfirmationListener(@Nullable Consumer<Boolean> dialogClickListener)
{
mDialogClickListener = dialogClickListener;
}
@Override
public void run()
{
if (mDialogClickListener == null)
return;
mDialogClickListener.accept(true);
}
}

View File

@@ -0,0 +1,20 @@
package app.organicmaps.sdk.downloader;
import androidx.annotation.Keep;
/**
* Info about data to be updated. Created by native code.
*/
// Called from JNI.
@Keep
public final class UpdateInfo
{
public final int filesCount;
public final long totalSize;
public UpdateInfo(int filesCount, long totalSize)
{
this.filesCount = filesCount;
this.totalSize = totalSize;
}
}

View File

@@ -0,0 +1,190 @@
package app.organicmaps.sdk.editor;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Size;
import androidx.annotation.WorkerThread;
import app.organicmaps.BuildConfig;
import app.organicmaps.sdk.Framework;
import app.organicmaps.sdk.bookmarks.data.Metadata;
import app.organicmaps.sdk.editor.data.FeatureCategory;
import app.organicmaps.sdk.editor.data.Language;
import app.organicmaps.sdk.editor.data.LocalizedName;
import app.organicmaps.sdk.editor.data.LocalizedStreet;
import app.organicmaps.sdk.editor.data.NamesDataSource;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Edits active(selected on the map) feature, which is represented as osm::EditableFeature in the core.
*/
public final class Editor
{
// Should correspond to core osm::FeatureStatus.
@Retention(RetentionPolicy.SOURCE)
@IntDef({UNTOUCHED, DELETED, OBSOLETE, MODIFIED, CREATED})
public @interface FeatureStatus
{}
public static final int UNTOUCHED = 0;
public static final int DELETED = 1;
public static final int OBSOLETE = 2;
public static final int MODIFIED = 3;
public static final int CREATED = 4;
private Editor() {}
static
{
nativeInit();
}
private static native void nativeInit();
@WorkerThread
public static void uploadChanges()
{
if (nativeHasSomethingToUpload() && OsmOAuth.isAuthorized())
nativeUploadChanges(OsmOAuth.getAuthToken(), BuildConfig.VERSION_NAME, BuildConfig.APPLICATION_ID);
}
public static native boolean nativeShouldShowEditPlace();
public static native boolean nativeShouldShowAddBusiness();
public static native boolean nativeShouldShowAddPlace();
public static native boolean nativeShouldEnableEditPlace();
public static native boolean nativeShouldEnableAddPlace();
@NonNull
public static native int[] nativeGetEditableProperties();
public static native String nativeGetCategory();
public static native String nativeGetMetadata(int id);
public static native boolean nativeIsMetadataValid(int id, String value);
public static native void nativeSetMetadata(int id, String value);
public static native String nativeGetOpeningHours();
public static native void nativeSetOpeningHours(String openingHours);
public static String nativeGetPhone()
{
return nativeGetMetadata(Metadata.MetadataType.FMD_PHONE_NUMBER.toInt());
}
public static void nativeSetPhone(String phone)
{
nativeSetMetadata(Metadata.MetadataType.FMD_PHONE_NUMBER.toInt(), phone);
}
public static native int nativeGetStars();
public static native int nativeGetMaxEditableBuildingLevels();
public static String nativeGetBuildingLevels()
{
return nativeGetMetadata(Metadata.MetadataType.FMD_BUILDING_LEVELS.toInt());
}
public static void nativeSetBuildingLevels(String levels)
{
nativeSetMetadata(Metadata.MetadataType.FMD_BUILDING_LEVELS.toInt(), levels);
}
public static native boolean nativeHasWifi();
public static native void nativeSetHasWifi(boolean hasWifi);
public static void nativeSetSwitchInput(int id, Boolean switchValue, String checkedValue, String uncheckedValue)
{
nativeSetMetadata(id, switchValue ? checkedValue : uncheckedValue);
}
public static boolean nativeGetSwitchInput(int id, String checkedValue)
{
String value = nativeGetMetadata(id);
return value.equals(checkedValue);
}
public static native boolean nativeIsAddressEditable();
public static native boolean nativeIsNameEditable();
public static native boolean nativeIsPointType();
public static native boolean nativeIsBuilding();
public static native NamesDataSource nativeGetNamesDataSource();
public static native void nativeSetNames(@NonNull LocalizedName[] names);
public static native LocalizedName nativeMakeLocalizedName(String langCode, String name);
public static native Language[] nativeGetSupportedLanguages(boolean includeServiceLangs);
public static native LocalizedStreet nativeGetStreet();
public static native void nativeSetStreet(LocalizedStreet street);
@NonNull
public static native LocalizedStreet[] nativeGetNearbyStreets();
public static native String nativeGetHouseNumber();
public static native void nativeSetHouseNumber(String houseNumber);
public static native boolean nativeIsHouseValid(String houseNumber);
public static native boolean nativeCheckHouseNumberWhenIsAddress();
public static boolean nativeIsLevelValid(String level)
{
return nativeIsMetadataValid(Metadata.MetadataType.FMD_BUILDING_LEVELS.toInt(), level);
}
public static boolean nativeIsPhoneValid(String phone)
{
return nativeIsMetadataValid(Metadata.MetadataType.FMD_PHONE_NUMBER.toInt(), phone);
}
public static native boolean nativeIsNameValid(String name);
public static native boolean nativeHasSomethingToUpload();
@WorkerThread
private static native void nativeUploadChanges(String oauthToken, String appVersion, String appId);
/**
* @return array [total edits count, uploaded edits count, last upload timestamp in seconds]
*/
@Size(3)
public static native long[] nativeGetStats();
public static native void nativeClearLocalEdits();
/**
* That method should be called, when user opens editor from place page, so that information in editor
* could refresh.
*/
public static native void nativeStartEdit();
/**
* @return true if feature was saved. False if some error occurred (eg. no space)
*/
public static native boolean nativeSaveEditedFeature();
@NonNull
public static native String[] nativeGetAllCreatableFeatureTypes(@NonNull String lang);
@NonNull
public static native String[] nativeSearchCreatableFeatureTypes(@NonNull String query, @NonNull String lang);
/**
* Creates new object on the map. Places it in the center of current viewport.
* {@link Framework#nativeIsDownloadedMapAtScreenCenter()} should be called before
* to check whether new feature can be created on the map.
*/
public static void createMapObject(FeatureCategory category)
{
nativeCreateMapObject(category.getType());
}
public static native void nativeCreateMapObject(@NonNull String type);
public static native void nativeCreateNote(String text);
public static native void nativePlaceDoesNotExist(@NonNull String comment);
public static native void nativeRollbackMapObject();
public static native void nativeCreateStandaloneNote(double lat, double lon, String text);
/**
* @return all cuisines keys.
*/
public static native String[] nativeGetCuisines();
/**
* @return selected cuisines keys.
*/
public static native String[] nativeGetSelectedCuisines();
public static native String[] nativeFilterCuisinesKeys(String substr);
public static native String[] nativeTranslateCuisines(String[] keys);
public static native void nativeSetSelectedCuisines(String[] keys);
/**
* @return properly formatted and appended cuisines string to display in UI.
*/
public static native String nativeGetFormattedCuisine();
public static native String nativeGetMwmName();
public static native long nativeGetMwmVersion();
@FeatureStatus
public static native int nativeGetMapObjectStatus();
public static native boolean nativeIsMapObjectUploaded();
}

View File

@@ -0,0 +1,61 @@
package app.organicmaps.sdk.editor;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.editor.OhState;
import app.organicmaps.sdk.editor.data.Timespan;
import app.organicmaps.sdk.editor.data.Timetable;
public final class OpeningHours
{
private OpeningHours() {}
static
{
nativeInit();
}
private static native void nativeInit();
@NonNull
public static native Timetable[] nativeGetDefaultTimetables();
@NonNull
public static native Timetable nativeGetComplementTimetable(Timetable[] timetableSet);
@NonNull
public static native Timetable nativeSetIsFullday(Timetable timetable, boolean isFullday);
@NonNull
public static native Timetable[] nativeAddWorkingDay(Timetable[] timetables, int timetableIndex,
@IntRange(from = 1, to = 7) int day);
@NonNull
public static native Timetable[] nativeRemoveWorkingDay(Timetable[] timetables, int timetableIndex,
@IntRange(from = 1, to = 7) int day);
@NonNull
public static native Timetable nativeSetOpeningTime(Timetable timetable, Timespan openingTime);
@NonNull
public static native Timetable nativeAddClosedSpan(Timetable timetable, Timespan closedSpan);
@NonNull
public static native Timetable nativeRemoveClosedSpan(Timetable timetable, int spanIndex);
@Nullable
public static native Timetable[] nativeTimetablesFromString(String source);
@NonNull
public static native String nativeTimetablesToString(@NonNull Timetable[] timetables);
/**
* Sometimes timetables cannot be parsed with {@link #nativeTimetablesFromString} (hence can't be displayed in UI),
* but still are valid OSM timetables.
* @return true if timetable string is valid OSM timetable.
*/
public static native boolean nativeIsTimetableStringValid(String source);
public static native OhState nativeCurrentState(@NonNull Timetable[] timetables);
}

View File

@@ -0,0 +1,150 @@
package app.organicmaps.sdk.editor;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.FragmentManager;
import app.organicmaps.sdk.util.NetworkPolicy;
public final class OsmOAuth
{
private OsmOAuth() {}
public enum AuthType
{
OSM("OSM"),
GOOGLE("Google");
public final String name;
AuthType(String name)
{
this.name = name;
}
}
@SuppressWarnings("NotNullFieldNotInitialized")
@NonNull
private static SharedPreferences mPrefs;
private static final String PREF_OSM_USERNAME = "OsmUsername";
private static final String PREF_OSM_CHANGESETS_COUNT = "OsmChangesetsCount";
private static final String PREF_OSM_OAUTH2_TOKEN = "OsmOAuth2Token";
public static final String URL_PARAM_VERIFIER = "oauth_verifier";
public static void init(@NonNull SharedPreferences prefs)
{
mPrefs = prefs;
}
public static boolean isAuthorized()
{
return mPrefs.contains(PREF_OSM_OAUTH2_TOKEN);
}
public static String getAuthToken()
{
return mPrefs.getString(PREF_OSM_OAUTH2_TOKEN, "");
}
public static String getUsername()
{
return mPrefs.getString(PREF_OSM_USERNAME, "");
}
public static Bitmap getProfilePicture()
{
// TODO(HB): load and store image in cache here
return null;
}
public static void setAuthorization(String oauthToken, String username)
{
mPrefs.edit().putString(PREF_OSM_OAUTH2_TOKEN, oauthToken).putString(PREF_OSM_USERNAME, username).apply();
}
public static void clearAuthorization()
{
mPrefs.edit()
.remove(PREF_OSM_USERNAME)
.remove(PREF_OSM_OAUTH2_TOKEN)
.apply();
}
@NonNull
public static String getHistoryUrl()
{
return nativeGetHistoryUrl(getUsername());
}
@NonNull
public static String getNotesUrl()
{
return nativeGetNotesUrl(getUsername());
}
/*
Returns 5 strings: ServerURL, ClientId, ClientSecret, Scope, RedirectUri
*/
@NonNull
public static native String nativeGetOAuth2Url();
/**
* @return string with OAuth2 token
*/
@WorkerThread
@Size(2)
@Nullable
public static native String nativeAuthWithPassword(String login, String password);
/**
* @return string with OAuth2 token
*/
@WorkerThread
@Nullable
public static native String nativeAuthWithOAuth2Code(String oauth2code);
@WorkerThread
@Nullable
public static native String nativeGetOsmUsername(String oauthToken);
@WorkerThread
@Nullable
public static native String nativeGetOsmProfilePictureUrl(String oauthToken);
@WorkerThread
@NonNull
public static native String nativeGetHistoryUrl(String user);
@WorkerThread
@NonNull
public static native String nativeGetNotesUrl(String user);
/**
* @return < 0 if failed to get changesets count.
*/
@WorkerThread
private static native int nativeGetOsmChangesetsCount(String oauthToken);
@WorkerThread
public static int getOsmChangesetsCount(@NonNull NetworkPolicy.DialogPresenter dialogPresenter,
@NonNull FragmentManager fm)
{
final int[] editsCount = {-1};
NetworkPolicy.checkNetworkPolicy(dialogPresenter, fm, policy -> {
if (!policy.canUseNetwork())
return;
final String token = getAuthToken();
editsCount[0] = OsmOAuth.nativeGetOsmChangesetsCount(token);
});
if (editsCount[0] < 0)
return mPrefs.getInt(PREF_OSM_CHANGESETS_COUNT, 0);
mPrefs.edit().putInt(PREF_OSM_CHANGESETS_COUNT, editsCount[0]).apply();
return editsCount[0];
}
}

View File

@@ -0,0 +1,65 @@
package app.organicmaps.sdk.editor.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class FeatureCategory implements Parcelable
{
@NonNull
private final String mType;
@NonNull
private final String mLocalizedTypeName;
public FeatureCategory(@NonNull String type, @NonNull String localizedTypeName)
{
mType = type;
mLocalizedTypeName = localizedTypeName;
}
private FeatureCategory(Parcel source)
{
mType = source.readString();
mLocalizedTypeName = source.readString();
}
@NonNull
public String getType()
{
return mType;
}
@NonNull
public String getLocalizedTypeName()
{
return mLocalizedTypeName;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeString(mType);
dest.writeString(mLocalizedTypeName);
}
public static final Creator<FeatureCategory> CREATOR = new Creator<>() {
@Override
public FeatureCategory createFromParcel(Parcel source)
{
return new FeatureCategory(source);
}
@Override
public FeatureCategory[] newArray(int size)
{
return new FeatureCategory[size];
}
};
}

View File

@@ -0,0 +1,77 @@
package app.organicmaps.sdk.editor.data;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.util.StringUtils;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class HoursMinutes implements Parcelable
{
public final long hours;
public final long minutes;
private final boolean m24HourFormat;
// 24 hours or even 25 and higher values are used in OSM data and passed here from JNI calls.
// Example: 18:00-24:00
public HoursMinutes(@IntRange(from = 0, to = 24) long hours, @IntRange(from = 0, to = 59) long minutes,
boolean is24HourFormat)
{
this.hours = hours;
this.minutes = minutes;
m24HourFormat = is24HourFormat;
}
protected HoursMinutes(Parcel in)
{
hours = in.readLong();
minutes = in.readLong();
m24HourFormat = in.readByte() != 0;
}
@NonNull
@Override
public String toString()
{
if (m24HourFormat)
return StringUtils.formatUsingUsLocale("%02d:%02d", hours, minutes);
// Formatting a string here with hours outside of 0-23 range causes DateTimeException.
final LocalTime localTime = LocalTime.of((int) hours % 24, (int) minutes);
return localTime.format(DateTimeFormatter.ofPattern("hh:mm a"));
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeLong(hours);
dest.writeLong(minutes);
dest.writeByte((byte) (m24HourFormat ? 1 : 0));
}
public static final Creator<HoursMinutes> CREATOR = new Creator<>() {
@Override
public HoursMinutes createFromParcel(Parcel in)
{
return new HoursMinutes(in);
}
@Override
public HoursMinutes[] newArray(int size)
{
return new HoursMinutes[size];
}
};
}

View File

@@ -0,0 +1,23 @@
package app.organicmaps.sdk.editor.data;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
// Corresponds to StringUtf8Multilang::Lang in core.
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class Language
{
// StringUtf8Multilang::GetLangByCode(StringUtf8Multilang::kDefaultCode).
public static final String DEFAULT_LANG_CODE = "default";
public final String code;
public final String name;
public Language(@NonNull String code, @NonNull String name)
{
this.code = code;
this.name = name;
}
}

View File

@@ -0,0 +1,26 @@
package app.organicmaps.sdk.editor.data;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class LocalizedName
{
public int code;
@NonNull
public String name;
@NonNull
public String lang;
@NonNull
public String langName;
public LocalizedName(int code, @NonNull String name, @NonNull String lang, @NonNull String langName)
{
this.code = code;
this.name = name;
this.lang = lang;
this.langName = langName;
}
}

View File

@@ -0,0 +1,19 @@
package app.organicmaps.sdk.editor.data;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class LocalizedStreet
{
public final String defaultName;
public final String localizedName;
public LocalizedStreet(@NonNull String defaultName, @NonNull String localizedName)
{
this.defaultName = defaultName;
this.localizedName = localizedName;
}
}

View File

@@ -0,0 +1,35 @@
package app.organicmaps.sdk.editor.data;
import androidx.annotation.Keep;
/**
* Class which contains array of localized names with following priority:
* 1. Names for Mwm languages;
* 2. User`s language name;
* 3. Other names;
* and mandatoryNamesCount - count of names which should be always shown.
*/
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class NamesDataSource
{
private final LocalizedName[] mNames;
private final int mMandatoryNamesCount;
public NamesDataSource(LocalizedName[] names, int mandatoryNamesCount)
{
this.mNames = names;
this.mMandatoryNamesCount = mandatoryNamesCount;
}
public LocalizedName[] getNames()
{
return mNames;
}
public int getMandatoryNamesCount()
{
return mMandatoryNamesCount;
}
}

View File

@@ -0,0 +1,29 @@
package app.organicmaps.sdk.editor.data;
import androidx.annotation.Keep;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class Timespan
{
public final HoursMinutes start;
public final HoursMinutes end;
public Timespan(HoursMinutes start, HoursMinutes end)
{
this.start = start;
this.end = end;
}
@Override
public String toString()
{
return start + "-" + end;
}
public String toWideString()
{
return start + "\u2014" + end;
}
}

View File

@@ -0,0 +1,55 @@
package app.organicmaps.sdk.editor.data;
import androidx.annotation.IntRange;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public class Timetable
{
public final Timespan workingTimespan;
public final Timespan[] closedTimespans;
public final boolean isFullday;
public final int[] weekdays;
public Timetable(@NonNull Timespan workingTime, @NonNull Timespan[] closedHours, boolean isFullday,
@NonNull int[] weekdays)
{
this.workingTimespan = workingTime;
this.closedTimespans = closedHours;
this.isFullday = isFullday;
this.weekdays = weekdays;
}
public boolean containsWeekday(@IntRange(from = 1, to = 7) int day)
{
for (int workingDay : weekdays)
{
if (workingDay == day)
return true;
}
return false;
}
public boolean isFullWeek()
{
return weekdays.length == 7;
}
@Override
public String toString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Working timespan : ").append(workingTimespan).append("\n").append("Closed timespans : ");
for (Timespan timespan : closedTimespans)
stringBuilder.append(timespan).append(" ");
stringBuilder.append("\n");
stringBuilder.append("Fullday : ").append(isFullday).append("\n").append("Weekdays : ");
for (int i : weekdays)
stringBuilder.append(i);
return stringBuilder.toString();
}
}

View File

@@ -0,0 +1,126 @@
package app.organicmaps.sdk.location;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import static app.organicmaps.sdk.util.concurrency.UiThread.runLater;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresPermission;
import androidx.core.location.LocationListenerCompat;
import androidx.core.location.LocationManagerCompat;
import androidx.core.location.LocationRequestCompat;
import app.organicmaps.sdk.util.log.Logger;
import java.util.HashSet;
import java.util.Set;
class AndroidNativeProvider extends BaseLocationProvider
{
private static final String TAG = AndroidNativeProvider.class.getSimpleName();
private class NativeLocationListener implements LocationListenerCompat
{
@Override
public void onLocationChanged(@NonNull Location location)
{
mListener.onLocationChanged(location);
}
@Override
public void onProviderDisabled(@NonNull String provider)
{
Logger.d(TAG, "Disabled location provider: " + provider);
mProviders.remove(provider);
if (mProviders.isEmpty())
mListener.onLocationDisabled();
}
@Override
public void onProviderEnabled(@NonNull String provider)
{
Logger.d(TAG, "Enabled location provider: " + provider);
mProviders.add(provider);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras)
{
Logger.d(TAG, "Status changed for location provider: " + provider + "; new status = " + status);
}
}
@NonNull
private final LocationManager mLocationManager;
private final Set<String> mProviders;
@NonNull
final private NativeLocationListener mNativeLocationListener = new NativeLocationListener();
AndroidNativeProvider(@NonNull Context context, @NonNull BaseLocationProvider.Listener listener)
{
super(listener);
mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
mProviders = new HashSet<>();
// This service is always available on all versions of Android
if (mLocationManager == null)
throw new IllegalStateException("Can't get LOCATION_SERVICE");
}
// A permission is checked externally
@Override
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
public void start(long interval)
{
Logger.d(TAG);
if (!mProviders.isEmpty())
throw new IllegalStateException("Already started");
final LocationRequestCompat locationRequest =
new LocationRequestCompat
.Builder(interval)
// The quality is a hint to providers on how they should weigh power vs accuracy tradeoffs.
.setQuality(LocationRequestCompat.QUALITY_HIGH_ACCURACY)
.build();
// API 31+ provides `fused` provider which aggregates `gps` and `network` and potentially other sensors as well.
// Unfortunately, certain LineageOS ROMs have broken `fused` provider that pretends to be enabled, but in
// reality it does absolutely nothing and doesn't return any location updates. For this reason, we try all
// (`fused`, `network`, `gps`) providers here, but prefer `fused` in LocationHelper.onLocationChanged().
//
// https://developer.android.com/reference/android/location/LocationManager#FUSED_PROVIDER
// https://issuetracker.google.com/issues/215186921#comment3
// https://github.com/organicmaps/organicmaps/issues/4158
//
mProviders.addAll(mLocationManager.getProviders(true));
mProviders.remove(LocationManager.PASSIVE_PROVIDER); // not really useful if other providers are enabled.
if (mProviders.isEmpty())
{
// Call this callback in the next event loop to allow LocationHelper::start() to finish.
Logger.e(TAG, "No providers available");
runLater(mListener::onLocationDisabled);
return;
}
for (String provider : mProviders)
{
Logger.d(TAG, "Request Android native provider '" + provider + "' to get locations at this interval = " + interval
+ " ms");
LocationManagerCompat.requestLocationUpdates(mLocationManager, provider, locationRequest, mNativeLocationListener,
Looper.myLooper());
}
}
@SuppressWarnings("MissingPermission")
// A permission is checked externally
@Override
public void stop()
{
Logger.d(TAG);
mProviders.clear();
LocationManagerCompat.removeUpdates(mLocationManager, mNativeLocationListener);
}
}

View File

@@ -0,0 +1,41 @@
package app.organicmaps.sdk.location;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.app.PendingIntent;
import android.location.Location;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresPermission;
import androidx.annotation.UiThread;
abstract class BaseLocationProvider
{
interface Listener
{
@UiThread
void onLocationChanged(@NonNull Location location);
@UiThread
void onLocationDisabled();
// Used by GoogleFusedLocationProvider.
@SuppressWarnings("unused")
@UiThread
void onLocationResolutionRequired(@NonNull PendingIntent pendingIntent);
// Used by GoogleFusedLocationProvider.
@SuppressWarnings("unused")
@UiThread
void onFusedLocationUnsupported();
}
@NonNull
protected final Listener mListener;
protected BaseLocationProvider(@NonNull Listener listener)
{
mListener = listener;
}
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
protected abstract void start(long interval);
protected abstract void stop();
}

View File

@@ -0,0 +1,493 @@
package app.organicmaps.sdk.location;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.annotation.UiThread;
import androidx.core.content.ContextCompat;
import androidx.core.location.GnssStatusCompat;
import androidx.core.location.LocationManagerCompat;
import app.organicmaps.sdk.Framework;
import app.organicmaps.sdk.Map;
import app.organicmaps.sdk.bookmarks.data.FeatureId;
import app.organicmaps.sdk.bookmarks.data.MapObject;
import app.organicmaps.sdk.routing.JunctionInfo;
import app.organicmaps.sdk.routing.RoutingController;
import app.organicmaps.sdk.util.Config;
import app.organicmaps.sdk.util.LocationUtils;
import app.organicmaps.sdk.util.NetworkPolicy;
import app.organicmaps.sdk.util.log.Logger;
import org.chromium.base.ObserverList;
public class LocationHelper implements BaseLocationProvider.Listener
{
private static final long INTERVAL_FOLLOW_MS = 0;
private static final long INTERVAL_NOT_FOLLOW_MS = 3000;
private static final long INTERVAL_NAVIGATION_MS = 1000;
private static final long INTERVAL_TRACK_RECORDING = 0;
private static final long AGPS_EXPIRATION_TIME_MS = 16 * 60 * 60 * 1000; // 16 hours
private static final long LOCATION_UPDATE_TIMEOUT_MS = 30 * 1000; // 30 seconds
@NonNull
private final Context mContext;
@NonNull
private final SensorHelper mSensorHelper;
private static final String TAG = LocationState.LOCATION_TAG;
private final ObserverList<LocationListener> mListeners = new ObserverList<>();
private final ObserverList.RewindableIterator<LocationListener> mListenersIterator = mListeners.rewindableIterator();
@Nullable
private Location mSavedLocation;
private MapObject mMyPosition;
@NonNull
private BaseLocationProvider mLocationProvider;
private long mInterval;
private boolean mInFirstRun;
private boolean mActive;
private Handler mHandler;
private Runnable mLocationTimeoutRunnable = this::notifyLocationUpdateTimeout;
@NonNull
private final GnssStatusCompat.Callback mGnssStatusCallback = new GnssStatusCompat.Callback() {
@Override
public void onStarted()
{
Logger.d(TAG);
}
@Override
public void onStopped()
{
Logger.d(TAG);
}
@Override
public void onFirstFix(int ttffMillis)
{
Logger.d(TAG, "ttffMillis = " + ttffMillis);
}
@Override
public void onSatelliteStatusChanged(@NonNull GnssStatusCompat status)
{
int used = 0;
boolean fixed = false;
for (int i = 0; i < status.getSatelliteCount(); i++)
{
if (status.usedInFix(i))
{
used++;
fixed = true;
}
}
Logger.d(TAG, "total = " + status.getSatelliteCount() + " used = " + used + " fixed = " + fixed);
}
};
public LocationHelper(@NonNull Context context, @NonNull SensorHelper sensorHelper)
{
mContext = context;
mSensorHelper = sensorHelper;
mLocationProvider = LocationProviderFactory.getProvider(mContext, this);
mHandler = new Handler();
}
/**
* @return MapObject.MY_POSITION, null if location is not yet determined or "My position" button is switched off.
*/
@Nullable
public MapObject getMyPosition()
{
if (!isActive())
{
mMyPosition = null;
return null;
}
if (mSavedLocation == null)
return null;
if (mMyPosition == null)
mMyPosition = MapObject.createMapObject(FeatureId.EMPTY, MapObject.MY_POSITION, "", "",
mSavedLocation.getLatitude(), mSavedLocation.getLongitude());
return mMyPosition;
}
/**
* Obtains last known location.
* @return {@code null} if no location is saved.
*/
@Nullable
public Location getSavedLocation()
{
return mSavedLocation;
}
/**
* Indicates about whether a location provider is polling location updates right now or not.
*/
public boolean isActive()
{
return mActive;
}
private void notifyLocationUpdated()
{
if (mSavedLocation == null)
throw new IllegalStateException("No saved location");
mHandler.removeCallbacks(mLocationTimeoutRunnable);
mHandler.postDelayed(mLocationTimeoutRunnable, LOCATION_UPDATE_TIMEOUT_MS); // Reset the timeout.
mListenersIterator.rewind();
while (mListenersIterator.hasNext())
mListenersIterator.next().onLocationUpdated(mSavedLocation);
// If we are still in the first run mode, i.e. user is staying on the first run screens,
// not on the map, we mustn't post location update to the core. Only this preserving allows us
// to play nice zoom animation once a user will leave first screens and will see a map.
if (mInFirstRun)
{
Logger.d(TAG, "Location update is obtained and must be ignored, because the app is in a first run mode");
return;
}
LocationState.nativeLocationUpdated(mSavedLocation.getTime(), mSavedLocation.getLatitude(),
mSavedLocation.getLongitude(), mSavedLocation.getAccuracy(),
mSavedLocation.getAltitude(), mSavedLocation.getSpeed(),
mSavedLocation.getBearing());
}
private void notifyLocationUpdateTimeout()
{
mHandler.removeCallbacks(mLocationTimeoutRunnable);
if (!isActive())
{
Logger.w(TAG, "Provider is not active");
return;
}
Logger.d(TAG);
mListenersIterator.rewind();
while (mListenersIterator.hasNext())
mListenersIterator.next().onLocationUpdateTimeout();
}
@Override
public void onLocationChanged(@NonNull Location location)
{
Logger.d(TAG, "provider = " + mLocationProvider.getClass().getSimpleName() + " location = " + location);
if (!isActive())
{
Logger.w(TAG, "Provider is not active");
return;
}
if (!LocationUtils.isAccuracySatisfied(location))
{
Logger.w(TAG, "Unsatisfied accuracy for location = " + location);
return;
}
if (mSavedLocation != null)
{
if (!LocationUtils.isLocationBetterThanLast(location, mSavedLocation))
{
Logger.d(TAG, "The new " + location + " is worse than the last " + mSavedLocation);
return;
}
}
mSavedLocation = location;
mMyPosition = null;
notifyLocationUpdated();
}
// Used by GoogleFusedLocationProvider.
@SuppressWarnings("unused")
@Override
@UiThread
public void onLocationResolutionRequired(@NonNull PendingIntent pendingIntent)
{
Logger.d(TAG);
if (!isActive())
{
Logger.w(TAG, "Provider is not active");
return;
}
// Stop provider until location resolution is granted.
stop();
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
mListenersIterator.rewind();
while (mListenersIterator.hasNext())
mListenersIterator.next().onLocationResolutionRequired(pendingIntent);
}
// Used by GoogleFusedLocationProvider.
@SuppressWarnings("unused")
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
@Override
@UiThread
public void onFusedLocationUnsupported()
{
// Try to downgrade to the native provider first and restart the service before notifying the user.
Logger.d(TAG, "provider = " + mLocationProvider.getClass().getSimpleName() + " is not supported,"
+ " downgrading to use native provider");
mLocationProvider.stop();
mLocationProvider = new AndroidNativeProvider(mContext, this);
mActive = true;
mLocationProvider.start(mInterval);
}
// RouteSimulationProvider doesn't really require location permissions.
@SuppressLint("MissingPermission")
public void startNavigationSimulation(JunctionInfo[] points)
{
Logger.i(TAG);
mLocationProvider.stop();
mLocationProvider = new RouteSimulationProvider(mContext, this, points);
mActive = true;
mLocationProvider.start(mInterval);
}
@Override
@UiThread
public void onLocationDisabled()
{
Logger.d(TAG, "provider = " + mLocationProvider.getClass().getSimpleName()
+ " settings = " + LocationUtils.areLocationServicesTurnedOn(mContext));
stop();
LocationState.nativeOnLocationError(LocationState.ERROR_GPS_OFF);
mListenersIterator.rewind();
while (mListenersIterator.hasNext())
mListenersIterator.next().onLocationDisabled();
}
/**
* Registers listener to obtain location updates.
*
* @param listener listener to be registered.
*/
@UiThread
public void addListener(@NonNull LocationListener listener)
{
Logger.d(TAG, "listener: " + listener + " count was: " + mListeners.size());
mListeners.addObserver(listener);
if (mSavedLocation != null)
listener.onLocationUpdated(mSavedLocation);
}
/**
* Removes given location listener.
* @param listener listener to unregister.
*/
@UiThread
public void removeListener(@NonNull LocationListener listener)
{
Logger.d(TAG, "listener: " + listener + " count was: " + mListeners.size());
mListeners.removeObserver(listener);
}
private long calcLocationUpdatesInterval()
{
if (RoutingController.get().isNavigating())
return INTERVAL_NAVIGATION_MS;
if (TrackRecorder.nativeIsTrackRecordingEnabled())
return INTERVAL_TRACK_RECORDING;
final int mode = Map.isEngineCreated() ? LocationState.getMode() : LocationState.NOT_FOLLOW_NO_POSITION;
return switch (mode)
{
case LocationState.PENDING_POSITION, LocationState.FOLLOW, LocationState.FOLLOW_AND_ROTATE -> INTERVAL_FOLLOW_MS;
case LocationState.NOT_FOLLOW, LocationState.NOT_FOLLOW_NO_POSITION -> INTERVAL_NOT_FOLLOW_MS;
default -> throw new IllegalArgumentException("Unsupported location mode: " + mode);
};
}
/**
* Restart the location with a new refresh interval if changed.
*/
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
public void restartWithNewMode()
{
if (!isActive())
{
start();
return;
}
final long newInterval = calcLocationUpdatesInterval();
if (newInterval == mInterval)
return;
Logger.i(TAG, "update refresh interval: old = " + mInterval + " new = " + newInterval);
mLocationProvider.stop();
mInterval = newInterval;
mLocationProvider.start(newInterval);
}
/**
* Starts polling location updates.
*/
@RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
public void start()
{
if (isActive())
{
Logger.d(TAG, "Already started");
return;
}
Logger.i(TAG);
checkForAgpsUpdates();
if (LocationUtils.checkFineLocationPermission(mContext))
mSensorHelper.start();
final long oldInterval = mInterval;
mInterval = calcLocationUpdatesInterval();
Logger.i(TAG, "provider = " + mLocationProvider.getClass().getSimpleName() + " mInFirstRun = " + mInFirstRun
+ " oldInterval = " + oldInterval + " interval = " + mInterval);
mActive = true;
mLocationProvider.start(mInterval);
mHandler.postDelayed(mLocationTimeoutRunnable, LOCATION_UPDATE_TIMEOUT_MS);
subscribeToGnssStatusUpdates();
}
/**
* Stops the polling location updates.
*/
public void stop()
{
if (!isActive())
{
Logger.d(TAG, "Already stopped");
return;
}
Logger.i(TAG);
mLocationProvider.stop();
unsubscribeFromGnssStatusUpdates();
mSensorHelper.stop();
mHandler.removeCallbacks(mLocationTimeoutRunnable);
mActive = false;
}
/**
* Resume location services when entering the foreground.
*/
public void resumeLocationInForeground()
{
if (isActive())
return;
else if (!Map.isEngineCreated())
{
// LocationState.nativeGetMode() is initialized only after drape creation.
// https://github.com/organicmaps/organicmaps/issues/1128#issuecomment-1784435190
Logger.d(TAG, "Engine is not created yet.");
return;
}
else if (LocationState.getMode() == LocationState.NOT_FOLLOW_NO_POSITION)
{
Logger.i(TAG, "Location updates are stopped by the user manually.");
return;
}
else if (!LocationUtils.checkLocationPermission(mContext))
{
Logger.i(TAG, "Permissions ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION are not granted");
return;
}
start();
}
private void checkForAgpsUpdates()
{
if (!NetworkPolicy.getCurrentNetworkUsageStatus())
return;
long previousTimestamp = Config.getAgpsTimestamp();
long currentTimestamp = System.currentTimeMillis();
if (previousTimestamp + AGPS_EXPIRATION_TIME_MS > currentTimestamp)
{
Logger.d(TAG, "A-GPS should be up to date");
return;
}
Logger.d(TAG, "Requesting new A-GPS data");
Config.setAgpsTimestamp(currentTimestamp);
final LocationManager manager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
manager.sendExtraCommand(LocationManager.GPS_PROVIDER, "force_xtra_injection", null);
manager.sendExtraCommand(LocationManager.GPS_PROVIDER, "force_time_injection", null);
}
private void subscribeToGnssStatusUpdates()
{
// Subscribe to the low-level GNSS status to keep the green dot location indicator always firing.
// https://github.com/organicmaps/organicmaps/issues/5999#issuecomment-1793713369
if (!LocationUtils.checkFineLocationPermission(mContext))
return;
final LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
LocationManagerCompat.registerGnssStatusCallback(locationManager, ContextCompat.getMainExecutor(mContext),
mGnssStatusCallback);
}
private void unsubscribeFromGnssStatusUpdates()
{
final LocationManager locationManager = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
LocationManagerCompat.unregisterGnssStatusCallback(locationManager, mGnssStatusCallback);
}
@UiThread
public boolean isInFirstRun()
{
return mInFirstRun;
}
@UiThread
public void onEnteredIntoFirstRun()
{
Logger.i(TAG);
mInFirstRun = true;
}
@UiThread
public void onExitFromFirstRun()
{
Logger.i(TAG);
if (!mInFirstRun)
throw new AssertionError("Must be called only after 'onEnteredIntoFirstRun' method!");
mInFirstRun = false;
// If there is a location we need just to pass it to the listeners, so that
// my position state machine will be switched to the FOLLOW state.
if (mSavedLocation != null)
{
notifyLocationUpdated();
Logger.d(TAG, "Current location is available, so play the nice zoom animation");
Framework.nativeRunFirstLaunchAnimation();
}
}
}

View File

@@ -0,0 +1,25 @@
package app.organicmaps.sdk.location;
import android.app.PendingIntent;
import android.location.Location;
import androidx.annotation.NonNull;
public interface LocationListener
{
void onLocationUpdated(@NonNull Location location);
default void onLocationUpdateTimeout()
{
// No op.
}
default void onLocationDisabled()
{
// No op.
}
default void onLocationResolutionRequired(@NonNull PendingIntent pendingIntent)
{
// No op.
}
}

View File

@@ -0,0 +1,76 @@
package app.organicmaps.sdk.location;
import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.Map;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public final class LocationState
{
public static final String LOCATION_TAG = LocationState.class.getSimpleName();
public interface ModeChangeListener
{
// Used by JNI.
@Keep
@SuppressWarnings("unused")
void onMyPositionModeChanged(int newMode);
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({PENDING_POSITION, NOT_FOLLOW_NO_POSITION, NOT_FOLLOW, FOLLOW, FOLLOW_AND_ROTATE})
@interface Value
{}
// These values should correspond to location::EMyPositionMode enum (from platform/location.hpp)
public static final int PENDING_POSITION = 0;
public static final int NOT_FOLLOW_NO_POSITION = 1;
public static final int NOT_FOLLOW = 2;
public static final int FOLLOW = 3;
public static final int FOLLOW_AND_ROTATE = 4;
// These constants should correspond to values defined in platform/location.hpp
// Leave 0-value as no any error.
// private static final int ERROR_UNKNOWN = 0;
// private static final int ERROR_NOT_SUPPORTED = 1;
public static final int ERROR_DENIED = 2;
public static final int ERROR_GPS_OFF = 3;
// public static final int ERROR_TIMEOUT = 4; // Unused on Android (only used on Qt)
public static native void nativeSwitchToNextMode();
@Value
private static native int nativeGetMode();
public static native void nativeSetListener(@NonNull ModeChangeListener listener);
public static native void nativeRemoveListener();
public static native void nativeOnLocationError(int errorCode);
static native void nativeLocationUpdated(long time, double lat, double lon, float accuracy, double altitude,
float speed, float bearing);
private LocationState() {}
@Value
public static int getMode()
{
if (!Map.isEngineCreated())
throw new IllegalStateException("Location mode is undefined until engine is created");
return nativeGetMode();
}
public static String nameOf(@Value int mode)
{
return switch (mode)
{
case PENDING_POSITION -> "PENDING_POSITION";
case NOT_FOLLOW_NO_POSITION -> "NOT_FOLLOW_NO_POSITION";
case NOT_FOLLOW -> "NOT_FOLLOW";
case FOLLOW -> "FOLLOW";
case FOLLOW_AND_ROTATE -> "FOLLOW_AND_ROTATE";
default -> "Unknown: " + mode;
};
}
}

View File

@@ -0,0 +1,290 @@
package app.organicmaps.sdk.location;
import android.annotation.SuppressLint;
import android.net.SSLCertificateSocketFactory;
import android.os.SystemClock;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.organicmaps.BuildConfig;
import app.organicmaps.sdk.util.log.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
/**
* Implements interface that will be used by the core for
* sending/receiving the raw data trough platform socket interface.
* <p>
* The instance of this class is supposed to be created in JNI layer
* and supposed to be used in the thread safe environment, i.e. thread safety
* should be provided externally (by the client of this class).
* <p>
* <b>All public methods are blocking and shouldn't be called from the main thread.</b>
*/
// Called from JNI.
@Keep
@SuppressWarnings("unused")
class PlatformSocket
{
private static final String TAG = PlatformSocket.class.getSimpleName();
private final static int DEFAULT_TIMEOUT = 30 * 1000;
private static volatile long sSslConnectionCounter;
@Nullable
private Socket mSocket;
@Nullable
private String mHost;
private int mPort;
private int mTimeout = DEFAULT_TIMEOUT;
PlatformSocket()
{
sSslConnectionCounter = 0;
Logger.d(TAG, "***********************************************************************************");
Logger.d(TAG, "Platform socket is created by core, ssl connection counter is discarded.");
}
public boolean open(@NonNull String host, int port)
{
if (mSocket != null)
{
Logger.e(TAG, "Socket is already opened. Seems that it wasn't closed.");
return false;
}
if (!isPortAllowed(port))
{
Logger.e(TAG, "A wrong port number = " + port + ", it must be within (0-65535) range");
return false;
}
mHost = host;
mPort = port;
Socket socket = createSocket(host, port, true);
if (socket != null && socket.isConnected())
{
setReadSocketTimeout(socket, mTimeout);
mSocket = socket;
}
return mSocket != null;
}
private static boolean isPortAllowed(int port)
{
return port >= 0 && port <= 65535;
}
@Nullable
private static Socket createSocket(@NonNull String host, int port, boolean ssl)
{
return ssl ? createSslSocket(host, port) : createRegularSocket(host, port);
}
@Nullable
private static Socket createSslSocket(@NonNull String host, int port)
{
Socket socket = null;
try
{
SocketFactory sf = getSocketFactory();
socket = sf.createSocket(host, port);
sSslConnectionCounter++;
Logger.d(TAG, "###############################################################################");
Logger.d(TAG, sSslConnectionCounter + " ssl connection is established.");
}
catch (IOException e)
{
Logger.e(TAG, "Failed to create the ssl socket, mHost = " + host + " mPort = " + port);
}
return socket;
}
@Nullable
private static Socket createRegularSocket(@NonNull String host, int port)
{
Socket socket = null;
try
{
socket = new Socket(host, port);
Logger.d(TAG, "Regular socket is created and tcp handshake is passed successfully");
}
catch (IOException e)
{
Logger.e(TAG, "Failed to create the socket, mHost = " + host + " mPort = " + port);
}
return socket;
}
@SuppressLint("SSLCertificateSocketFactoryGetInsecure")
@NonNull
private static SocketFactory getSocketFactory()
{
// Trusting to any ssl certificate factory that will be used in
// debug mode, for testing purposes only.
if (BuildConfig.DEBUG)
// TODO: implement the custom KeyStore to make the self-signed certificates work
return SSLCertificateSocketFactory.getInsecure(0, null);
return SSLSocketFactory.getDefault();
}
public void close()
{
if (mSocket == null)
{
Logger.d(TAG, "Socket is already closed or it wasn't opened yet\n");
return;
}
try
{
mSocket.close();
Logger.d(TAG, "Socket has been closed: " + this + "\n");
}
catch (IOException e)
{
Logger.e(TAG, "Failed to close socket: " + this + "\n");
}
finally
{
mSocket = null;
}
}
public boolean read(@NonNull byte[] data, int count)
{
if (!checkSocketAndArguments(data, count))
return false;
Logger.d(TAG, "Reading method is started, data.length = " + data.length + ", count = " + count);
long startTime = SystemClock.elapsedRealtime();
int readBytes = 0;
try
{
if (mSocket == null)
throw new AssertionError("mSocket cannot be null");
InputStream in = mSocket.getInputStream();
while (readBytes != count && (SystemClock.elapsedRealtime() - startTime) < mTimeout)
{
try
{
Logger.d(TAG, "Attempting to read " + count + " bytes from offset = " + readBytes);
int read = in.read(data, readBytes, count - readBytes);
if (read == -1)
{
Logger.d(TAG, "All data is read from the stream, read bytes count = " + readBytes + "\n");
break;
}
if (read == 0)
{
Logger.e(TAG, "0 bytes are obtained. It's considered as error\n");
break;
}
Logger.d(TAG, "Read bytes count = " + read + "\n");
readBytes += read;
}
catch (SocketTimeoutException e)
{
long readingTime = SystemClock.elapsedRealtime() - startTime;
Logger.e(TAG, "Socked timeout has occurred after " + readingTime + " (ms)\n ");
if (readingTime > mTimeout)
{
Logger.e(TAG, "Socket wrapper timeout has occurred, requested count = " + (count - readBytes)
+ ", readBytes = " + readBytes + "\n");
break;
}
}
}
}
catch (IOException e)
{
Logger.e(TAG, "Failed to read data from socket: " + this + "\n");
}
return count == readBytes;
}
public boolean write(@NonNull byte[] data, int count)
{
if (!checkSocketAndArguments(data, count))
return false;
Logger.d(TAG, "Writing method is started, data.length = " + data.length + ", count = " + count);
long startTime = SystemClock.elapsedRealtime();
try
{
if (mSocket == null)
throw new AssertionError("mSocket cannot be null");
OutputStream out = mSocket.getOutputStream();
out.write(data, 0, count);
Logger.d(TAG, count + " bytes are written\n");
return true;
}
catch (SocketTimeoutException e)
{
long writingTime = SystemClock.elapsedRealtime() - startTime;
Logger.e(TAG, "Socked timeout has occurred after " + writingTime + " (ms)\n");
}
catch (IOException e)
{
Logger.e(TAG, "Failed to write data to socket: " + this + "\n");
}
return false;
}
private boolean checkSocketAndArguments(@NonNull byte[] data, int count)
{
if (mSocket == null)
{
Logger.e(TAG, "Socket must be opened before reading/writing\n");
return false;
}
if (count < 0 || count > data.length)
{
Logger.e(TAG, "Illegal arguments, data.length = " + data.length + ", count = " + count + "\n");
return false;
}
return true;
}
public void setTimeout(int millis)
{
mTimeout = millis;
Logger.d(TAG, "Setting the socket wrapper timeout = " + millis + " ms\n");
}
private void setReadSocketTimeout(@NonNull Socket socket, int millis)
{
try
{
socket.setSoTimeout(millis);
}
catch (SocketException e)
{
Logger.e(TAG, "Failed to set system socket timeout: " + millis + "ms, " + this + "\n");
}
}
@Override
public String toString()
{
return "PlatformSocket{"
+ "mSocket=" + mSocket + ", mHost='" + mHost + '\'' + ", mPort=" + mPort + '}';
}
}

View File

@@ -0,0 +1,64 @@
package app.organicmaps.sdk.location;
import android.content.Context;
import android.location.Location;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.routing.JunctionInfo;
import app.organicmaps.sdk.util.LocationUtils;
import app.organicmaps.sdk.util.concurrency.UiThread;
import app.organicmaps.sdk.util.log.Logger;
class RouteSimulationProvider extends BaseLocationProvider
{
private static final String TAG = RouteSimulationProvider.class.getSimpleName();
private static final long INTERVAL_MS = 1000;
private final JunctionInfo[] mPoints;
private int mCurrentPoint = 0;
private boolean mActive = false;
RouteSimulationProvider(@NonNull Context context, @NonNull Listener listener, JunctionInfo[] points)
{
super(listener);
mPoints = points;
}
@Override
public void start(long interval)
{
Logger.i(TAG);
if (mActive)
throw new IllegalStateException("Already started");
mActive = true;
UiThread.runLater(this::nextPoint);
}
@Override
public void stop()
{
Logger.i(TAG);
mActive = false;
}
public void nextPoint()
{
if (!mActive)
return;
if (mCurrentPoint >= mPoints.length)
{
Logger.i(TAG, "Finished the final point");
mActive = false;
return;
}
final Location location = new Location(LocationUtils.FUSED_PROVIDER);
location.setLatitude(mPoints[mCurrentPoint].mLat);
location.setLongitude(mPoints[mCurrentPoint].mLon);
location.setAccuracy(1.0f);
location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
mListener.onLocationChanged(location);
mCurrentPoint += 1;
UiThread.runLater(this::nextPoint, INTERVAL_MS);
}
}

View File

@@ -0,0 +1,157 @@
package app.organicmaps.sdk.location;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import app.organicmaps.sdk.util.LocationUtils;
import app.organicmaps.sdk.util.log.Logger;
import java.util.LinkedHashSet;
import java.util.Set;
public class SensorHelper implements SensorEventListener
{
private static final String TAG = SensorHelper.class.getSimpleName();
@NonNull
private final SensorManager mSensorManager;
@Nullable
private Sensor mRotationVectorSensor;
private final float[] mRotationMatrix = new float[9];
private final float[] mRotationValues = new float[3];
// Initialized with purposely invalid value.
private int mLastAccuracy = -42;
private double mSavedNorth = Double.NaN;
private int mRotation = 0;
@NonNull
private final Set<SensorListener> mListeners = new LinkedHashSet<>();
@Override
public void onSensorChanged(SensorEvent event)
{
// Here we can have events from one out of these two sensors:
// TYPE_GEOMAGNETIC_ROTATION_VECTOR
// TYPE_ROTATION_VECTOR
if (mLastAccuracy != event.accuracy)
{
mLastAccuracy = event.accuracy;
switch (mLastAccuracy)
{
case SensorManager.SENSOR_STATUS_ACCURACY_HIGH: break;
case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
for (SensorListener listener : mListeners)
listener.onCompassCalibrationRecommended();
break;
case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
case SensorManager.SENSOR_STATUS_UNRELIABLE:
default:
for (SensorListener listener : mListeners)
listener.onCompassCalibrationRequired();
}
}
if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE)
return;
SensorManager.getRotationMatrixFromVector(mRotationMatrix, event.values);
SensorManager.getOrientation(mRotationMatrix, mRotationValues);
// mRotationValues indexes: 0 - yaw (azimuth), 1 - pitch, 2 - roll.
mSavedNorth = LocationUtils.correctCompassAngle(mRotation, mRotationValues[0]);
for (SensorListener listener : mListeners)
listener.onCompassUpdated(mSavedNorth);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy)
{
Log.w("onAccuracyChanged", "Sensor " + sensor.getStringType() + " has changed accuracy to " + accuracy);
// This method is called _only_ when accuracy changes. To know the initial startup accuracy,
// and to show calibration warning toast if necessary, we check it in onSensorChanged().
// Looks like modern Androids can send this event after starting the sensor.
}
public SensorHelper(@NonNull Context context)
{
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
}
public double getSavedNorth()
{
return mSavedNorth;
}
public void setRotation(int rotation)
{
Logger.i(TAG, "rotation = " + rotation);
mRotation = rotation;
}
/**
* Registers listener to obtain compass updates.
* @param listener listener to be registered.
*/
@UiThread
public void addListener(@NonNull SensorListener listener)
{
Logger.d(TAG, "listener: " + listener + " count was: " + mListeners.size());
mListeners.add(listener);
if (!Double.isNaN(mSavedNorth))
listener.onCompassUpdated(mSavedNorth);
}
/**
* Removes given compass listener.
* @param listener listener to unregister.
*/
@UiThread
public void removeListener(@NonNull SensorListener listener)
{
Logger.d(TAG, "listener: " + listener + " count was: " + mListeners.size());
mListeners.remove(listener);
}
public void start()
{
if (mRotationVectorSensor != null)
{
Logger.d(TAG, "Already started");
return;
}
mRotationVectorSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
if (mRotationVectorSensor == null)
{
Logger.w(TAG, "There is no ROTATION_VECTOR sensor, requesting GEOMAGNETIC_ROTATION_VECTOR");
mRotationVectorSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR);
if (mRotationVectorSensor == null)
{
// Can be null in rare cases on devices without magnetic sensors.
Logger.w(TAG, "There is no GEOMAGNETIC_ROTATION_VECTOR sensor, device orientation can not be calculated");
return;
}
}
Logger.d(TAG);
mSensorManager.registerListener(this, mRotationVectorSensor, SensorManager.SENSOR_DELAY_UI);
}
public void stop()
{
if (mRotationVectorSensor == null)
return;
Logger.d(TAG);
mSensorManager.unregisterListener(this);
mRotationVectorSensor = null;
}
}

View File

@@ -0,0 +1,16 @@
package app.organicmaps.sdk.location;
public interface SensorListener
{
void onCompassUpdated(double north);
default void onCompassCalibrationRecommended()
{
// No op.
}
default void onCompassCalibrationRequired()
{
// No op.
}
}

View File

@@ -0,0 +1,14 @@
package app.organicmaps.sdk.location;
public class TrackRecorder
{
public static native void nativeStartTrackRecording();
public static native void nativeStopTrackRecording();
public static native void nativeSaveTrackRecordingWithName(String name);
public static native boolean nativeIsTrackRecordingEmpty();
public static native boolean nativeIsTrackRecordingEnabled();
}

View File

@@ -0,0 +1,71 @@
package app.organicmaps.sdk.maplayer;
import android.content.Context;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.Framework;
import app.organicmaps.sdk.maplayer.isolines.IsolinesManager;
import app.organicmaps.sdk.maplayer.subway.SubwayManager;
import app.organicmaps.sdk.maplayer.traffic.TrafficManager;
public enum Mode
{
TRAFFIC {
@Override
public boolean isEnabled(@NonNull Context context)
{
return !SubwayManager.isEnabled() && TrafficManager.INSTANCE.isEnabled();
}
@Override
public void setEnabled(@NonNull Context context, boolean isEnabled)
{
TrafficManager.INSTANCE.setEnabled(isEnabled);
}
},
SUBWAY {
@Override
public boolean isEnabled(@NonNull Context context)
{
return SubwayManager.isEnabled();
}
@Override
public void setEnabled(@NonNull Context context, boolean isEnabled)
{
SubwayManager.setEnabled(isEnabled);
}
},
ISOLINES {
@Override
public boolean isEnabled(@NonNull Context context)
{
return IsolinesManager.isEnabled();
}
@Override
public void setEnabled(@NonNull Context context, boolean isEnabled)
{
IsolinesManager.setEnabled(isEnabled);
}
},
OUTDOORS {
@Override
public boolean isEnabled(@NonNull Context context)
{
return Framework.nativeIsOutdoorsLayerEnabled();
}
@Override
public void setEnabled(@NonNull Context context, boolean isEnabled)
{
Framework.nativeSetOutdoorsLayerEnabled(isEnabled);
// TODO: ThemeSwitcher is outside sdk package. Properly fix dependencies
// ThemeSwitcher.INSTANCE.restart(true);
}
};
public abstract boolean isEnabled(@NonNull Context context);
public abstract void setEnabled(@NonNull Context context, boolean isEnabled);
}

View File

@@ -0,0 +1,8 @@
package app.organicmaps.sdk.maplayer.isolines;
import androidx.annotation.NonNull;
public interface IsolinesErrorDialogListener
{
void onStateChanged(@NonNull IsolinesState type);
}

View File

@@ -0,0 +1,52 @@
package app.organicmaps.sdk.maplayer.isolines;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.Framework;
public class IsolinesManager
{
@NonNull
private final OnIsolinesChangedListener mListener = new OnIsolinesChangedListener();
static public boolean isEnabled()
{
return Framework.nativeIsIsolinesLayerEnabled();
}
private void registerListener()
{
nativeAddListener(mListener);
}
static public void setEnabled(boolean isEnabled)
{
if (isEnabled == isEnabled())
return;
Framework.nativeSetIsolinesLayerEnabled(isEnabled);
}
public void initialize()
{
registerListener();
}
private static native void nativeAddListener(@NonNull OnIsolinesChangedListener listener);
private static native void nativeRemoveListener(@NonNull OnIsolinesChangedListener listener);
private static native boolean nativeShouldShowNotification();
public void attach(@NonNull IsolinesErrorDialogListener listener)
{
mListener.attach(listener);
}
public void detach()
{
mListener.detach();
}
public boolean shouldShowNotification()
{
return nativeShouldShowNotification();
}
}

View File

@@ -0,0 +1,9 @@
package app.organicmaps.sdk.maplayer.isolines;
public enum IsolinesState
{
DISABLED,
ENABLED,
EXPIREDDATA,
NODATA;
}

View File

@@ -0,0 +1,31 @@
package app.organicmaps.sdk.maplayer.isolines;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
class OnIsolinesChangedListener
{
@Nullable
private IsolinesErrorDialogListener mListener;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public void onStateChanged(int type)
{
if (mListener == null)
return;
mListener.onStateChanged(IsolinesState.values()[type]);
}
public void attach(@NonNull IsolinesErrorDialogListener listener)
{
mListener = listener;
}
public void detach()
{
mListener = null;
}
}

View File

@@ -0,0 +1,33 @@
package app.organicmaps.sdk.maplayer.subway;
import android.content.Context;
import androidx.annotation.Keep;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
interface OnTransitSchemeChangedListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
void onTransitStateChanged(int type);
class Default implements OnTransitSchemeChangedListener
{
@NonNull
private final Context mContext;
Default(@NonNull Context context)
{
mContext = context;
}
@Override
public void onTransitStateChanged(int index)
{
TransitSchemeState state = TransitSchemeState.values()[index];
state.activate(mContext);
}
}
}

View File

@@ -0,0 +1,44 @@
package app.organicmaps.sdk.maplayer.subway;
import android.content.Context;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.Framework;
public class SubwayManager
{
@NonNull
private final OnTransitSchemeChangedListener mSchemeChangedListener;
public SubwayManager(@NonNull Context context)
{
mSchemeChangedListener = new OnTransitSchemeChangedListener.Default(context);
}
static public void setEnabled(boolean isEnabled)
{
if (isEnabled == isEnabled())
return;
Framework.nativeSetTransitSchemeEnabled(isEnabled);
Framework.nativeSaveSettingSchemeEnabled(isEnabled);
}
static public boolean isEnabled()
{
return Framework.nativeIsTransitSchemeEnabled();
}
public void initialize()
{
registerListener();
}
private void registerListener()
{
nativeAddListener(mSchemeChangedListener);
}
private static native void nativeAddListener(@NonNull OnTransitSchemeChangedListener listener);
private static native void nativeRemoveListener(@NonNull OnTransitSchemeChangedListener listener);
}

View File

@@ -0,0 +1,24 @@
package app.organicmaps.sdk.maplayer.subway;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.NonNull;
import app.organicmaps.R;
enum TransitSchemeState
{
DISABLED,
ENABLED,
NO_DATA {
@Override
public void activate(@NonNull Context context)
{
Toast.makeText(context, R.string.subway_data_unavailable, Toast.LENGTH_SHORT).show();
}
};
void activate(@NonNull Context context)
{
/* Do nothing by default */
}
}

View File

@@ -0,0 +1,148 @@
package app.organicmaps.sdk.maplayer.traffic;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.util.log.Logger;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("unused")
@MainThread
public enum TrafficManager {
INSTANCE;
private static final String TAG = TrafficManager.class.getSimpleName();
@NonNull
private final TrafficState.StateChangeListener mStateChangeListener = new TrafficStateListener();
@NonNull
private TrafficState mState = TrafficState.DISABLED;
@NonNull
private final List<TrafficCallback> mCallbacks = new ArrayList<>();
private boolean mInitialized = false;
public void initialize()
{
Logger.d(TAG, "Initialization of traffic manager and setting the listener for traffic state changes");
TrafficState.nativeSetListener(mStateChangeListener);
mInitialized = true;
}
public void toggle()
{
checkInitialization();
if (isEnabled())
disable();
else
enable();
}
private void enable()
{
Logger.d(TAG, "Enable traffic");
TrafficState.nativeEnable();
}
private void disable()
{
checkInitialization();
Logger.d(TAG, "Disable traffic");
TrafficState.nativeDisable();
}
public boolean isEnabled()
{
checkInitialization();
return TrafficState.nativeIsEnabled();
}
public void attach(@NonNull TrafficCallback callback)
{
checkInitialization();
if (mCallbacks.contains(callback))
{
throw new IllegalStateException("A callback '" + callback
+ "' is already attached. Check that the 'detachAll' method was called.");
}
Logger.d(TAG, "Attach callback '" + callback + "'");
mCallbacks.add(callback);
postPendingState();
}
private void postPendingState()
{
mStateChangeListener.onTrafficStateChanged(mState.ordinal());
}
public void detachAll()
{
checkInitialization();
if (mCallbacks.isEmpty())
{
Logger.w(TAG,
"There are no attached callbacks. Invoke the 'detachAll' method "
+ "only when it's really needed!",
new Throwable());
return;
}
for (TrafficCallback callback : mCallbacks)
Logger.d(TAG, "Detach callback '" + callback + "'");
mCallbacks.clear();
}
private void checkInitialization()
{
if (!mInitialized)
throw new AssertionError("Traffic manager is not initialized!");
}
public void setEnabled(boolean enabled)
{
checkInitialization();
if (isEnabled() == enabled)
return;
if (enabled)
enable();
else
disable();
}
private class TrafficStateListener implements TrafficState.StateChangeListener
{
@Override
@MainThread
public void onTrafficStateChanged(int index)
{
TrafficState newTrafficState = TrafficState.values()[index];
Logger.d(TAG, "onTrafficStateChanged current state = " + mState + " new value = " + newTrafficState);
if (mState == newTrafficState)
return;
mState = newTrafficState;
mState.activate(mCallbacks);
}
}
public interface TrafficCallback
{
void onEnabled();
void onDisabled();
void onWaitingData();
void onOutdated();
void onNetworkError();
void onNoData();
void onExpiredData();
void onExpiredApp();
}
}

View File

@@ -0,0 +1,101 @@
package app.organicmaps.sdk.maplayer.traffic;
import androidx.annotation.Keep;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import java.util.List;
@SuppressWarnings("unused")
enum TrafficState {
DISABLED {
@Override
protected void activateInternal(@NonNull TrafficManager.TrafficCallback callback)
{
callback.onDisabled();
}
},
ENABLED {
@Override
protected void activateInternal(@NonNull TrafficManager.TrafficCallback callback)
{
callback.onEnabled();
}
},
WAITING_DATA {
@Override
protected void activateInternal(@NonNull TrafficManager.TrafficCallback callback)
{
callback.onWaitingData();
}
},
OUTDATED {
@Override
protected void activateInternal(@NonNull TrafficManager.TrafficCallback callback)
{
callback.onOutdated();
}
},
NO_DATA {
@Override
protected void activateInternal(@NonNull TrafficManager.TrafficCallback callback)
{
callback.onNoData();
}
},
NETWORK_ERROR {
@Override
protected void activateInternal(@NonNull TrafficManager.TrafficCallback callback)
{
callback.onNetworkError();
}
},
EXPIRED_DATA {
@Override
protected void activateInternal(@NonNull TrafficManager.TrafficCallback callback)
{
callback.onExpiredData();
}
},
EXPIRED_APP {
@Override
protected void activateInternal(@NonNull TrafficManager.TrafficCallback callback)
{
callback.onExpiredApp();
}
};
public void activate(@NonNull List<TrafficManager.TrafficCallback> trafficCallbacks)
{
for (TrafficManager.TrafficCallback callback : trafficCallbacks)
activateInternal(callback);
}
protected abstract void activateInternal(@NonNull TrafficManager.TrafficCallback callback);
interface StateChangeListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
void onTrafficStateChanged(int state);
}
@MainThread
static native void nativeSetListener(@NonNull StateChangeListener listener);
static native void nativeRemoveListener();
static native void nativeEnable();
static native void nativeDisable();
static native boolean nativeIsEnabled();
}

View File

@@ -0,0 +1,71 @@
package app.organicmaps.sdk.routing;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.R;
/**
* IMPORTANT : Order of enum values MUST BE the same as native CarDirection enum.
*/
public enum CarDirection
{
NO_TURN(R.drawable.ic_turn_straight, 0),
GO_STRAIGHT(R.drawable.ic_turn_straight, 0),
TURN_RIGHT(R.drawable.ic_turn_right, R.drawable.ic_then_right),
TURN_SHARP_RIGHT(R.drawable.ic_turn_right_sharp, R.drawable.ic_then_right_sharp),
TURN_SLIGHT_RIGHT(R.drawable.ic_turn_right_slight, R.drawable.ic_then_right_slight),
TURN_LEFT(R.drawable.ic_turn_left, R.drawable.ic_then_left),
TURN_SHARP_LEFT(R.drawable.ic_turn_left_sharp, R.drawable.ic_then_left_sharp),
TURN_SLIGHT_LEFT(R.drawable.ic_turn_left_slight, R.drawable.ic_then_left_slight),
U_TURN_LEFT(R.drawable.ic_turn_uleft, R.drawable.ic_then_uleft),
U_TURN_RIGHT(R.drawable.ic_turn_uright, R.drawable.ic_then_uright),
ENTER_ROUND_ABOUT(R.drawable.ic_turn_round, R.drawable.ic_then_round),
LEAVE_ROUND_ABOUT(R.drawable.ic_turn_round, R.drawable.ic_then_round),
STAY_ON_ROUND_ABOUT(R.drawable.ic_turn_round, R.drawable.ic_then_round),
START_AT_THE_END_OF_STREET(0, 0),
REACHED_YOUR_DESTINATION(R.drawable.ic_turn_finish, R.drawable.ic_then_finish),
EXIT_HIGHWAY_TO_LEFT(R.drawable.ic_exit_highway_to_left, R.drawable.ic_then_exit_highway_to_left),
EXIT_HIGHWAY_TO_RIGHT(R.drawable.ic_exit_highway_to_right, R.drawable.ic_then_exit_highway_to_right);
private final int mTurnRes;
private final int mNextTurnRes;
CarDirection(@DrawableRes int mainResId, @DrawableRes int nextResId)
{
mTurnRes = mainResId;
mNextTurnRes = nextResId;
}
public int getTurnRes()
{
return mTurnRes;
}
public void setTurnDrawable(@NonNull ImageView imageView)
{
imageView.setImageResource(mTurnRes);
imageView.setRotation(0.0f);
}
public void setNextTurnDrawable(@NonNull ImageView imageView)
{
imageView.setImageResource(mNextTurnRes);
}
public boolean containsNextTurn()
{
return mNextTurnRes != 0;
}
public static boolean isRoundAbout(CarDirection turn)
{
return turn == ENTER_ROUND_ABOUT || turn == LEAVE_ROUND_ABOUT || turn == STAY_ON_ROUND_ABOUT;
}
}

View File

@@ -0,0 +1,18 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.Keep;
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public final class JunctionInfo
{
public final double mLat;
public final double mLon;
private JunctionInfo(double lat, double lon)
{
mLat = lat;
mLon = lon;
}
}

View File

@@ -0,0 +1,32 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.DrawableRes;
import app.organicmaps.sdk.R;
/**
* IMPORTANT : Order of enum values MUST BE the same
* with native LaneWay enum (see routing/turns.hpp for details).
* Information for every lane is composed of some number values below.
* For example, a lane may have THROUGH and RIGHT values.
*/
public enum LaneWay
{
NONE(R.drawable.ic_turn_straight),
REVERSE(R.drawable.ic_turn_uleft),
SHARP_LEFT(R.drawable.ic_turn_left_sharp),
LEFT(R.drawable.ic_turn_left),
SLIGHT_LEFT(R.drawable.ic_turn_left_slight),
MERGE_TO_RIGHT(R.drawable.ic_turn_right_slight),
THROUGH(R.drawable.ic_turn_straight),
MERGE_TO_LEFT(R.drawable.ic_turn_left_slight),
SLIGHT_RIGHT(R.drawable.ic_turn_right_slight),
RIGHT(R.drawable.ic_turn_right),
SHARP_RIGHT(R.drawable.ic_turn_right_sharp);
public final int mTurnRes;
LaneWay(@DrawableRes int turnRes)
{
mTurnRes = turnRes;
}
}

View File

@@ -0,0 +1,42 @@
package app.organicmaps.sdk.routing;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.R;
public enum PedestrianTurnDirection
{
NO_TURN(R.drawable.ic_turn_straight, 0),
GO_STRAIGHT(R.drawable.ic_turn_straight, 0),
TURN_RIGHT(R.drawable.ic_turn_right, R.drawable.ic_then_right),
TURN_LEFT(R.drawable.ic_turn_left, R.drawable.ic_then_left),
REACHED_YOUR_DESTINATION(R.drawable.ic_turn_finish, R.drawable.ic_then_finish);
private final int mTurnRes;
private final int mNextTurnRes;
PedestrianTurnDirection(@DrawableRes int mainResId, @DrawableRes int nextResId)
{
mTurnRes = mainResId;
mNextTurnRes = nextResId;
}
public void setTurnDrawable(@NonNull ImageView imageView)
{
imageView.setImageResource(mTurnRes);
imageView.setRotation(0.0f);
}
public void setNextTurnDrawable(@NonNull ImageView imageView)
{
imageView.setImageResource(mNextTurnRes);
}
public boolean containsNextTurn()
{
return mNextTurnRes != 0;
}
}

View File

@@ -0,0 +1,23 @@
package app.organicmaps.sdk.routing;
public interface ResultCodes
{
// Codes correspond to native routing::RouterResultCode in routing/routing_callbacks.hpp
int NO_ERROR = 0;
int CANCELLED = 1;
int NO_POSITION = 2;
int INCONSISTENT_MWM_ROUTE = 3;
int ROUTING_FILE_NOT_EXIST = 4;
int START_POINT_NOT_FOUND = 5;
int END_POINT_NOT_FOUND = 6;
int DIFFERENT_MWM = 7;
int ROUTE_NOT_FOUND = 8;
int NEED_MORE_MAPS = 9;
int INTERNAL_ERROR = 10;
int FILE_TOO_OLD = 11;
int INTERMEDIATE_POINT_NOT_FOUND = 12;
int TRANSIT_ROUTE_NOT_FOUND_NO_NETWORK = 13;
int TRANSIT_ROUTE_NOT_FOUND_TOO_LONG_PEDESTRIAN = 14;
int ROUTE_NOT_FOUND_REDRESS_ROUTE_ERROR = 15;
int HAS_WARNINGS = 16;
}

View File

@@ -0,0 +1,53 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
/**
* Represents RouteMarkData from core.
*/
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public final class RouteMarkData
{
@Nullable
public final String mTitle;
@Nullable
public final String mSubtitle;
public RouteMarkType mPointType;
public int mIntermediateIndex;
public final boolean mIsVisible;
public final boolean mIsMyPosition;
public final boolean mIsPassed;
public final double mLat;
public final double mLon;
private RouteMarkData(@Nullable String title, @Nullable String subtitle, int pointType, int intermediateIndex,
boolean isVisible, boolean isMyPosition, boolean isPassed, double lat, double lon)
{
this(title, subtitle, RouteMarkType.values()[pointType], intermediateIndex, isVisible, isMyPosition, isPassed, lat,
lon);
}
public RouteMarkData(@Nullable String title, @Nullable String subtitle, RouteMarkType pointType,
int intermediateIndex, boolean isVisible, boolean isMyPosition, boolean isPassed, double lat,
double lon)
{
mTitle = title;
mSubtitle = subtitle;
mPointType = pointType;
mIntermediateIndex = intermediateIndex;
mIsVisible = isVisible;
mIsMyPosition = isMyPosition;
mIsPassed = isPassed;
mLat = lat;
mLon = lon;
}
public boolean equals(RouteMarkData other)
{
return mTitle != null && other.mTitle != null && mTitle.compareTo(other.mTitle) == 0 && mSubtitle != null
&& other.mSubtitle != null && mSubtitle.compareTo(other.mSubtitle) == 0;
}
}

View File

@@ -0,0 +1,8 @@
package app.organicmaps.sdk.routing;
public enum RouteMarkType
{
Start,
Intermediate,
Finish
}

View File

@@ -0,0 +1,85 @@
package app.organicmaps.sdk.routing;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public final class RoutePointInfo implements Parcelable
{
public static final Creator<RoutePointInfo> CREATOR = new Creator<>() {
@Override
public RoutePointInfo createFromParcel(Parcel in)
{
return new RoutePointInfo(in);
}
@Override
public RoutePointInfo[] newArray(int size)
{
return new RoutePointInfo[size];
}
};
public final RouteMarkType mMarkType;
public final int mIntermediateIndex;
// Called from JNI.
@Keep
public RoutePointInfo(int markType, int intermediateIndex)
{
switch (markType)
{
case 0: mMarkType = RouteMarkType.Start; break;
case 1: mMarkType = RouteMarkType.Intermediate; break;
case 2: mMarkType = RouteMarkType.Finish; break;
default: throw new IllegalArgumentException("Mark type is not valid = " + markType);
}
mIntermediateIndex = intermediateIndex;
}
private RoutePointInfo(@NonNull RouteMarkType markType, int intermediateIndex)
{
mMarkType = markType;
mIntermediateIndex = intermediateIndex;
}
private RoutePointInfo(@NonNull Parcel in)
{
// noinspection WrongConstant
this(RouteMarkType.values()[in.readInt()] /* mMarkType */, in.readInt() /* mIntermediateIndex */);
}
boolean isIntermediatePoint()
{
return mMarkType == RouteMarkType.Intermediate;
}
boolean isFinishPoint()
{
return mMarkType == RouteMarkType.Finish;
}
boolean isStartPoint()
{
return mMarkType == RouteMarkType.Start;
}
@Override
public int describeContents()
{
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags)
{
dest.writeInt(mMarkType.ordinal());
dest.writeInt(mIntermediateIndex);
}
}

View File

@@ -0,0 +1,6 @@
package app.organicmaps.sdk.routing;
public enum RouteRecommendationType
{
RebuildAfterPointsLoading
}

View File

@@ -0,0 +1,866 @@
package app.organicmaps.sdk.routing;
import android.text.TextUtils;
import androidx.annotation.IntRange;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import app.organicmaps.sdk.Framework;
import app.organicmaps.sdk.Router;
import app.organicmaps.sdk.bookmarks.data.FeatureId;
import app.organicmaps.sdk.bookmarks.data.MapObject;
import app.organicmaps.sdk.location.LocationHelper;
import app.organicmaps.sdk.util.concurrency.UiThread;
import app.organicmaps.sdk.util.log.Logger;
import app.organicmaps.sdk.widget.placepage.CoordinatesFormat;
@androidx.annotation.UiThread
public class RoutingController
{
private static final String TAG = RoutingController.class.getSimpleName();
private enum State
{
NONE,
PREPARE,
NAVIGATION
}
public enum BuildState
{
NONE,
BUILDING,
BUILT,
ERROR
}
public interface Container
{
default void showRoutePlan(boolean show, @Nullable Runnable completionListener) {}
default void showNavigation(boolean show) {}
default void updateMenu() {}
default void onNavigationCancelled() {}
default void onNavigationStarted() {}
default void onPlanningCancelled() {}
default void onPlanningStarted() {}
default void onAddedStop() {}
default void onRemovedStop() {}
default void onResetToPlanningState() {}
default void onBuiltRoute() {}
default void onDrivingOptionsWarning() {}
default void onCommonBuildError(int lastResultCode, @NonNull String[] lastMissingMaps) {}
default void onDrivingOptionsBuildError() {}
/**
* @param progress progress to be displayed.
* */
default void updateBuildProgress(@IntRange(from = 0, to = 100) int progress, Router router) {}
default void onStartRouteBuilding() {}
}
private static final RoutingController sInstance = new RoutingController();
@Nullable
private Container mContainer;
private BuildState mBuildState = BuildState.NONE;
private State mState = State.NONE;
@Nullable
private RouteMarkType mWaitingPoiPickType = null;
private int mLastBuildProgress;
private Router mLastRouterType;
private boolean mHasContainerSavedState;
private boolean mContainsCachedResult;
private int mLastResultCode;
private String[] mLastMissingMaps;
@Nullable
private RoutingInfo mCachedRoutingInfo;
@Nullable
private TransitRouteInfo mCachedTransitRouteInfo;
private int mInvalidRoutePointsTransactionId;
private int mRemovingIntermediatePointsTransactionId;
@SuppressWarnings("FieldCanBeLocal")
private final RoutingListener mRoutingListener = new RoutingListener() {
@MainThread
@Override
public void onRoutingEvent(final int resultCode, @Nullable final String[] missingMaps)
{
Logger.d(TAG, "onRoutingEvent(resultCode: " + resultCode + ")");
mLastResultCode = resultCode;
mLastMissingMaps = missingMaps;
mContainsCachedResult = true;
if (mLastResultCode == ResultCodes.NO_ERROR || resultCode == ResultCodes.NEED_MORE_MAPS)
{
onBuiltRoute();
}
else if (mLastResultCode == ResultCodes.HAS_WARNINGS)
{
onBuiltRoute();
if (mContainer != null)
mContainer.onDrivingOptionsWarning();
}
processRoutingEvent();
}
};
private void onBuiltRoute()
{
mCachedRoutingInfo = Framework.nativeGetRouteFollowingInfo();
if (mLastRouterType == Router.Transit)
mCachedTransitRouteInfo = Framework.nativeGetTransitRouteInfo();
setBuildState(BuildState.BUILT);
mLastBuildProgress = 100;
if (mContainer != null)
mContainer.onBuiltRoute();
}
private final RoutingProgressListener mRoutingProgressListener = progress ->
{
mLastBuildProgress = (int) progress;
updateProgress();
};
private final 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 (isDrivingOptionsBuildError())
mContainer.onDrivingOptionsWarning();
if (mLastResultCode == ResultCodes.NO_ERROR || mLastResultCode == ResultCodes.HAS_WARNINGS)
{
updatePlan();
return;
}
if (mLastResultCode == ResultCodes.CANCELLED)
{
setBuildState(BuildState.NONE);
updatePlan();
return;
}
if (mLastResultCode != ResultCodes.NEED_MORE_MAPS)
{
setBuildState(BuildState.ERROR);
mLastBuildProgress = 0;
updateProgress();
}
if (isDrivingOptionsBuildError())
mContainer.onDrivingOptionsBuildError();
else
mContainer.onCommonBuildError(mLastResultCode, mLastMissingMaps);
}
private boolean isDrivingOptionsBuildError()
{
return mLastResultCode != ResultCodes.NEED_MORE_MAPS && RoutingOptions.hasAnyOptions() && !isRulerRouterType();
}
private void setState(State newState)
{
Logger.d(TAG, "[S] State: " + mState + " -> " + newState + ", BuildState: " + mBuildState);
mState = newState;
if (mContainer != null)
mContainer.updateMenu();
}
private void setBuildState(BuildState newState)
{
Logger.d(TAG, "[B] State: " + mState + ", BuildState: " + mBuildState + " -> " + newState);
mBuildState = newState;
final MapObject startPoint = getStartPoint();
if (mBuildState == BuildState.BUILT && (startPoint == null || !startPoint.isMyPosition()))
Framework.nativeDisableFollowing();
if (mContainer != null)
mContainer.updateMenu();
}
private void updateProgress()
{
if (mContainer != null)
mContainer.updateBuildProgress(mLastBuildProgress, mLastRouterType);
}
private void showRoutePlan()
{
showRoutePlan(null, null);
}
private void showRoutePlan(final @Nullable MapObject startPoint, final @Nullable MapObject endPoint)
{
if (mContainer != null)
{
mContainer.showRoutePlan(true, () -> {
if (startPoint == null || endPoint == null)
updatePlan();
else
build();
});
}
}
public void attach(@NonNull Container container)
{
mContainer = container;
}
public void initialize(@NonNull LocationHelper locationHelper)
{
mLastRouterType = Router.getLastUsed();
mInvalidRoutePointsTransactionId = Framework.nativeInvalidRoutePointsTransactionId();
mRemovingIntermediatePointsTransactionId = mInvalidRoutePointsTransactionId;
Framework.nativeSetRoutingListener(mRoutingListener);
Framework.nativeSetRouteProgressListener(mRoutingProgressListener);
Framework.nativeSetRoutingRecommendationListener(recommendation -> UiThread.run(() -> {
if (recommendation == RouteRecommendationType.RebuildAfterPointsLoading)
setStartPoint(locationHelper.getMyPosition());
}));
Framework.nativeSetRoutingLoadPointsListener(mRoutingLoadPointsListener);
}
public void detach()
{
mContainer = null;
}
@MainThread
public void restore()
{
mHasContainerSavedState = false;
if (isPlanning())
showRoutePlan();
if (mContainer != null)
{
mContainer.showNavigation(isNavigating());
mContainer.updateMenu();
}
processRoutingEvent();
}
public void onSaveState()
{
mHasContainerSavedState = true;
}
private void build()
{
Framework.nativeRemoveRoute();
Logger.d(TAG, "build");
mLastBuildProgress = 0;
setBuildState(BuildState.BUILDING);
if (mContainer != null)
mContainer.onStartRouteBuilding();
updatePlan();
Framework.nativeBuildRoute();
}
public void restoreRoute()
{
Framework.nativeLoadRoutePoints();
}
public boolean hasSavedRoute()
{
return Framework.nativeHasSavedRoutePoints();
}
public void saveRoute()
{
if (isNavigating() || (isPlanning() && isBuilt()))
Framework.nativeSaveRoutePoints();
}
public void deleteSavedRoute()
{
Framework.nativeDeleteSavedRoutePoints();
}
public void rebuildLastRoute()
{
setState(State.NONE);
setBuildState(BuildState.NONE);
prepare(getStartPoint(), getEndPoint());
}
public void prepare(@Nullable MapObject startPoint, @Nullable MapObject endPoint)
{
Logger.d(TAG, "prepare (" + (endPoint == null ? "route)" : "p2p)"));
initLastRouteType(startPoint, endPoint);
prepare(startPoint, endPoint, mLastRouterType);
}
private void initLastRouteType(@Nullable MapObject startPoint, @Nullable MapObject endPoint)
{
if (startPoint != null && endPoint != null)
mLastRouterType = Router.getBest(startPoint.getLat(), startPoint.getLon(), endPoint.getLat(), endPoint.getLon());
}
public void prepare(final @Nullable MapObject startPoint, final @Nullable MapObject endPoint, Router routerType)
{
cancel();
setState(State.PREPARE);
mLastRouterType = routerType;
Router.set(mLastRouterType);
if (startPoint != null || endPoint != null)
setPointsInternal(startPoint, endPoint);
startPlanning(startPoint, endPoint);
}
public void start()
{
Logger.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();
setState(State.NAVIGATION);
cancelPlanning(false);
startNavigation();
Framework.nativeFollowRoute();
}
public void addStop(@NonNull MapObject mapObject)
{
addRoutePoint(RouteMarkType.Intermediate, mapObject);
build();
if (mContainer != null)
mContainer.onAddedStop();
resetToPlanningStateIfNavigating();
}
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);
build();
if (mContainer != null)
mContainer.onRemovedStop();
resetToPlanningStateIfNavigating();
}
public void launchPlanning()
{
build();
setState(State.PREPARE);
startPlanning();
if (mContainer != null)
mContainer.updateMenu();
if (mContainer != null)
mContainer.onResetToPlanningState();
}
/**
* @return False if not navigating, true otherwise
*/
public boolean resetToPlanningStateIfNavigating()
{
if (isNavigating())
{
build();
setState(State.PREPARE);
cancelNavigation(false);
startPlanning();
if (mContainer != null)
mContainer.updateMenu();
if (mContainer != null)
mContainer.onResetToPlanningState();
return true;
}
return false;
}
@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();
}
public boolean isRoutePoint(@NonNull MapObject mapObject)
{
return mapObject.getRoutePointInfo() != null;
}
private void updatePlan()
{
updateProgress();
}
private void cancelInternal()
{
Logger.d(TAG, "cancelInternal");
mWaitingPoiPickType = null;
setBuildState(BuildState.NONE);
setState(State.NONE);
applyRemovingIntermediatePointsTransaction();
Framework.nativeDeleteSavedRoutePoints();
Framework.nativeCloseRouting();
}
public boolean cancel()
{
if (isPlanning())
{
Logger.d(TAG, "cancel: planning");
cancelInternal();
cancelPlanning(true);
return true;
}
if (isNavigating())
{
Logger.d(TAG, "cancel: navigating");
cancelInternal();
cancelNavigation(true);
if (mContainer != null)
{
mContainer.updateMenu();
}
return true;
}
Logger.d(TAG, "cancel: none");
return false;
}
private void startPlanning()
{
if (mContainer != null)
{
showRoutePlan();
}
}
private void startPlanning(final @Nullable MapObject startPoint, final @Nullable MapObject endPoint)
{
if (mContainer != null)
{
showRoutePlan(startPoint, endPoint);
mContainer.onPlanningStarted();
}
}
private void cancelPlanning(boolean fireEvent)
{
if (mContainer != null)
{
mContainer.showRoutePlan(false, null);
if (fireEvent)
mContainer.onPlanningCancelled();
}
}
private void startNavigation()
{
if (mContainer != null)
{
mContainer.showNavigation(true);
mContainer.onNavigationStarted();
}
}
private void cancelNavigation(boolean fireEvent)
{
if (mContainer != null)
{
mContainer.showNavigation(false);
if (fireEvent)
mContainer.onNavigationCancelled();
}
}
public boolean isPlanning()
{
return mState == State.PREPARE;
}
public boolean isTransitType()
{
return mLastRouterType == Router.Transit;
}
public boolean isVehicleRouterType()
{
return mLastRouterType == Router.Vehicle;
}
public boolean isRulerRouterType()
{
return mLastRouterType == Router.Ruler;
}
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(@NonNull RouteMarkType pointType)
{
mWaitingPoiPickType = pointType;
}
public boolean isWaitingPoiPick()
{
return mWaitingPoiPickType != null;
}
public BuildState getBuildState()
{
return mBuildState;
}
@Nullable
public MapObject getStartPoint()
{
return getStartOrEndPointByType(RouteMarkType.Start);
}
@Nullable
public MapObject getEndPoint()
{
return getStartOrEndPointByType(RouteMarkType.Finish);
}
@Nullable
private MapObject getStartOrEndPointByType(@NonNull RouteMarkType 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 == RouteMarkType.Start)
return toMapObject(points[0]);
if (type == RouteMarkType.Finish)
return toMapObject(points[size - 1]);
return null;
}
@Nullable
public RoutingInfo getCachedRoutingInfo()
{
return mCachedRoutingInfo;
}
@Nullable
public TransitRouteInfo getCachedTransitInfo()
{
return mCachedTransitRouteInfo;
}
private void setPointsInternal(@Nullable MapObject startPoint, @Nullable MapObject endPoint)
{
final boolean hasStart = startPoint != null;
final boolean hasEnd = endPoint != null;
final boolean hasOnePointAtLeast = hasStart || hasEnd;
if (hasOnePointAtLeast)
applyRemovingIntermediatePointsTransaction();
if (hasStart)
addRoutePoint(RouteMarkType.Start, startPoint);
if (hasEnd)
addRoutePoint(RouteMarkType.Finish, endPoint);
if (hasOnePointAtLeast && mContainer != null)
mContainer.updateMenu();
}
public void checkAndBuildRoute()
{
if (isWaitingPoiPick())
showRoutePlan();
if (getStartPoint() != null && getEndPoint() != null)
build();
}
/**
* Sets starting point.
* <ul>
* <li>If {@code point} matches ending one and the starting point was set &mdash; swap points.
* <li>The same as the currently set starting point is skipped.
* </ul>
* Route starts to build if both points were set.
*
* @return {@code true} if the point was set.
*/
@SuppressWarnings("Duplicates")
public boolean setStartPoint(@Nullable MapObject point)
{
Logger.d(TAG, "setStartPoint");
MapObject startPoint = getStartPoint();
MapObject endPoint = getEndPoint();
boolean isSamePoint = MapObject.same(startPoint, point);
if (point != null)
{
applyRemovingIntermediatePointsTransaction();
addRoutePoint(RouteMarkType.Start, point);
startPoint = getStartPoint();
}
if (isSamePoint)
{
Logger.d(TAG, "setStartPoint: skip the same starting point");
return false;
}
if (point != null && point.sameAs(endPoint))
{
if (startPoint == null)
{
Logger.d(TAG, "setStartPoint: skip because starting point is empty");
return false;
}
Logger.d(TAG, "setStartPoint: swap with end point");
endPoint = startPoint;
}
startPoint = point;
setPointsInternal(startPoint, endPoint);
checkAndBuildRoute();
return true;
}
/**
* Sets ending point.
* <ul>
* <li>If {@code point} is the same as starting point &mdash; swap points if ending point is set, skip otherwise.
* <li>Set starting point to MyPosition if it was not set before.
* </ul>
* Route starts to build if both points were set.
*
* @return {@code true} if the point was set.
*/
@SuppressWarnings("Duplicates")
public boolean setEndPoint(@Nullable MapObject point)
{
Logger.d(TAG, "setEndPoint");
MapObject startPoint = getStartPoint();
MapObject endPoint = getEndPoint();
boolean isSamePoint = MapObject.same(endPoint, point);
if (point != null)
{
applyRemovingIntermediatePointsTransaction();
addRoutePoint(RouteMarkType.Finish, point);
endPoint = getEndPoint();
}
if (isSamePoint)
return false;
if (point != null && point.sameAs(startPoint))
{
if (endPoint == null)
return false;
startPoint = endPoint;
}
endPoint = point;
setPointsInternal(startPoint, endPoint);
checkAndBuildRoute();
return true;
}
private static void addRoutePoint(@NonNull RouteMarkType type, @NonNull MapObject point)
{
Pair<String, String> description = getDescriptionForPoint(point);
Framework.nativeAddRoutePoint(description.first /* title */, description.second /* subtitle */, type,
0 /* intermediateIndex */, point.isMyPosition(), point.getLat(), point.getLon(),
true /* reorderIntermediatePoints */);
}
@NonNull
private static Pair<String, String> getDescriptionForPoint(@NonNull MapObject point)
{
String title, subtitle = "";
if (!TextUtils.isEmpty(point.getTitle()))
{
title = point.getTitle();
subtitle = point.getSubtitle();
}
else
{
if (!TextUtils.isEmpty(point.getSubtitle()))
{
title = point.getSubtitle();
}
else if (!TextUtils.isEmpty(point.getAddress()))
{
title = point.getAddress();
}
else
{
title = Framework.nativeFormatLatLon(point.getLat(), point.getLon(), CoordinatesFormat.LatLonDecimal.getId());
}
}
return new Pair<>(title, subtitle);
}
public void swapPoints()
{
Logger.d(TAG, "swapPoints");
MapObject startPoint = getStartPoint();
MapObject endPoint = getEndPoint();
MapObject point = startPoint;
startPoint = endPoint;
endPoint = point;
setPointsInternal(startPoint, endPoint);
checkAndBuildRoute();
if (mContainer != null)
mContainer.updateMenu();
}
public void setRouterType(Router router)
{
Logger.d(TAG, "setRouterType: " + mLastRouterType + " -> " + router);
// Repeating tap on Taxi icon should trigger the route building always,
// because it may be "No internet connection, try later" case
if (router == mLastRouterType)
return;
mLastRouterType = router;
Router.set(router);
cancelRemovingIntermediatePointsTransaction();
if (getStartPoint() != null && getEndPoint() != null)
build();
}
public Router getLastRouterType()
{
return mLastRouterType;
}
private void cancelRemovingIntermediatePointsTransaction()
{
if (mRemovingIntermediatePointsTransactionId == mInvalidRoutePointsTransactionId)
return;
Framework.nativeCancelRoutePointsTransaction(mRemovingIntermediatePointsTransactionId);
mRemovingIntermediatePointsTransactionId = mInvalidRoutePointsTransactionId;
}
private void applyRemovingIntermediatePointsTransaction()
{
// We have to apply removing intermediate points transaction each time
// we add/remove route points in the taxi mode.
if (mRemovingIntermediatePointsTransactionId == mInvalidRoutePointsTransactionId)
return;
Framework.nativeApplyRoutePointsTransaction(mRemovingIntermediatePointsTransactionId);
mRemovingIntermediatePointsTransactionId = mInvalidRoutePointsTransactionId;
}
public void onPoiSelected(@Nullable MapObject point)
{
if (!isWaitingPoiPick())
return;
if (mWaitingPoiPickType != RouteMarkType.Start && mWaitingPoiPickType != RouteMarkType.Finish)
throw new AssertionError("Only start and finish points can be added through search!");
if (point != null)
{
if (mWaitingPoiPickType == RouteMarkType.Finish)
setEndPoint(point);
else
setStartPoint(point);
}
if (mContainer != null)
{
mContainer.updateMenu();
showRoutePlan();
}
mWaitingPoiPickType = null;
}
}

View File

@@ -0,0 +1,69 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.Keep;
import app.organicmaps.sdk.util.Distance;
// Called from JNI.
@Keep
@SuppressWarnings("unused")
public final class RoutingInfo
{
// Target (end point of route).
public final Distance distToTarget;
// Next turn.
public final Distance distToTurn;
public final int totalTimeInSeconds;
// Current street name.
public final String currentStreet;
// The next street name.
public final String nextStreet;
// The next next street name.
public final String nextNextStreet;
public final double completionPercent;
// For vehicle routing.
public final CarDirection carDirection;
public final CarDirection nextCarDirection;
public final int exitNum;
public final SingleLaneInfo[] lanes;
// For pedestrian routing.
public final PedestrianTurnDirection pedestrianTurnDirection;
// Current speed limit in meters per second.
// If no info about speed limit then speedLimitMps < 0.
public final double speedLimitMps;
private final boolean speedCamLimitExceeded;
private final boolean shouldPlayWarningSignal;
private RoutingInfo(Distance distToTarget, Distance distToTurn, String currentStreet, String nextStreet,
String nextNextStreet, double completionPercent, int vehicleTurnOrdinal,
int vehicleNextTurnOrdinal, int pedestrianTurnOrdinal, int exitNum, int totalTime,
SingleLaneInfo[] lanes, double speedLimitMps, boolean speedLimitExceeded,
boolean shouldPlayWarningSignal)
{
this.distToTarget = distToTarget;
this.distToTurn = distToTurn;
this.currentStreet = currentStreet;
this.nextStreet = nextStreet;
this.nextNextStreet = nextNextStreet;
this.totalTimeInSeconds = totalTime;
this.completionPercent = completionPercent;
this.carDirection = CarDirection.values()[vehicleTurnOrdinal];
this.nextCarDirection = CarDirection.values()[vehicleNextTurnOrdinal];
this.lanes = lanes;
this.exitNum = exitNum;
this.pedestrianTurnDirection = PedestrianTurnDirection.values()[pedestrianTurnOrdinal];
this.speedLimitMps = speedLimitMps;
this.speedCamLimitExceeded = speedLimitExceeded;
this.shouldPlayWarningSignal = shouldPlayWarningSignal;
}
public boolean isSpeedCamLimitExceeded()
{
return speedCamLimitExceeded;
}
public boolean shouldPlayWarningSignal()
{
return shouldPlayWarningSignal;
}
}

View File

@@ -0,0 +1,13 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.Keep;
import androidx.annotation.MainThread;
public interface RoutingListener
{
// Called from JNI
@Keep
@SuppressWarnings("unused")
@MainThread
void onRoutingEvent(int resultCode, String[] missingMaps);
}

View File

@@ -0,0 +1,11 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.Keep;
public interface RoutingLoadPointsListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
void onRoutePointsLoaded(boolean success);
}

View File

@@ -0,0 +1,56 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.NonNull;
import app.organicmaps.sdk.settings.RoadType;
import java.util.HashSet;
import java.util.Set;
public final class RoutingOptions
{
public static void addOption(@NonNull RoadType roadType)
{
nativeAddOption(roadType.ordinal());
}
public static void removeOption(@NonNull RoadType roadType)
{
nativeRemoveOption(roadType.ordinal());
}
public static boolean hasOption(@NonNull RoadType roadType)
{
return nativeHasOption(roadType.ordinal());
}
public static boolean hasAnyOptions()
{
for (RoadType each : RoadType.values())
{
if (hasOption(each))
return true;
}
return false;
}
@NonNull
public static Set<RoadType> getActiveRoadTypes()
{
Set<RoadType> roadTypes = new HashSet<>();
for (RoadType each : RoadType.values())
{
if (hasOption(each))
roadTypes.add(each);
}
return roadTypes;
}
private RoutingOptions() throws IllegalAccessException
{
throw new IllegalAccessException("RoutingOptions is a utility class and should not be instantiated");
}
private static native void nativeAddOption(int option);
private static native void nativeRemoveOption(int option);
private static native boolean nativeHasOption(int option);
}

View File

@@ -0,0 +1,13 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.Keep;
import androidx.annotation.MainThread;
public interface RoutingProgressListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
@MainThread
void onRouteBuildingProgress(float progress);
}

View File

@@ -0,0 +1,11 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.Keep;
public interface RoutingRecommendationListener
{
// Called from JNI.
@Keep
@SuppressWarnings("unused")
void onRecommend(RouteRecommendationType recommendation);
}

View File

@@ -0,0 +1,31 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.NonNull;
public final class SingleLaneInfo
{
public LaneWay[] mLane;
public boolean mIsActive;
public SingleLaneInfo(@NonNull byte[] laneOrdinals, boolean isActive)
{
mLane = new LaneWay[laneOrdinals.length];
final LaneWay[] values = LaneWay.values();
for (int i = 0; i < mLane.length; i++)
mLane[i] = values[laneOrdinals[i]];
mIsActive = isActive;
}
@NonNull
@Override
public String toString()
{
final int initialCapacity = 32;
StringBuilder sb = new StringBuilder(initialCapacity);
sb.append("Is the lane active? ").append(mIsActive).append(". The lane directions IDs are");
for (LaneWay i : mLane)
sb.append(" ").append(i.ordinal());
return sb.toString();
}
}

View File

@@ -0,0 +1,70 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Represents TransitRouteInfo from core.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public final class TransitRouteInfo
{
@NonNull
private final String mTotalDistance;
@NonNull
private final String mTotalDistanceUnits;
private final int mTotalTimeInSec;
@NonNull
private final String mTotalPedestrianDistance;
@NonNull
private final String mTotalPedestrianDistanceUnits;
private final int mTotalPedestrianTimeInSec;
@NonNull
private final TransitStepInfo[] mSteps;
private TransitRouteInfo(@NonNull String totalDistance, @NonNull String totalDistanceUnits, int totalTimeInSec,
@NonNull String totalPedestrianDistance, @NonNull String totalPedestrianDistanceUnits,
int totalPedestrianTimeInSec, @NonNull TransitStepInfo[] steps)
{
mTotalDistance = totalDistance;
mTotalDistanceUnits = totalDistanceUnits;
mTotalTimeInSec = totalTimeInSec;
mTotalPedestrianDistance = totalPedestrianDistance;
mTotalPedestrianDistanceUnits = totalPedestrianDistanceUnits;
mTotalPedestrianTimeInSec = totalPedestrianTimeInSec;
mSteps = steps;
}
@NonNull
public String getTotalPedestrianDistance()
{
return mTotalPedestrianDistance;
}
public int getTotalPedestrianTimeInSec()
{
return mTotalPedestrianTimeInSec;
}
@NonNull
public String getTotalPedestrianDistanceUnits()
{
return mTotalPedestrianDistanceUnits;
}
public int getTotalTime()
{
return mTotalTimeInSec;
}
@NonNull
public List<TransitStepInfo> getTransitSteps()
{
return new ArrayList<>(Arrays.asList(mSteps));
}
}

View File

@@ -0,0 +1,89 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Represents TransitStepInfo from core.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
public final class TransitStepInfo
{
@NonNull
private final TransitStepType mType;
@Nullable
private final String mDistance;
@Nullable
private final String mDistanceUnits;
private final int mTimeInSec;
@Nullable
private final String mNumber;
private final int mColor;
private final int mIntermediateIndex;
private TransitStepInfo(int type, @Nullable String distance, @Nullable String distanceUnits, int timeInSec,
@Nullable String number, int color, int intermediateIndex)
{
mType = TransitStepType.values()[type];
mDistance = distance;
mDistanceUnits = distanceUnits;
mTimeInSec = timeInSec;
mNumber = number;
mColor = color;
mIntermediateIndex = intermediateIndex;
}
@NonNull
public static TransitStepInfo intermediatePoint(int intermediateIndex)
{
return new TransitStepInfo(TransitStepType.INTERMEDIATE_POINT.ordinal(), null, null, 0, null, 0, intermediateIndex);
}
@NonNull
public static TransitStepInfo ruler(@NonNull String distance, @NonNull String distanceUnits)
{
return new TransitStepInfo(TransitStepType.RULER.ordinal(), distance, distanceUnits, 0, null, 0, -1);
}
@NonNull
public TransitStepType getType()
{
return mType;
}
@Nullable
public String getDistance()
{
return mDistance;
}
@Nullable
public String getDistanceUnits()
{
return mDistanceUnits;
}
public int getTimeInSec()
{
return mTimeInSec;
}
@Nullable
public String getNumber()
{
return mNumber;
}
public int getColor()
{
return mColor;
}
public int getIntermediateIndex()
{
return mIntermediateIndex;
}
}

View File

@@ -0,0 +1,30 @@
package app.organicmaps.sdk.routing;
import androidx.annotation.DrawableRes;
import app.organicmaps.sdk.R;
public enum TransitStepType
{
// A specific icon for different intermediate points is calculated dynamically in TransitStepView.
INTERMEDIATE_POINT(R.drawable.ic_20px_route_planning_walk),
PEDESTRIAN(R.drawable.ic_20px_route_planning_walk),
SUBWAY(app.organicmaps.R.drawable.ic_route_planning_metro),
TRAIN(app.organicmaps.R.drawable.ic_route_planning_train),
LIGHT_RAIL(app.organicmaps.R.drawable.ic_route_planning_train),
MONORAIL(app.organicmaps.R.drawable.ic_route_planning_monorail),
RULER(R.drawable.ic_ruler_route);
@DrawableRes
private final int mDrawable;
TransitStepType(@DrawableRes int drawable)
{
mDrawable = drawable;
}
@DrawableRes
public int getDrawable()
{
return mDrawable;
}
}

View File

@@ -0,0 +1,28 @@
package app.organicmaps.sdk.search;
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
/**
* Native search will return results via this interface.
*/
public interface BookmarkSearchListener
{
/**
* @param bookmarkIds Founded bookmark ids.
* @param timestamp Timestamp of search request.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
void onBookmarkSearchResultsUpdate(@Nullable long[] bookmarkIds, long timestamp);
/**
* @param bookmarkIds Founded bookmark ids.
* @param timestamp Timestamp of search request.
*/
// Used by JNI.
@Keep
@SuppressWarnings("unused")
void onBookmarkSearchResultsEnd(@Nullable long[] bookmarkIds, long timestamp);
}

Some files were not shown because too many files have changed in this diff Show More