mirror of
https://codeberg.org/comaps/comaps
synced 2025-12-21 05:43:37 +00:00
[android][sdk] Move java files into sdk module
Signed-off-by: Andrei Shkrob <github@shkrob.dev>
This commit is contained in:
committed by
Konstantin Pastbin
parent
447266c328
commit
6a85526ac9
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
347
android/sdk/src/main/java/app/organicmaps/sdk/Framework.java
Normal file
347
android/sdk/src/main/java/app/organicmaps/sdk/Framework.java
Normal 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();
|
||||
}
|
||||
425
android/sdk/src/main/java/app/organicmaps/sdk/Map.java
Normal file
425
android/sdk/src/main/java/app/organicmaps/sdk/Map.java
Normal 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);
|
||||
}
|
||||
@@ -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()
|
||||
{}
|
||||
}
|
||||
59
android/sdk/src/main/java/app/organicmaps/sdk/MapStyle.java
Normal file
59
android/sdk/src/main/java/app/organicmaps/sdk/MapStyle.java
Normal 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);
|
||||
}
|
||||
230
android/sdk/src/main/java/app/organicmaps/sdk/OrganicMaps.java
Normal file
230
android/sdk/src/main/java/app/organicmaps/sdk/OrganicMaps.java
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
52
android/sdk/src/main/java/app/organicmaps/sdk/Router.java
Normal file
52
android/sdk/src/main/java/app/organicmaps/sdk/Router.java
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package app.organicmaps.sdk.bookmarks.data;
|
||||
|
||||
public interface DataChangedListener
|
||||
{
|
||||
void onChanged();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.organicmaps.sdk.bookmarks.data;
|
||||
|
||||
public enum RoadWarningMarkType
|
||||
{
|
||||
TOLL,
|
||||
FERRY,
|
||||
DIRTY,
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package app.organicmaps.sdk.content;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface DataSource<D>
|
||||
{
|
||||
@NonNull
|
||||
D getData();
|
||||
|
||||
void invalidate();
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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(() -> {}));
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package app.organicmaps.sdk.display;
|
||||
|
||||
public enum DisplayType
|
||||
{
|
||||
Device,
|
||||
Car
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) + "% }";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
190
android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java
Normal file
190
android/sdk/src/main/java/app/organicmaps/sdk/editor/Editor.java
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package app.organicmaps.sdk.maplayer.isolines;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface IsolinesErrorDialogListener
|
||||
{
|
||||
void onStateChanged(@NonNull IsolinesState type);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.organicmaps.sdk.maplayer.isolines;
|
||||
|
||||
public enum IsolinesState
|
||||
{
|
||||
DISABLED,
|
||||
ENABLED,
|
||||
EXPIREDDATA,
|
||||
NODATA;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package app.organicmaps.sdk.routing;
|
||||
|
||||
public enum RouteMarkType
|
||||
{
|
||||
Start,
|
||||
Intermediate,
|
||||
Finish
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package app.organicmaps.sdk.routing;
|
||||
|
||||
public enum RouteRecommendationType
|
||||
{
|
||||
RebuildAfterPointsLoading
|
||||
}
|
||||
@@ -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 — 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 — 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user