diff options
author | Antonio Zugaldia <antonio@mapbox.com> | 2016-02-17 10:55:12 -0500 |
---|---|---|
committer | Antonio Zugaldia <antonio@mapbox.com> | 2016-02-26 09:13:17 -0500 |
commit | 01e55f183e4468d040e7b144536ca592c9b64cb5 (patch) | |
tree | f448bd208c3b43650ae5aa3918090f298d708fbf | |
parent | f683b10e106e7376322b9c67c2baf57f5022cec2 (diff) | |
download | qtlocation-mapboxgl-01e55f183e4468d040e7b144536ca592c9b64cb5.tar.gz |
# This is a combination of 8 commits.
# The first commit's message is:
# This is a combination of 2 commits.
# The first commit's message is:
# This is a combination of 3 commits.
# The first commit's message is:
# This is a combination of 2 commits.
# The first commit's message is:
# This is a combination of 21 commits.
# The first commit's message is:
[android] - Implements Android bindings for offline API
Fixes #3891
# The 2nd commit message will be skipped:
# [android] #3891 - rename OFFLINE_MAX_CACHE_SIZE to DEFAULT_MAX_CACHE_SIZE and adjust value
# The 3rd commit message will be skipped:
# [android] #3891 - makes de documentation more relevant to the current android implementation
# The 4th commit message will be skipped:
# [android] #3891 - rename isRequiredResourceCountIsIndeterminate() to isRequiredResourceCountPrecise()
# The 5th commit message will be skipped:
# [android] #3891 - rename complete() to isComplete()
# The 6th commit message will be skipped:
# [android] #3891 - rename OfflineRegionDefinition to OfflineTilePyramidRegionDefinition and make OfflineRegionDefinition an interface. Docs for corresponding classes updated.
# The 7th commit message will be skipped:
# [android] #3891 - make reason a more idiomatic ErrorReason Android IntDef and remove unnecessary constructor
# The 8th commit message will be skipped:
# [android] #3891 - reuse the calling object instead of creating a new manager
# The 9th commit message will be skipped:
# [android] #3891 - location, location, location
# The 10th commit message will be skipped:
# [android] #3891 - simpler list regions iteration
# The 11th commit message will be skipped:
# [android] #3891 - proper indeterminate -> precise transition
# The 12th commit message will be skipped:
# [android] #3891 - improve description for DEFAULT_MAX_CACHE_SIZE
# The 13th commit message will be skipped:
# [android] #3891 - delete global refs for obj and listCallback
# The 14th commit message will be skipped:
# [android] #3891 - simplify metadata conversion and fix metadata object
# The 15th commit message will be skipped:
# [android] - Implements Android bindings for offline API
# Fixes #3891
# The 16th commit message will be skipped:
# [android] #3891 - avoid exposing the int reason value in the public API
# The 17th commit message will be skipped:
# [android] #3891 - delete global refs for remaining callbacks and observer
# The 18th commit message will be skipped:
# [android] #3891 - remove unused offlineManagerClassConstructorId together with unnecessary private java constructor
# The 19th commit message will be skipped:
# [android] #3891 - remove non-relevant line
# The 20th commit message will be skipped:
# [android] #3891 - handle requiredResourceCountIsIndeterminate -> requiredResourceCountIsPrecise rename
# The 21st commit message will be skipped:
# [android] #3891 - revert map changes to allow rebase
# The 2nd commit message will be skipped:
# [android] #3891 - avoid exposing the int reason value in the public API
# The 2nd commit message will be skipped:
# [android] #3891 - rename complete() to isComplete()
# The 3rd commit message will be skipped:
# [android] #3891 - rename OfflineRegionDefinition to OfflineTilePyramidRegionDefinition and make OfflineRegionDefinition an interface. Docs for corresponding classes updated.
# The 2nd commit message will be skipped:
# [android] #3891 - location, location, location
# The 2nd commit message will be skipped:
# [android] #3891 - improve description for DEFAULT_MAX_CACHE_SIZE
# The 3rd commit message will be skipped:
# [android] #3891 - delete global refs for obj and listCallback
# The 4th commit message will be skipped:
# [android] #3891 - simplify metadata conversion and fix metadata object
# The 5th commit message will be skipped:
# [android] #3891 - delete global refs for remaining callbacks and observer
# The 6th commit message will be skipped:
# [android] #3891 - remove unused offlineManagerClassConstructorId together with unnecessary private java constructor
# The 7th commit message will be skipped:
# [android] #3891 - remove non-relevant line
# The 8th commit message will be skipped:
# [android] #3891 - handle requiredResourceCountIsIndeterminate -> requiredResourceCountIsPrecise rename
22 files changed, 2568 insertions, 43 deletions
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java index 0c2690aec1..6b213bb655 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java @@ -620,11 +620,26 @@ public class MapView extends FrameLayout { // Zoom // - double getZoom() { + /** + * Returns the current zoom level of the map view. + * + * @return The current zoom. + */ + @UiThread + @FloatRange(from = MapboxConstants.MINIMUM_ZOOM, to = MapboxConstants.MAXIMUM_ZOOM) + public double getZoom() { return mNativeMapView.getZoom(); } - void setMinZoom(@FloatRange(from = MapboxConstants.MINIMUM_ZOOM, to = MapboxConstants.MAXIMUM_ZOOM) double minZoom) { + /** + * <p> + * Sets the minimum zoom level the map can be displayed at. + * </p> + * + * @param minZoom The new minimum zoom level. + */ + @UiThread + public void setMinZoom(@FloatRange(from = MapboxConstants.MINIMUM_ZOOM, to = MapboxConstants.MAXIMUM_ZOOM) double minZoom) { mNativeMapView.setMinZoom(minZoom); } @@ -640,7 +655,19 @@ public class MapView extends FrameLayout { return mNativeMapView.getMaxZoom(); } - void setZoomControlsEnabled(boolean enabled) { + /** + * <p> + * Sets whether the zoom controls are enabled. + * If enabled, the zoom controls are a pair of buttons + * (one for zooming in, one for zooming out) that appear on the screen. + * When pressed, they cause the camera to zoom in (or out) by one zoom level. + * If disabled, the zoom controls are not shown. + * </p> + * By default the zoom controls are enabled if the device is only single touch capable; + * + * @param enabled If true, the zoom controls are enabled. + */ + public void setZoomControlsEnabled(boolean enabled) { mZoomButtonsController.setVisible(enabled); } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java index 6c9806f79b..9530096131 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapboxMap.java @@ -1,7 +1,6 @@ package com.mapbox.mapboxsdk.maps; import android.content.Context; - import android.location.Location; import android.os.Bundle; import android.os.SystemClock; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java new file mode 100644 index 0000000000..b56ecfc057 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java @@ -0,0 +1,184 @@ +package com.mapbox.mapboxsdk.offline; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; + +import java.io.File; + +/** + * The offline manager is the main entry point for offline-related functionality. + * It'll help you list and create offline regions. + */ +public class OfflineManager { + + // Default database name + private final static String OFFLINE_DATABASE_NAME = "mbgl-offline.db"; + + /* + * The maximumCacheSize parameter is a limit applied to non-offline resources only, + * i.e. resources added to the database for the "ambient use" caching functionality. + * There is no size limit for offline resources. + */ + private final static long DEFAULT_MAX_CACHE_SIZE = 50 * 1024 * 1024; + + // Holds the pointer to JNI DefaultFileSource + private long mDefaultFileSourcePtr = 0; + + // Makes sure callbacks come back to the main thread + private Handler handler; + + // This object is implemented as a singleton + private static OfflineManager instance; + + /* + * Callbacks + */ + + public interface ListOfflineRegionsCallback { + void onList(OfflineRegion[] offlineRegions); + void onError(String error); + } + + public interface CreateOfflineRegionCallback { + void onCreate(OfflineRegion offlineRegion); + void onError(String error); + } + + /* + * Constructors + */ + + private OfflineManager(Context context) { + // Get a pointer to the DefaultFileSource instance + String assetRoot = context.getFilesDir().getAbsolutePath(); + String cachePath = assetRoot + File.separator + OFFLINE_DATABASE_NAME; + mDefaultFileSourcePtr = createDefaultFileSource(cachePath, assetRoot, DEFAULT_MAX_CACHE_SIZE); + } + + public static synchronized OfflineManager getInstance(Context context) { + if (instance == null) { + instance = new OfflineManager(context); + } + + return instance; + } + + /* + * Access token getter/setter + */ + public void setAccessToken(String accessToken) { + setAccessToken(mDefaultFileSourcePtr, accessToken); + } + + public String getAccessToken() { + return getAccessToken(mDefaultFileSourcePtr); + } + + private Handler getHandler() { + if (handler == null) { + handler = new Handler(Looper.getMainLooper()); + } + + return handler; + } + + /** + * Retrieve all regions in the offline database. + * + * The query will be executed asynchronously and the results passed to the given + * callback on the main thread. + */ + public void listOfflineRegions(@NonNull final ListOfflineRegionsCallback callback) { + listOfflineRegions(mDefaultFileSourcePtr, new ListOfflineRegionsCallback() { + @Override + public void onList(final OfflineRegion[] offlineRegions) { + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onList(offlineRegions); + } + }); + } + + @Override + public void onError(final String error) { + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onError(error); + } + }); + } + }); + } + + /** + * Create an offline region in the database. + * + * When the initial database queries have completed, the provided callback will be + * executed on the main thread. + * + * Note that the resulting region will be in an inactive download state; to begin + * downloading resources, call `OfflineRegion.setDownloadState(DownloadState.STATE_ACTIVE)`, + * optionally registering an `OfflineRegionObserver` beforehand. + */ + public void createOfflineRegion( + @NonNull OfflineRegionDefinition definition, + @NonNull OfflineRegionMetadata metadata, + @NonNull final CreateOfflineRegionCallback callback) { + + createOfflineRegion(mDefaultFileSourcePtr, definition, metadata, new CreateOfflineRegionCallback() { + @Override + public void onCreate(final OfflineRegion offlineRegion) { + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onCreate(offlineRegion); + } + }); + } + + @Override + public void onError(final String error) { + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onError(error); + } + }); + } + }); + } + + /* + * Changing or bypassing this limit without permission from Mapbox is prohibited + * by the Mapbox Terms of Service. + */ + public void setOfflineMapboxTileCountLimit(long limit) { + setOfflineMapboxTileCountLimit(mDefaultFileSourcePtr, limit); + } + + + /* + * Native methods + */ + + private native long createDefaultFileSource( + String cachePath, String assetRoot, long maximumCacheSize); + + private native void setAccessToken(long defaultFileSourcePtr, String accessToken); + private native String getAccessToken(long defaultFileSourcePtr); + + private native void listOfflineRegions( + long defaultFileSourcePtr, ListOfflineRegionsCallback callback); + + private native void createOfflineRegion( + long defaultFileSourcePtr, OfflineRegionDefinition definition, + OfflineRegionMetadata metadata, CreateOfflineRegionCallback callback); + + private native void setOfflineMapboxTileCountLimit( + long defaultFileSourcePtr, long limit); + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java new file mode 100644 index 0000000000..9e518f1e6a --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegion.java @@ -0,0 +1,278 @@ +package com.mapbox.mapboxsdk.offline; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * To use offline maps on mobile, you will first have to create an offline region. + * Use OfflineManager.createOfflineRegion() to create a new offline region. + */ +public class OfflineRegion { + + private final static String LOG_TAG = "OfflineRegion"; + + // Parent OfflineManager + private OfflineManager offlineManager; + + // Members + private long mId = 0; + private OfflineRegionDefinition mDefinition = null; + private OfflineRegionMetadata mMetadata = null; + + // Holds the pointer to JNI OfflineRegion + private long mOfflineRegionPtr = 0; + + // Makes sure callbacks come back to the main thread + private Handler handler; + + /** + * A region can have a single observer, which gets notified whenever a change + * to the region's status occurs. + */ + public interface OfflineRegionObserver { + /** + * Implement this method to be notified of a change in the status of an + * offline region. Status changes include any change in state of the members + * of OfflineRegionStatus. + * + * This method will be executed on the main thread. + */ + void onStatusChanged(OfflineRegionStatus status); + + /** + * Implement this method to be notified of errors encountered while downloading + * regional resources. Such errors may be recoverable; for example the implementation + * will attempt to re-request failed resources based on an exponential backoff + * algorithm, or when it detects that network access has been restored. + * + * This method will be executed on the main thread. + */ + void onError(OfflineRegionError error); + + /* + * Implement this method to be notified when the limit on the number of Mapbox + * tiles stored for offline regions has been reached. + * + * Once the limit has been reached, the SDK will not download further offline + * tiles from Mapbox APIs until existing tiles have been removed. Contact your + * Mapbox sales representative to raise the limit. + * + * This limit does not apply to non-Mapbox tile sources. + * + * This method will be executed on the main thread. + */ + void mapboxTileCountLimitExceeded(long limit); + } + + /* + * Callbacks + */ + + public interface OfflineRegionStatusCallback { + void onStatus(OfflineRegionStatus status); + void onError(String error); + } + + public interface OfflineRegionDeleteCallback { + void onDelete(); + void onError(String error); + } + + /** + * A region is either inactive (not downloading, but previously-downloaded + * resources are available for use), or active (resources are being downloaded + * or will be downloaded, if necessary, when network access is available). + * + * This state is independent of whether or not the complete set of resources + * is currently available for offline use. To check if that is the case, use + * `OfflineRegionStatus.isComplete()`. + */ + + @IntDef({STATE_INACTIVE, STATE_ACTIVE}) + @Retention(RetentionPolicy.SOURCE) + public @interface DownloadState {} + + public static final int STATE_INACTIVE = 0; + public static final int STATE_ACTIVE = 1; + + /* + * Constructor + */ + + private OfflineRegion() { + // For JNI use only, to create a new offline region, use + // OfflineManager.createOfflineRegion() instead. + } + + /* + * Getters + */ + + public long getID() { + return mId; + } + + public OfflineRegionDefinition getDefinition() { + return mDefinition; + } + + public OfflineRegionMetadata getMetadata() { + return mMetadata; + } + + private Handler getHandler() { + if (handler == null) { + handler = new Handler(Looper.getMainLooper()); + } + + return handler; + } + + /** + * Register an observer to be notified when the state of the region changes. + */ + public void setObserver(@NonNull final OfflineRegionObserver observer) { + setOfflineRegionObserver(this, new OfflineRegionObserver() { + @Override + public void onStatusChanged(final OfflineRegionStatus status) { + getHandler().post(new Runnable() { + @Override + public void run() { + observer.onStatusChanged(status); + } + }); + } + + @Override + public void onError(final OfflineRegionError error) { + getHandler().post(new Runnable() { + @Override + public void run() { + observer.onError(error); + } + }); + } + + @Override + public void mapboxTileCountLimitExceeded(final long limit) { + getHandler().post(new Runnable() { + @Override + public void run() { + observer.mapboxTileCountLimitExceeded(limit); + } + }); + } + }); + } + + /** + * Pause or resume downloading of regional resources. + */ + public void setDownloadState(@DownloadState int state) { + setOfflineRegionDownloadState(this, state); + } + + /** + * Retrieve the current status of the region. The query will be executed + * asynchronously and the results passed to the given callback which will be + * executed on the main thread. + */ + public void getStatus(@NonNull final OfflineRegionStatusCallback callback) { + getOfflineRegionStatus(this, new OfflineRegionStatusCallback() { + @Override + public void onStatus(final OfflineRegionStatus status) { + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onStatus(status); + } + }); + } + + @Override + public void onError(final String error) { + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onError(error); + } + }); + } + }); + } + + /** + * Remove an offline region from the database and perform any resources evictions + * necessary as a result. + * + * Eviction works by removing the least-recently requested resources not also required + * by other regions, until the database shrinks below a certain size. + * + * When the operation is complete or encounters an error, the given callback will be + * executed on the main thread. + */ + public void delete(@NonNull final OfflineRegionDeleteCallback callback) { + deleteOfflineRegion(this, new OfflineRegionDeleteCallback() { + @Override + public void onDelete() { + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onDelete(); + OfflineRegion.this.finalize(); + } + }); + } + + @Override + public void onError(final String error) { + getHandler().post(new Runnable() { + @Override + public void run() { + callback.onError(error); + } + }); + } + }); + } + + @Override + protected void finalize() { + try { + super.finalize(); + destroyOfflineRegion(mOfflineRegionPtr); + mOfflineRegionPtr = 0; + } catch (Throwable throwable) { + Log.e(LOG_TAG, "Failed to finalize OfflineRegion: " + throwable.getMessage()); + } + } + + /* + * Native methods + */ + + private native void destroyOfflineRegion(long offlineRegionPtr); + + private native void setOfflineRegionObserver( + OfflineRegion offlineRegion, + OfflineRegionObserver observerCallback); + + private native void setOfflineRegionDownloadState( + OfflineRegion offlineRegion, + @DownloadState int offlineRegionDownloadState); + + private native void getOfflineRegionStatus( + OfflineRegion offlineRegion, + OfflineRegionStatusCallback statusCallback); + + private native void deleteOfflineRegion( + OfflineRegion offlineRegion, + OfflineRegionDeleteCallback deleteCallback); + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionDefinition.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionDefinition.java new file mode 100644 index 0000000000..0e7fb38e1c --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionDefinition.java @@ -0,0 +1,9 @@ +package com.mapbox.mapboxsdk.offline; + +/** + * This is the interface that all Offline Region definitions have to implement. + * + * For the present, a tile pyramid is the only type of offline region. + */ +public interface OfflineRegionDefinition { +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionError.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionError.java new file mode 100644 index 0000000000..e7a57379c5 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionError.java @@ -0,0 +1,53 @@ +package com.mapbox.mapboxsdk.offline; + +import android.support.annotation.StringDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * An Offline Region error + */ +public class OfflineRegionError { + + /** + * Error code, as a string, self-explanatory. + */ + @StringDef({REASON_SUCCESS, REASON_NOT_FOUND, REASON_SERVER, REASON_CONNECTION, REASON_OTHER}) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorReason {} + + public static final String REASON_SUCCESS = "REASON_SUCCESS"; + public static final String REASON_NOT_FOUND = "REASON_NOT_FOUND"; + public static final String REASON_SERVER = "REASON_SERVER"; + public static final String REASON_CONNECTION = "REASON_CONNECTION"; + public static final String REASON_OTHER = "REASON_OTHER"; + + private @ErrorReason String reason; + + /** + /* An error message from the request handler, e.g. a server message or a system message + /* informing the user about the reason for the failure. + */ + private String message; + + /* + * Constructors + */ + + private OfflineRegionError() { + // For JNI use only + } + + /* + * Getters + */ + + public @ErrorReason String getReason() { + return reason; + } + + public String getMessage() { + return message; + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionMetadata.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionMetadata.java new file mode 100644 index 0000000000..bbea4580f8 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionMetadata.java @@ -0,0 +1,63 @@ +package com.mapbox.mapboxsdk.offline; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +/** + * Arbitrary binary region metadata. The contents are opaque to the SDK implementation; + * it just stores and retrieves a byte[]. + */ +public class OfflineRegionMetadata { + + private byte[] metadata; + + /* + * Constructor + */ + + public OfflineRegionMetadata(byte[] metadata) { + this.metadata = metadata; + } + + /* + * Getters and setters + */ + + public byte[] getMetadata() { + return metadata; + } + + public void setMetadata(byte[] metadata) { + this.metadata = metadata; + } + + /* + * Overrides + */ + + @Override + public String toString() { + return "OfflineRegionMetadata{metadata=" + metadata + "}"; + } + + /* + * byte[] utils + */ + + public static byte[] serialize(Object obj) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream os = new ObjectOutputStream(out); + os.writeObject(obj); + return out.toByteArray(); + } + + public static Object deserialize(byte[] data) throws IOException, ClassNotFoundException { + ByteArrayInputStream in = new ByteArrayInputStream(data); + ObjectInputStream is = new ObjectInputStream(in); + return is.readObject(); + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionStatus.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionStatus.java new file mode 100644 index 0000000000..e65a20f18e --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineRegionStatus.java @@ -0,0 +1,84 @@ +package com.mapbox.mapboxsdk.offline; + +/** + * A region's status includes its active/inactive state as well as counts + * of the number of resources that have completed downloading, their total + * size in bytes, and the total number of resources that are required. + * + * Note that the total required size in bytes is not currently available. A + * future API release may provide an estimate of this number. + */ +public class OfflineRegionStatus { + + @OfflineRegion.DownloadState private int downloadState = OfflineRegion.STATE_INACTIVE; + + /** + * The number of resources that have been fully downloaded and are ready for + * offline access. + */ + private long completedResourceCount = 0; + + /** + * The cumulative size, in bytes, of all resources that have been fully downloaded. + */ + private long completedResourceSize = 0; + + /** + * The number of resources that are known to be required for this region. See the + * documentation for `requiredResourceCountIsPrecise` for an important caveat + * about this number. + */ + private long requiredResourceCount = 0; + + /** + * This property is true when the value of requiredResourceCount is a precise + * count of the number of required resources, and false when it is merely a lower + * bound. + * + * Specifically, it is false during early phases of an offline download. Once + * style and tile sources have been downloaded, it is possible to calculate the + * precise number of required resources, at which point it is set to true. + */ + private boolean requiredResourceCountIsPrecise = true; + + /* + * Use setObserver(OfflineRegionObserver observer) to obtain a OfflineRegionStatus object. + */ + + private OfflineRegionStatus() { + // For JNI use only + } + + /* + * Is the region complete? + */ + + public boolean isComplete() { + return (completedResourceCount == requiredResourceCount); + } + + /* + * Getters + */ + + public @OfflineRegion.DownloadState int getDownloadState() { + return downloadState; + } + + public long getCompletedResourceCount() { + return completedResourceCount; + } + + public long getCompletedResourceSize() { + return completedResourceSize; + } + + public long getRequiredResourceCount() { + return requiredResourceCount; + } + + public boolean isRequiredResourceCountPrecise() { + return requiredResourceCountIsPrecise; + } + +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineTilePyramidRegionDefinition.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineTilePyramidRegionDefinition.java new file mode 100644 index 0000000000..5a0be6b33f --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineTilePyramidRegionDefinition.java @@ -0,0 +1,65 @@ +package com.mapbox.mapboxsdk.offline; + +import com.mapbox.mapboxsdk.geometry.LatLngBounds; + +/** + * An offline region defined by a style URL, geographic bounding box, zoom range, and + * device pixel ratio. + * + * Both minZoom and maxZoom must be ≥ 0, and maxZoom must be ≥ minZoom. + * + * maxZoom may be ∞, in which case for each tile source, the region will include + * tiles from minZoom up to the maximum zoom level provided by that source. + * + * pixelRatio must be ≥ 0 and should typically be 1.0 or 2.0. + */ +public class OfflineTilePyramidRegionDefinition implements OfflineRegionDefinition { + + private String styleURL; + private LatLngBounds bounds; + private double minZoom; + private double maxZoom; + private float pixelRatio; + + /* + * Constructors + */ + + private OfflineTilePyramidRegionDefinition() { + // For JNI use only + } + + public OfflineTilePyramidRegionDefinition( + String styleURL, LatLngBounds bounds, double minZoom, double maxZoom, float pixelRatio) { + this.styleURL = styleURL; + this.bounds = bounds; + this.minZoom = minZoom; + this.maxZoom = maxZoom; + this.pixelRatio = pixelRatio; + } + + /* + * Getters + */ + + public String getStyleURL() { + return styleURL; + } + + public LatLngBounds getBounds() { + return bounds; + } + + public double getMinZoom() { + return minZoom; + } + + public double getMaxZoom() { + return maxZoom; + } + + public float getPixelRatio() { + return pixelRatio; + } + +} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml index 4b44efdfe5..37bf928a5a 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml @@ -21,7 +21,6 @@ android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> @@ -91,22 +90,24 @@ android:label="@string/activity_dynamic_marker" /> <activity android:name=".MapPaddingActivity" - android:screenOrientation="portrait" - android:label="@string/activity_map_padding" /> + android:label="@string/activity_map_padding" + android:screenOrientation="portrait" /> + <activity + android:name=".OfflineActivity" + android:label="@string/activity_offline" /> <meta-data android:name="com.mapbox.AccessToken" android:value="" /> - <meta-data android:name="com.mapbox.TestEventsServer" android:value="https://cloudfront-staging.tilestream.net" /> - <meta-data android:name="com.mapbox.TestEventsAccessToken" android:value="sk.eyJ1IjoiYmxlZWdlIiwiYSI6InNpcml1c2x5In0.KyT-boMyC_xZYTYojTc8zg" /> <service android:name="com.mapbox.mapboxsdk.telemetry.TelemetryService" /> + </application> </manifest> diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/MainActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/MainActivity.java index d1ac656cd0..451876e39b 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/MainActivity.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/MainActivity.java @@ -472,6 +472,10 @@ public class MainActivity extends AppCompatActivity { startActivity(new Intent(getApplicationContext(),MapPaddingActivity.class)); return true; + case R.id.action_offline: + startActivity(new Intent(getApplicationContext(), OfflineActivity.class)); + return true; + default: return changeMapStyle(menuItem.getItemId()); } diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/OfflineActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/OfflineActivity.java new file mode 100644 index 0000000000..64ecc2100e --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/OfflineActivity.java @@ -0,0 +1,364 @@ +package com.mapbox.mapboxsdk.testapp; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.Toast; + +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; +import com.mapbox.mapboxsdk.constants.Style; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; +import com.mapbox.mapboxsdk.offline.OfflineManager; +import com.mapbox.mapboxsdk.offline.OfflineRegion; +import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition; +import com.mapbox.mapboxsdk.offline.OfflineRegionError; +import com.mapbox.mapboxsdk.offline.OfflineRegionMetadata; +import com.mapbox.mapboxsdk.offline.OfflineRegionStatus; +import com.mapbox.mapboxsdk.testapp.offline.DownloadRegionDialog; +import com.mapbox.mapboxsdk.testapp.offline.ListRegionsDialog; +import com.mapbox.mapboxsdk.utils.ApiAccess; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +public class OfflineActivity extends AppCompatActivity + implements DownloadRegionDialog.DownloadRegionDialogListener { + + private final static String LOG_TAG = "OfflineActivity"; + + private final static String KEY_REGION_NAME = "KEY_REGION_NAME"; + + /* + * UI elements + */ + private MapView mMapView; + private MapboxMap mMapboxMap; + private ProgressBar mProgressBar; + private Button downloadRegion; + private Button listRegions; + + private boolean isEndNotified; + + /* + * Offline objects + */ + private OfflineManager mOfflineManager; + private OfflineRegion mOfflineRegion; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_offline); + + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + } + + // Set up map + mMapView = (MapView) findViewById(R.id.mapView); + mMapView.setAccessToken(ApiAccess.getToken(this)); + mMapView.onCreate(savedInstanceState); + mMapView.getMapAsync(new OnMapReadyCallback() { + @Override + public void onMapReady(@NonNull MapboxMap mapboxMap) { + Log.d(LOG_TAG, "Map is ready"); + mMapboxMap = mapboxMap; + + // Set style + mapboxMap.setStyle(Style.MAPBOX_STREETS); + + // Set initial position to UNHQ in NYC + mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition( + new CameraPosition.Builder() + .target(new LatLng(40.749851, -73.967966)) + .zoom(14) + .bearing(0) + .tilt(0) + .build())); + } + }); + + // The progress bar + mProgressBar = (ProgressBar) findViewById(R.id.progress_bar); + + // Set up button listeners + downloadRegion = (Button) findViewById(R.id.button_download_region); + downloadRegion.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleDownloadRegion(); + } + }); + + listRegions = (Button) findViewById(R.id.button_list_regions); + listRegions.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handleListRegions(); + } + }); + + // Set up the OfflineManager + mOfflineManager = OfflineManager.getInstance(this); + mOfflineManager.setAccessToken(ApiAccess.getToken(this)); + } + + @Override + protected void onStart() { + super.onStart(); + mMapView.onStart(); + } + + @Override + public void onResume() { + super.onResume(); + mMapView.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + mMapView.onPause(); + } + + @Override + protected void onStop() { + super.onStop(); + mMapView.onStop(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mMapView.onSaveInstanceState(outState); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mMapView.onDestroy(); + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + mMapView.onLowMemory(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /* + * Buttons logic + */ + + private void handleDownloadRegion() { + Log.d(LOG_TAG, "handleDownloadRegion"); + + // Show dialog + DownloadRegionDialog downloadRegionDialog = new DownloadRegionDialog(); + downloadRegionDialog.show(getSupportFragmentManager(), "download"); + } + + private void handleListRegions() { + Log.d(LOG_TAG, "handleListRegions"); + + // Query the DB asynchronously + mOfflineManager.listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() { + @Override + public void onList(OfflineRegion[] offlineRegions) { + // Check result + if (offlineRegions == null || offlineRegions.length == 0) { + Log.d(LOG_TAG, "You have no regions yet."); + return; + } + + // Get regions info + ArrayList<String> offlineRegionsNames = new ArrayList<>(); + for (OfflineRegion offlineRegion : offlineRegions) { + offlineRegionsNames.add(getRegionName(offlineRegion)); + } + + // Create args + Bundle args = new Bundle(); + args.putStringArrayList(ListRegionsDialog.ITEMS, offlineRegionsNames); + + // Show dialog + ListRegionsDialog listRegionsDialog = new ListRegionsDialog(); + listRegionsDialog.setArguments(args); + listRegionsDialog.show(getSupportFragmentManager(), "list"); + } + + @Override + public void onError(String error) { + Log.e(LOG_TAG, "Error: " + error); + } + + private String getRegionName(OfflineRegion offlineRegion) { + String regionName = "Region " + offlineRegion.getID(); + + try { + byte[] metadata = offlineRegion.getMetadata().getMetadata(); + HashMap<String, String> data = (HashMap<String, String>) OfflineRegionMetadata.deserialize(metadata); + regionName = data.get(KEY_REGION_NAME); + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to decode metadata: " + e.getMessage()); + } + + return regionName; + } + }); + } + + /* + * Dialogs + */ + + @Override + public void onDownloadRegionDialogPositiveClick(final String regionName) { + Log.d(LOG_TAG, "Download started: " + regionName); + + // Start progress bar + startProgress(); + + // Definition + String styleURL = mMapboxMap.getStyleUrl(); + LatLngBounds bounds = mMapboxMap.getProjection().getVisibleRegion().latLngBounds; + double minZoom = 14; // Switch to dynamic once map refactor is complete + double maxZoom = 16; // Switch to dynamic once map refactor is complete + float pixelRatio = this.getResources().getDisplayMetrics().density; + OfflineTilePyramidRegionDefinition definition = new OfflineTilePyramidRegionDefinition( + styleURL, bounds, minZoom, maxZoom, pixelRatio); + + // Sample way of encoding metadata + OfflineRegionMetadata metadata = null; + try { + HashMap<String, String> data = new HashMap<>(); + data.put(KEY_REGION_NAME, regionName); + byte[] dataEncoded = OfflineRegionMetadata.serialize(data); + metadata = new OfflineRegionMetadata(dataEncoded); + } catch (IOException e) { + Log.e(LOG_TAG, "Metadata encoding failed: " + e.getMessage()); + } + + // Create region + mOfflineManager.createOfflineRegion(definition, metadata, new OfflineManager.CreateOfflineRegionCallback() { + @Override + public void onCreate(OfflineRegion offlineRegion) { + Log.d(LOG_TAG, "Offline region created: " + regionName); + mOfflineRegion = offlineRegion; + launchDownload(); + } + + @Override + public void onError(String error) { + Log.e(LOG_TAG, "Error: " + error); + } + }); + } + + private void launchDownload() { + // Set an observer + mOfflineRegion.setObserver(new OfflineRegion.OfflineRegionObserver() { + @Override + public void onStatusChanged(OfflineRegionStatus status) { + // Compute a percentage + double percentage = status.getRequiredResourceCount() >= 0 ? + (100.0 * status.getCompletedResourceCount() / status.getRequiredResourceCount()) : + 0.0; + + if (status.isComplete() || percentage >= 98.0 /* Known issue */) { + // Download complete + endProgress("Region downloaded successfully."); + return; + } else if (status.isRequiredResourceCountPrecise()) { + // Switch to determinate state + setPercentage((int) Math.round(percentage)); + } + + // Debug + Log.d(LOG_TAG, String.format("%s/%s resources; %s bytes downloaded.", + String.valueOf(status.getCompletedResourceCount()), + String.valueOf(status.getRequiredResourceCount()), + String.valueOf(status.getCompletedResourceSize()))); + } + + @Override + public void onError(OfflineRegionError error) { + Log.e(LOG_TAG, "onError reason: " + error.getReason()); + Log.e(LOG_TAG, "onError message: " + error.getMessage()); + } + + @Override + public void mapboxTileCountLimitExceeded(long limit) { + Log.e(LOG_TAG, "Mapbox tile count limit exceeded: " + limit); + } + }); + + // Change the region state + mOfflineRegion.setDownloadState(OfflineRegion.STATE_ACTIVE); + } + + /* + * Progress bar + */ + + private void startProgress() { + // Disable buttons + downloadRegion.setEnabled(false); + listRegions.setEnabled(false); + + // Start and show the progress bar + isEndNotified = false; + mProgressBar.setIndeterminate(true); + mProgressBar.setVisibility(View.VISIBLE); + } + private void setPercentage(final int percentage) { + mProgressBar.setIndeterminate(false); + mProgressBar.setProgress(percentage); + } + + private void endProgress(final String message) { + // Don't notify more than once + if (isEndNotified) return; + + // Enable buttons + downloadRegion.setEnabled(true); + listRegions.setEnabled(true); + + // Stop and hide the progress bar + isEndNotified = true; + mProgressBar.setIndeterminate(false); + mProgressBar.setVisibility(View.GONE); + + // Show a toast + Toast.makeText(OfflineActivity.this, message, Toast.LENGTH_LONG).show(); + } + +} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/offline/DownloadRegionDialog.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/offline/DownloadRegionDialog.java new file mode 100644 index 0000000000..81b681cc50 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/offline/DownloadRegionDialog.java @@ -0,0 +1,60 @@ +package com.mapbox.mapboxsdk.testapp.offline; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.widget.EditText; + +import com.mapbox.mapboxsdk.testapp.R; + +/** + * Created by antonio on 2/17/16. + */ +public class DownloadRegionDialog extends DialogFragment { + + private final static String LOG_TAG = "DownloadRegionDialog"; + + public interface DownloadRegionDialogListener { + void onDownloadRegionDialogPositiveClick(String regionName); + } + + DownloadRegionDialogListener mListener; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mListener = (DownloadRegionDialogListener) activity; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + // Let the user choose a name for the region + final EditText regionNameEdit = new EditText(getActivity()); + + builder.setTitle("Choose a name for the region") + .setIcon(R.drawable.ic_airplanemode_active_black_24dp) + .setView(regionNameEdit) + .setPositiveButton("Start", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String regionName = regionNameEdit.getText().toString(); + mListener.onDownloadRegionDialogPositiveClick(regionName); + } + }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d(LOG_TAG, "Download cancelled."); + } + }); + + return builder.create(); + } +} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/offline/ListRegionsDialog.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/offline/ListRegionsDialog.java new file mode 100644 index 0000000000..50df71ad00 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/offline/ListRegionsDialog.java @@ -0,0 +1,51 @@ +package com.mapbox.mapboxsdk.testapp.offline; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.util.Log; + +import com.mapbox.mapboxsdk.testapp.R; + +import java.util.ArrayList; + +/** + * Created by antonio on 2/17/16. + */ +public class ListRegionsDialog extends DialogFragment { + + private final static String LOG_TAG = "ListRegionsDialog"; + + public final static String ITEMS = "ITEMS"; + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + // Read args + Bundle args = getArguments(); + ArrayList<String> offlineRegionsNames = (args == null ? null : args.getStringArrayList(ITEMS)); + CharSequence[] items = offlineRegionsNames.toArray(new CharSequence[offlineRegionsNames.size()]); + + builder.setTitle("List of offline regions") + .setIcon(R.drawable.ic_airplanemode_active_black_24dp) + .setItems(items, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d(LOG_TAG, "Selected item: " + which); + } + }) + .setPositiveButton("Accept", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d(LOG_TAG, "Dialog dismissed"); + } + }); + + return builder.create(); + } +} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_airplanemode_active_black_24dp.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_airplanemode_active_black_24dp.xml new file mode 100644 index 0000000000..55a8d22a54 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_airplanemode_active_black_24dp.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M10.18,9"/> + <path + android:fillColor="#FF000000" + android:pathData="M21,16v-2l-8,-5V3.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,2.67 10,3.5V9l-8,5v2l8,-2.5V19l-2,1.5V22l3.5,-1 3.5,1v-1.5L13,19v-5.5l8,2.5z"/> +</vector> diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera.xml index ce055138b1..0b318d781b 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_camera.xml @@ -1,9 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_offline.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_offline.xml new file mode 100644 index 0000000000..5f24ea812e --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_offline.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <android.support.v7.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:background="@color/primary" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" /> + + <ProgressBar + android:id="@+id/progress_bar" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="25dp" + android:layout_below="@+id/toolbar" + style="@android:style/Widget.Material.ProgressBar.Horizontal" /> + + <com.mapbox.mapboxsdk.maps.MapView + android:id="@+id/mapView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_below="@+id/progress_bar" /> + + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="10dp" + android:text="@string/button_download_region" + android:id="@+id/button_download_region" + android:layout_margin="@dimen/fab_margin" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_alignParentBottom="true" + android:background="@color/white"/> + + <Button + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="10dp" + android:text="@string/button_list_regions" + android:id="@+id/button_list_regions" + android:layout_alignBottom="@+id/button_download_region" + android:layout_alignParentRight="true" + android:layout_marginRight="@dimen/fab_margin" + android:background="@color/white"/> + +</RelativeLayout> diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_drawer.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_drawer.xml index 512d195593..92ec45b8c8 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_drawer.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_drawer.xml @@ -73,6 +73,12 @@ <menu> <item + android:id="@+id/action_offline" + android:checkable="false" + android:icon="@drawable/ic_airplanemode_active_black_24dp" + android:title="@string/action_offline" /> + + <item android:id="@+id/action_mapboxmap" android:checkable="false" android:icon="@drawable/ic_mapboxmap" diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/dimens.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/dimens.xml index c852ed0e7a..aeda60d478 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/dimens.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/dimens.xml @@ -6,5 +6,4 @@ <dimen name="map_padding_left">96dp</dimen> <dimen name="map_padding_bottom">256dp</dimen> <dimen name="map_padding_right">32dp</dimen> - </resources> diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml index ea346674b5..85e928aadc 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/strings.xml @@ -27,6 +27,7 @@ <string name="activity_scroll_by">Scroll By Activity</string> <string name="activity_dynamic_marker">Dynamic Marker Activity</string> <string name="activity_map_padding">Map Padding Activity</string> + <string name="activity_offline">Offline Map Activity</string> <string name="navdrawer_menu_title_mainactivity_controls">Main Activity Controls</string> <string name="navdrawer_menu_title_mainactivity_styles">Main Activity Styles</string> @@ -56,6 +57,7 @@ <string name="action_double_mapview">Double MapView</string> <string name="action_dynamic_marker">Dynamic Marker</string> <string name="action_map_padding">Map Padding</string> + <string name="action_offline">Offline Map</string> <string name="button_camera_move">Move</string> <string name="button_camera_ease">Ease</string> @@ -98,5 +100,7 @@ <string name="scrollby_x_value">X: %1$d</string> <string name="scrollby_y_value">Y: %1$d</string> + <string name="button_download_region">Download region</string> + <string name="button_list_regions">List regions</string> </resources> diff --git a/platform/android/src/jni.cpp b/platform/android/src/jni.cpp index db6bc73726..cfb7ca34e3 100755 --- a/platform/android/src/jni.cpp +++ b/platform/android/src/jni.cpp @@ -25,6 +25,7 @@ #include <mbgl/platform/log.hpp> #include <mbgl/storage/network_status.hpp> #include <mbgl/util/exception.hpp> +#include <mbgl/util/string.hpp> #pragma clang diagnostic ignored "-Wunused-parameter" @@ -115,6 +116,67 @@ jfieldID customLayerInitializeFunctionId = nullptr; jfieldID customLayerRenderFunctionId = nullptr; jfieldID customLayerDeinitializeFunctionId = nullptr; +// Offline declarations start + +jclass offlineManagerClass = nullptr; +jfieldID offlineManagerClassPtrId = nullptr; + +jclass listOfflineRegionsCallbackClass = nullptr; +jmethodID listOnListMethodId = nullptr; +jmethodID listOnErrorMethodId = nullptr; + +jclass offlineRegionClass = nullptr; +jmethodID offlineRegionConstructorId = nullptr; +jfieldID offlineRegionOfflineManagerId = nullptr; +jfieldID offlineRegionIdId = nullptr; +jfieldID offlineRegionDefinitionId = nullptr; +jfieldID offlineRegionMetadataId = nullptr; +jfieldID offlineRegionPtrId = nullptr; + +jclass offlineRegionDefinitionClass = nullptr; +jmethodID offlineRegionDefinitionConstructorId = nullptr; +jfieldID offlineRegionDefinitionStyleURLId = nullptr; +jfieldID offlineRegionDefinitionBoundsId = nullptr; +jfieldID offlineRegionDefinitionMinZoomId = nullptr; +jfieldID offlineRegionDefinitionMaxZoomId = nullptr; +jfieldID offlineRegionDefinitionPixelRatioId = nullptr; + +jclass offlineRegionMetadataClass = nullptr; +jmethodID offlineRegionMetadataConstructorId = nullptr; +jfieldID offlineRegionMetadataMetadataId = nullptr; + +jclass createOfflineRegionCallbackClass = nullptr; +jmethodID createOnCreateMethodId = nullptr; +jmethodID createOnErrorMethodId = nullptr; + +jclass offlineRegionObserverClass = nullptr; +jmethodID offlineRegionObserveronStatusChangedId = nullptr; +jmethodID offlineRegionObserveronErrorId = nullptr; +jmethodID offlineRegionObserveronLimitId = nullptr; + +jclass offlineRegionStatusClass = nullptr; +jmethodID offlineRegionStatusConstructorId = nullptr; +jfieldID offlineRegionStatusDownloadStateId = nullptr; +jfieldID offlineRegionStatusCompletedResourceCountId = nullptr; +jfieldID offlineRegionStatusCompletedResourceSizeId = nullptr; +jfieldID offlineRegionStatusRequiredResourceCountId = nullptr; +jfieldID offlineRegionStatusRequiredResourceCountIsPreciseId = nullptr; + +jclass offlineRegionErrorClass = nullptr; +jmethodID offlineRegionErrorConstructorId = nullptr; +jfieldID offlineRegionErrorReasonId = nullptr; +jfieldID offlineRegionErrorMessageId = nullptr; + +jclass offlineRegionStatusCallbackClass = nullptr; +jmethodID offlineRegionStatusOnStatusId = nullptr; +jmethodID offlineRegionStatusOnErrorId = nullptr; + +jclass offlineRegionDeleteCallbackClass = nullptr; +jmethodID offlineRegionDeleteOnDeleteId = nullptr; +jmethodID offlineRegionDeleteOnErrorId = nullptr; + +// Offline declarations end + bool throw_jni_error(JNIEnv *env, const char *msg) { if (env->ThrowNew(runtimeExceptionClass, msg) < 0) { env->ExceptionDescribe(); @@ -373,6 +435,59 @@ std::pair<mbgl::AnnotationSegment, mbgl::ShapeAnnotation::Properties> annotation return std::make_pair(segment, shapeProperties); } +static std::vector<uint8_t> metadata_from_java(JNIEnv* env, jbyteArray j) { + mbgl::Log::Debug(mbgl::Event::JNI, "metadata_from_java"); + jsize length = env->GetArrayLength(j); + std::vector<uint8_t> c; + c.resize(length); + env->GetByteArrayRegion(j, 0, length, reinterpret_cast<jbyte*>(c.data())); + return c; +} + +static jbyteArray metadata_from_native(JNIEnv* env, const std::vector<uint8_t>& c) { + mbgl::Log::Debug(mbgl::Event::JNI, "metadata_from_java"); + jsize length = static_cast<jsize>(c.size()); + jbyteArray j = env->NewByteArray(length); + env->SetByteArrayRegion(j, 0, c.size(), reinterpret_cast<const jbyte*>(c.data())); + return j; +} + +static mbgl::LatLngBounds latlngbounds_from_java(JNIEnv *env, jobject latLngBounds) { + // Checks + if (env->ExceptionCheck() || (latLngBounds == nullptr)) { + env->ExceptionDescribe(); + return mbgl::LatLngBounds::empty(); + } + + jdouble swLat = env->GetDoubleField(latLngBounds, latLngBoundsLatSouthId); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + return mbgl::LatLngBounds::empty(); + } + + jdouble swLon = env->GetDoubleField(latLngBounds, latLngBoundsLonWestId); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + return mbgl::LatLngBounds::empty(); + } + + jdouble neLat = env->GetDoubleField(latLngBounds, latLngBoundsLatNorthId); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + return mbgl::LatLngBounds::empty(); + } + + jdouble neLon = env->GetDoubleField(latLngBounds, latLngBoundsLonEastId); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + return mbgl::LatLngBounds::empty(); + } + + // Build object + mbgl::LatLngBounds result = mbgl::LatLngBounds::hull({ swLat, swLon }, { neLat, neLon }); + return result; +} + } } @@ -1184,43 +1299,20 @@ void JNICALL nativeRemoveAnnotations(JNIEnv *env, jobject obj, jlong nativeMapVi nativeMapView->getMap().removeAnnotations(ids); } -jlongArray JNICALL nativeGetAnnotationsInBounds(JNIEnv *env, jobject obj, jlong nativeMapViewPtr, jobject latLngBounds) { +jlongArray JNICALL nativeGetAnnotationsInBounds(JNIEnv *env, jobject obj, jlong nativeMapViewPtr, jobject latLngBounds_) { mbgl::Log::Debug(mbgl::Event::JNI, "nativeGetAnnotationsInBounds"); assert(nativeMapViewPtr != 0); NativeMapView *nativeMapView = reinterpret_cast<NativeMapView *>(nativeMapViewPtr); - if (env->ExceptionCheck() || (latLngBounds == nullptr)) { - env->ExceptionDescribe(); + // Conversion + mbgl::LatLngBounds latLngBounds = latlngbounds_from_java(env, latLngBounds_); + if (latLngBounds.isEmpty()) { return nullptr; } - jdouble swLat = env->GetDoubleField(latLngBounds, latLngBoundsLatSouthId); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - return nullptr; - } - - jdouble swLon = env->GetDoubleField(latLngBounds, latLngBoundsLonWestId); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - return nullptr; - } - - jdouble neLat = env->GetDoubleField(latLngBounds, latLngBoundsLatNorthId); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - return nullptr; - } - - jdouble neLon = env->GetDoubleField(latLngBounds, latLngBoundsLonEastId); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - return nullptr; - } - - // assume only points for now + // Assume only points for now std::vector<uint32_t> annotations = nativeMapView->getMap().getPointAnnotationsInBounds( - mbgl::LatLngBounds::hull({ swLat, swLon }, { neLat, neLon })); + latLngBounds); return std_vector_uint_to_jobject(env, annotations); } @@ -1620,6 +1712,422 @@ void JNICALL nativeRemoveCustomLayer(JNIEnv *env, jobject obj, jlong nativeMapVi nativeMapView->getMap().removeCustomLayer(std_string_from_jstring(env, id)); } +// Offline calls begin + +jlong JNICALL createDefaultFileSource(JNIEnv *env, jobject obj, jstring cachePath_, jstring assetRoot_, jlong maximumCacheSize) { + mbgl::Log::Debug(mbgl::Event::JNI, "createDefaultFileSource"); + std::string cachePath = std_string_from_jstring(env, cachePath_); + std::string assetRoot = std_string_from_jstring(env, assetRoot_); + mbgl::DefaultFileSource *defaultFileSource = new mbgl::DefaultFileSource(cachePath, assetRoot, maximumCacheSize); + jlong defaultFileSourcePtr = reinterpret_cast<jlong>(defaultFileSource); + return defaultFileSourcePtr; +} + +void JNICALL setAccessToken(JNIEnv *env, jobject obj, jlong defaultFileSourcePtr, jstring accessToken_) { + mbgl::Log::Debug(mbgl::Event::JNI, "setAccessToken"); + assert(defaultFileSourcePtr != 0); + std::string accessToken = std_string_from_jstring(env, accessToken_); + mbgl::DefaultFileSource *defaultFileSource = reinterpret_cast<mbgl::DefaultFileSource *>(defaultFileSourcePtr); + defaultFileSource->setAccessToken(accessToken); +} + +jstring JNICALL getAccessToken(JNIEnv *env, jobject obj, jlong defaultFileSourcePtr) { + mbgl::Log::Debug(mbgl::Event::JNI, "getAccessToken"); + assert(defaultFileSourcePtr != 0); + mbgl::DefaultFileSource *defaultFileSource = reinterpret_cast<mbgl::DefaultFileSource *>(defaultFileSourcePtr); + std::string accessToken = defaultFileSource->getAccessToken(); + return std_string_to_jstring(env, accessToken); +} + +void JNICALL listOfflineRegions(JNIEnv *env, jobject obj, jlong defaultFileSourcePtr, jobject listCallback) { + mbgl::Log::Debug(mbgl::Event::JNI, "listOfflineRegions"); + + // Checks + assert(defaultFileSourcePtr != 0); + if (listCallback == nullptr) { + mbgl::Log::Error(mbgl::Event::JNI, "Callback has to be set."); + return; + } + + // Makes sure the objects don't get GC'ed + obj = reinterpret_cast<jobject>(env->NewGlobalRef(obj)); + listCallback = reinterpret_cast<jobject>(env->NewGlobalRef(listCallback)); + + // Launch listCallback + mbgl::DefaultFileSource *defaultFileSource = reinterpret_cast<mbgl::DefaultFileSource *>(defaultFileSourcePtr); + defaultFileSource->listOfflineRegions([obj, defaultFileSourcePtr, listCallback](std::exception_ptr error, mbgl::optional<std::vector<mbgl::OfflineRegion>> regions) { + + // Reattach, the callback comes from a different thread + JNIEnv *env2; + jboolean renderDetach = attach_jni_thread(theJVM, &env2, "Offline Thread"); + if (renderDetach) { + mbgl::Log::Debug(mbgl::Event::JNI, "Attached."); + } + + if (error) { + std::string message = mbgl::util::toString(error); + env2->CallVoidMethod(listCallback, listOnErrorMethodId, std_string_to_jstring(env2, message)); + } else if (regions) { + // Build jobjectArray + jsize index = 0; + jobjectArray jregions = env2->NewObjectArray(regions->size(), offlineRegionClass, NULL); + for (auto& region : *regions) { + // Build the Region object + jobject jregion = env2->NewObject(offlineRegionClass, offlineRegionConstructorId); + env2->SetObjectField(jregion, offlineRegionOfflineManagerId, obj); + env2->SetLongField(jregion, offlineRegionIdId, region.getID()); + + // Metadata object + jbyteArray metadata = metadata_from_native(env2, region.getMetadata()); + jobject jmetadata = env2->NewObject(offlineRegionMetadataClass, offlineRegionMetadataConstructorId, metadata); + env2->SetObjectField(jregion, offlineRegionMetadataId, jmetadata); + + // Moves the region on the stack into a heap-allocated one + env2->SetLongField(jregion, offlineRegionPtrId, + reinterpret_cast<jlong>(new mbgl::OfflineRegion(std::move(region)))); + + env2->SetObjectArrayElement(jregions, index, jregion); + index++; + } + + // Trigger callback + env2->CallVoidMethod(listCallback, listOnListMethodId, jregions); + } + + // Delete global refs and detach when we're done + env2->DeleteGlobalRef(obj); + env2->DeleteGlobalRef(listCallback); + detach_jni_thread(theJVM, &env2, renderDetach); + }); +} + +void JNICALL createOfflineRegion(JNIEnv *env, jobject obj, jlong defaultFileSourcePtr, jobject definition_, jobject metadata_, jobject createCallback) { + mbgl::Log::Debug(mbgl::Event::JNI, "createOfflineRegion"); + + // Checks + assert(defaultFileSourcePtr != 0); + if (createCallback == nullptr) { + mbgl::Log::Error(mbgl::Event::JNI, "Callback has to be set."); + return; + } + + // Definition fields + jstring jStyleURL = reinterpret_cast<jstring>(env->GetObjectField(definition_, offlineRegionDefinitionStyleURLId)); + std::string styleURL = std_string_from_jstring(env, jStyleURL); + jobject jBounds = env->GetObjectField(definition_, offlineRegionDefinitionBoundsId); + jdouble jMinZoom = env->GetDoubleField(definition_, offlineRegionDefinitionMinZoomId); + jdouble jMaxZoom = env->GetDoubleField(definition_, offlineRegionDefinitionMaxZoomId); + jfloat jPixelRatio = env->GetFloatField(definition_, offlineRegionDefinitionPixelRatioId); + + // Convert bounds fields to native + mbgl::LatLngBounds bounds = latlngbounds_from_java(env, jBounds); + + // Definition + mbgl::OfflineTilePyramidRegionDefinition definition(styleURL, bounds, jMinZoom, jMaxZoom, jPixelRatio); + + // Metadata + mbgl::OfflineRegionMetadata metadata; + jbyteArray jmetadata = (jbyteArray)env->GetObjectField(metadata_, offlineRegionMetadataMetadataId); + if (jmetadata != nullptr) { + metadata = metadata_from_java(env, jmetadata); + } + + // Makes sure the objects don't get GC'ed + obj = reinterpret_cast<jobject>(env->NewGlobalRef(obj)); + createCallback = reinterpret_cast<jobject>(env->NewGlobalRef(createCallback)); + + // Launch createCallback + mbgl::DefaultFileSource *defaultFileSource = reinterpret_cast<mbgl::DefaultFileSource *>(defaultFileSourcePtr); + defaultFileSource->createOfflineRegion(definition, metadata, [obj, defaultFileSourcePtr, createCallback] (std::exception_ptr error, mbgl::optional<mbgl::OfflineRegion> region) { + + // Reattach, the callback comes from a different thread + JNIEnv *env2; + jboolean renderDetach = attach_jni_thread(theJVM, &env2, "Offline Thread"); + if (renderDetach) { + mbgl::Log::Debug(mbgl::Event::JNI, "Attached."); + } + + if (error) { + std::string message = mbgl::util::toString(error); + env2->CallVoidMethod(createCallback, createOnErrorMethodId, std_string_to_jstring(env2, message)); + } else if (region) { + // Build the Region object + jobject jregion = env2->NewObject(offlineRegionClass, offlineRegionConstructorId); + env2->SetObjectField(jregion, offlineRegionOfflineManagerId, obj); + env2->SetLongField(jregion, offlineRegionIdId, region->getID()); + + // Metadata object + jbyteArray xmetadata = metadata_from_native(env2, region->getMetadata()); + jobject xjmetadata = env2->NewObject(offlineRegionMetadataClass, offlineRegionMetadataConstructorId, xmetadata); + env2->SetObjectField(jregion, offlineRegionMetadataId, xjmetadata); + + // Moves the region on the stack into a heap-allocated one + env2->SetLongField(jregion, offlineRegionPtrId, + reinterpret_cast<jlong>(new mbgl::OfflineRegion(std::move(*region)))); + + // Invoke Java callback + env2->CallVoidMethod(createCallback, createOnCreateMethodId, jregion); + } + + // Delete global refs and detach when we're done + env2->DeleteGlobalRef(obj); + env2->DeleteGlobalRef(createCallback); + detach_jni_thread(theJVM, &env2, renderDetach); + }); +} + +void JNICALL setOfflineMapboxTileCountLimit(JNIEnv *env, jobject obj, jlong defaultFileSourcePtr, jlong limit) { + mbgl::Log::Debug(mbgl::Event::JNI, "setOfflineMapboxTileCountLimit"); + + // Checks + assert(defaultFileSourcePtr != 0); + assert(limit > 0); + + // Set limit + mbgl::DefaultFileSource *defaultFileSource = reinterpret_cast<mbgl::DefaultFileSource *>(defaultFileSourcePtr); + defaultFileSource->setOfflineMapboxTileCountLimit(limit); +} + +void JNICALL destroyOfflineRegion(JNIEnv *env, jobject obj, jlong offlineRegionPtr) { + mbgl::Log::Debug(mbgl::Event::JNI, "destroyOfflineRegion"); + assert(offlineRegionPtr != 0); + mbgl::OfflineRegion *offlineRegion = reinterpret_cast<mbgl::OfflineRegion *>(offlineRegionPtr); + delete offlineRegion; + offlineRegion = nullptr; +} + +void JNICALL setOfflineRegionObserver(JNIEnv *env, jobject obj, jobject offlineRegion_, jobject observerCallback) { + mbgl::Log::Debug(mbgl::Event::JNI, "setOfflineRegionObserver"); + + // Offline region + jlong offlineRegionPtr = env->GetLongField(offlineRegion_, offlineRegionPtrId); + mbgl::OfflineRegion *offlineRegion = reinterpret_cast<mbgl::OfflineRegion *>(offlineRegionPtr); + + // File source + jobject jmanager = env->GetObjectField(offlineRegion_, offlineRegionOfflineManagerId); + jlong defaultFileSourcePtr = env->GetLongField(jmanager, offlineManagerClassPtrId); + mbgl::DefaultFileSource *defaultFileSource = reinterpret_cast<mbgl::DefaultFileSource *>(defaultFileSourcePtr); + + // Define the observer + class Observer : public mbgl::OfflineRegionObserver { + public: + Observer(jobject observerCallback_) + : observerCallback(observerCallback_) { + } + + void statusChanged(mbgl::OfflineRegionStatus status) override { + // Env + JNIEnv* env2; + jboolean renderDetach = attach_jni_thread(theJVM, &env2, "Offline Thread"); + + // Conver to jint + jint downloadState; + switch(status.downloadState) { + case mbgl::OfflineRegionDownloadState::Inactive: + downloadState = 0; + break; + case mbgl::OfflineRegionDownloadState::Active: + downloadState = 1; + break; + } + + // Stats object + jobject jstatus = env2->NewObject(offlineRegionStatusClass, offlineRegionStatusConstructorId); + env2->SetIntField(jstatus, offlineRegionStatusDownloadStateId, downloadState); + env2->SetLongField(jstatus, offlineRegionStatusCompletedResourceCountId, status.completedResourceCount); + env2->SetLongField(jstatus, offlineRegionStatusCompletedResourceSizeId, status.completedResourceSize); + env2->SetLongField(jstatus, offlineRegionStatusRequiredResourceCountId, status.requiredResourceCount); + env2->SetBooleanField(jstatus, offlineRegionStatusRequiredResourceCountIsPreciseId, status.requiredResourceCountIsPrecise); + env2->CallVoidMethod(observerCallback, offlineRegionObserveronStatusChangedId, jstatus); + + // Delete global refs and detach when we're done + env2->DeleteGlobalRef(observerCallback); + detach_jni_thread(theJVM, &env2, renderDetach); + } + + void responseError(mbgl::Response::Error error) override { + // Env + JNIEnv* env2; + jboolean renderDetach = attach_jni_thread(theJVM, &env2, "Offline Thread"); + + // Handle the value of reason independently of the underlying int value + std::string errorReason; + switch(error.reason) { + case mbgl::Response::Error::Reason::Success: + errorReason = "REASON_SUCCESS"; + break; + case mbgl::Response::Error::Reason::NotFound: + errorReason = "REASON_NOT_FOUND"; + break; + case mbgl::Response::Error::Reason::Server: + errorReason = "REASON_SERVER"; + break; + case mbgl::Response::Error::Reason::Connection: + errorReason = "REASON_CONNECTION"; + break; + case mbgl::Response::Error::Reason::Other: + errorReason = "REASON_OTHER"; + break; + } + + // Error object + jobject jerror = env2->NewObject(offlineRegionErrorClass, offlineRegionErrorConstructorId); + env2->SetObjectField(jerror, offlineRegionErrorReasonId, std_string_to_jstring(env2, errorReason)); + env2->SetObjectField(jerror, offlineRegionErrorMessageId, std_string_to_jstring(env2, error.message)); + env2->CallVoidMethod(observerCallback, offlineRegionObserveronErrorId, jerror); + + // Delete global refs and detach when we're done + env2->DeleteGlobalRef(observerCallback); + detach_jni_thread(theJVM, &env2, renderDetach); + } + + void mapboxTileCountLimitExceeded(uint64_t limit) override { + // Env + JNIEnv* env2; + jboolean renderDetach = attach_jni_thread(theJVM, &env2, "Offline Thread"); + + // Send limit + env2->CallVoidMethod(observerCallback, offlineRegionObserveronLimitId, limit); + + // Delete global refs and detach when we're done + env2->DeleteGlobalRef(observerCallback); + detach_jni_thread(theJVM, &env2, renderDetach); + } + + jobject observerCallback; + }; + + // Makes sure the callback doesn't get GC'ed + observerCallback = reinterpret_cast<jobject>(env->NewGlobalRef(observerCallback)); + + // Set the observer + defaultFileSource->setOfflineRegionObserver(*offlineRegion, std::make_unique<Observer>(observerCallback)); +} + +void JNICALL setOfflineRegionDownloadState(JNIEnv *env, jobject obj, jobject offlineRegion_, jint offlineRegionDownloadState) { + mbgl::Log::Debug(mbgl::Event::JNI, "setOfflineRegionDownloadState"); + + // State + mbgl::OfflineRegionDownloadState state; + if (offlineRegionDownloadState == 0) { + state = mbgl::OfflineRegionDownloadState::Inactive; + } else if (offlineRegionDownloadState == 1) { + state = mbgl::OfflineRegionDownloadState::Active; + } else { + mbgl::Log::Error(mbgl::Event::JNI, "State can only be 0 (inactive) or 1 (active)."); + return; + } + + // Offline region + jlong offlineRegionPtr = env->GetLongField(offlineRegion_, offlineRegionPtrId); + mbgl::OfflineRegion *offlineRegion = reinterpret_cast<mbgl::OfflineRegion *>(offlineRegionPtr); + + // File source + jobject jmanager = env->GetObjectField(offlineRegion_, offlineRegionOfflineManagerId); + jlong defaultFileSourcePtr = env->GetLongField(jmanager, offlineManagerClassPtrId); + mbgl::DefaultFileSource *defaultFileSource = reinterpret_cast<mbgl::DefaultFileSource *>(defaultFileSourcePtr); + + // Set new state + defaultFileSource->setOfflineRegionDownloadState(*offlineRegion, state); +} + +void JNICALL getOfflineRegionStatus(JNIEnv *env, jobject obj, jobject offlineRegion_, jobject statusCallback) { + mbgl::Log::Debug(mbgl::Event::JNI, "getOfflineRegionStatus"); + + // Offline region + jlong offlineRegionPtr = env->GetLongField(offlineRegion_, offlineRegionPtrId); + mbgl::OfflineRegion *offlineRegion = reinterpret_cast<mbgl::OfflineRegion *>(offlineRegionPtr); + + // File source + jobject jmanager = env->GetObjectField(offlineRegion_, offlineRegionOfflineManagerId); + jlong defaultFileSourcePtr = env->GetLongField(jmanager, offlineManagerClassPtrId); + mbgl::DefaultFileSource *defaultFileSource = reinterpret_cast<mbgl::DefaultFileSource *>(defaultFileSourcePtr); + + // Makes sure the callback doesn't get GC'ed + statusCallback = reinterpret_cast<jobject>(env->NewGlobalRef(statusCallback)); + + // Set new state + defaultFileSource->getOfflineRegionStatus(*offlineRegion, [statusCallback](std::exception_ptr error, mbgl::optional<mbgl::OfflineRegionStatus> status) { + + // Reattach, the callback comes from a different thread + JNIEnv *env2; + jboolean renderDetach = attach_jni_thread(theJVM, &env2, "Offline Thread"); + if (renderDetach) { + mbgl::Log::Debug(mbgl::Event::JNI, "Attached."); + } + + if (error) { + std::string message = mbgl::util::toString(error); + env2->CallVoidMethod(statusCallback, offlineRegionStatusOnErrorId, std_string_to_jstring(env2, message)); + } else if (status) { + // Conver to jint + jint downloadState = -1; + if (status->downloadState == mbgl::OfflineRegionDownloadState::Inactive) { + downloadState = 0; + } else if (status->downloadState == mbgl::OfflineRegionDownloadState::Active) { + downloadState = 1; + } else { + mbgl::Log::Error(mbgl::Event::JNI, "Unsupported OfflineRegionDownloadState value."); + return; + } + + // Stats object + jobject jstatus = env2->NewObject(offlineRegionStatusClass, offlineRegionStatusConstructorId); + env2->SetIntField(jstatus, offlineRegionStatusDownloadStateId, downloadState); + env2->SetLongField(jstatus, offlineRegionStatusCompletedResourceCountId, status->completedResourceCount); + env2->SetLongField(jstatus, offlineRegionStatusCompletedResourceSizeId, status->completedResourceSize); + env2->SetLongField(jstatus, offlineRegionStatusRequiredResourceCountId, status->requiredResourceCount); + env2->SetBooleanField(jstatus, offlineRegionStatusRequiredResourceCountIsPreciseId, status->requiredResourceCountIsPrecise); + env2->CallVoidMethod(statusCallback, offlineRegionStatusOnStatusId, jstatus); + } + + // Delete global refs and detach when we're done + env2->DeleteGlobalRef(statusCallback); + detach_jni_thread(theJVM, &env2, renderDetach); + }); +} + +void JNICALL deleteOfflineRegion(JNIEnv *env, jobject obj, jobject offlineRegion_, jobject deleteCallback) { + mbgl::Log::Debug(mbgl::Event::JNI, "deleteOfflineRegion"); + + // Offline region + jlong offlineRegionPtr = env->GetLongField(offlineRegion_, offlineRegionPtrId); + mbgl::OfflineRegion *offlineRegion = reinterpret_cast<mbgl::OfflineRegion *>(offlineRegionPtr); + + // File source + jobject jmanager = env->GetObjectField(offlineRegion_, offlineRegionOfflineManagerId); + jlong defaultFileSourcePtr = env->GetLongField(jmanager, offlineManagerClassPtrId); + mbgl::DefaultFileSource *defaultFileSource = reinterpret_cast<mbgl::DefaultFileSource *>(defaultFileSourcePtr); + + // Makes sure the callback doesn't get GC'ed + deleteCallback = reinterpret_cast<jobject>(env->NewGlobalRef(deleteCallback)); + + // Set new state + defaultFileSource->deleteOfflineRegion(std::move(*offlineRegion), [deleteCallback](std::exception_ptr error) { + + // Reattach, the callback comes from a different thread + JNIEnv *env2; + jboolean renderDetach = attach_jni_thread(theJVM, &env2, "Offline Thread"); + if (renderDetach) { + mbgl::Log::Debug(mbgl::Event::JNI, "Attached."); + } + + if (error) { + std::string message = mbgl::util::toString(error); + env2->CallVoidMethod(deleteCallback, offlineRegionDeleteOnErrorId, std_string_to_jstring(env2, message)); + } else { + std::string message = mbgl::util::toString(error); + env2->CallVoidMethod(deleteCallback, offlineRegionDeleteOnDeleteId); + } + + // Delete global refs and detach when we're done + env2->DeleteGlobalRef(deleteCallback); + detach_jni_thread(theJVM, &env2, renderDetach); + }); +} + +// Offline calls end + } extern "C" { @@ -1946,63 +2454,356 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { httpContextClass = env->FindClass("com/mapbox/mapboxsdk/http/HTTPContext"); if (httpContextClass == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } httpContextGetInstanceId = env->GetStaticMethodID(httpContextClass, "getInstance", "()Lcom/mapbox/mapboxsdk/http/HTTPContext;"); if (httpContextGetInstanceId == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } httpContextCreateRequestId = env->GetMethodID(httpContextClass, "createRequest", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/mapbox/mapboxsdk/http/HTTPContext$HTTPRequest;"); if (httpContextCreateRequestId == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } httpRequestClass = env->FindClass("com/mapbox/mapboxsdk/http/HTTPContext$HTTPRequest"); if (httpRequestClass == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } httpRequestStartId = env->GetMethodID(httpRequestClass, "start", "()V"); if (httpRequestStartId == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } httpRequestCancelId = env->GetMethodID(httpRequestClass, "cancel", "()V"); if (httpRequestCancelId == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } customLayerClass = env->FindClass("com/mapbox/mapboxsdk/layers/CustomLayer"); if (customLayerClass == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } customLayerIdId = env->GetFieldID(customLayerClass, "mID", "Ljava/lang/String;"); if (customLayerIdId == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } customLayerContextId = env->GetFieldID(customLayerClass, "mContext", "J"); if (customLayerContextId == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } customLayerInitializeFunctionId = env->GetFieldID(customLayerClass, "mInitializeFunction", "J"); if (customLayerInitializeFunctionId == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } customLayerRenderFunctionId = env->GetFieldID(customLayerClass, "mRenderFunction", "J"); if (customLayerRenderFunctionId == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; } customLayerDeinitializeFunctionId = env->GetFieldID(customLayerClass, "mDeinitializeFunction", "J"); if (customLayerDeinitializeFunctionId == nullptr) { env->ExceptionDescribe(); + return JNI_ERR; + } + + // Offline definitions begin + + offlineManagerClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineManager"); + if (offlineManagerClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineManagerClassPtrId = env->GetFieldID(offlineManagerClass, "mDefaultFileSourcePtr", "J"); + if (offlineManagerClassPtrId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + listOfflineRegionsCallbackClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineManager$ListOfflineRegionsCallback"); + if (listOfflineRegionsCallbackClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + listOnListMethodId = env->GetMethodID(listOfflineRegionsCallbackClass, "onList", "([Lcom/mapbox/mapboxsdk/offline/OfflineRegion;)V"); + if (listOnListMethodId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + listOnErrorMethodId = env->GetMethodID(listOfflineRegionsCallbackClass, "onError", "(Ljava/lang/String;)V"); + if (listOnErrorMethodId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineRegion"); + if (offlineRegionClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionConstructorId = env->GetMethodID(offlineRegionClass, "<init>", "()V"); + if (offlineRegionConstructorId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionOfflineManagerId = env->GetFieldID(offlineRegionClass, "offlineManager", "Lcom/mapbox/mapboxsdk/offline/OfflineManager;"); + if (offlineRegionOfflineManagerId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionIdId = env->GetFieldID(offlineRegionClass, "mId", "J"); + if (offlineRegionIdId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDefinitionId = env->GetFieldID(offlineRegionClass, "mDefinition", "Lcom/mapbox/mapboxsdk/offline/OfflineRegionDefinition;"); + if (offlineRegionDefinitionId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionMetadataId = env->GetFieldID(offlineRegionClass, "mMetadata", "Lcom/mapbox/mapboxsdk/offline/OfflineRegionMetadata;"); + if (offlineRegionMetadataId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionPtrId = env->GetFieldID(offlineRegionClass, "mOfflineRegionPtr", "J"); + if (offlineRegionPtrId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + // This needs to be updated once we support more than one type of region definition + offlineRegionDefinitionClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineTilePyramidRegionDefinition"); + if (offlineRegionDefinitionClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDefinitionConstructorId = env->GetMethodID(offlineRegionDefinitionClass, "<init>", "()V"); + if (offlineRegionDefinitionConstructorId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDefinitionStyleURLId = env->GetFieldID(offlineRegionDefinitionClass, "styleURL", "Ljava/lang/String;"); + if (offlineRegionDefinitionStyleURLId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDefinitionBoundsId = env->GetFieldID(offlineRegionDefinitionClass, "bounds", "Lcom/mapbox/mapboxsdk/geometry/LatLngBounds;"); + if (offlineRegionDefinitionBoundsId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDefinitionMinZoomId = env->GetFieldID(offlineRegionDefinitionClass, "minZoom", "D"); + if (offlineRegionDefinitionMinZoomId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDefinitionMaxZoomId = env->GetFieldID(offlineRegionDefinitionClass, "maxZoom", "D"); + if (offlineRegionDefinitionMaxZoomId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDefinitionPixelRatioId = env->GetFieldID(offlineRegionDefinitionClass, "pixelRatio", "F"); + if (offlineRegionDefinitionPixelRatioId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionMetadataClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineRegionMetadata"); + if (offlineRegionMetadataClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionMetadataConstructorId = env->GetMethodID(offlineRegionMetadataClass, "<init>", "([B)V"); + if (offlineRegionMetadataConstructorId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionMetadataMetadataId = env->GetFieldID(offlineRegionMetadataClass, "metadata", "[B"); + if (offlineRegionMetadataMetadataId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + createOfflineRegionCallbackClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineManager$CreateOfflineRegionCallback"); + if (createOfflineRegionCallbackClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + createOnCreateMethodId = env->GetMethodID(createOfflineRegionCallbackClass, "onCreate", "(Lcom/mapbox/mapboxsdk/offline/OfflineRegion;)V"); + if (createOnCreateMethodId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + createOnErrorMethodId = env->GetMethodID(createOfflineRegionCallbackClass, "onError", "(Ljava/lang/String;)V"); + if (createOnErrorMethodId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionObserverClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionObserver"); + if (offlineRegionObserverClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; } + offlineRegionObserveronStatusChangedId = env->GetMethodID(offlineRegionObserverClass, "onStatusChanged", "(Lcom/mapbox/mapboxsdk/offline/OfflineRegionStatus;)V"); + if (offlineRegionObserveronStatusChangedId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionObserveronErrorId = env->GetMethodID(offlineRegionObserverClass, "onError", "(Lcom/mapbox/mapboxsdk/offline/OfflineRegionError;)V"); + if (offlineRegionObserveronErrorId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionObserveronLimitId = env->GetMethodID(offlineRegionObserverClass, "mapboxTileCountLimitExceeded", "(J)V"); + if (offlineRegionObserveronLimitId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineRegionStatus"); + if (offlineRegionStatusClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusConstructorId = env->GetMethodID(offlineRegionStatusClass, "<init>", "()V"); + if (offlineRegionStatusConstructorId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusDownloadStateId = env->GetFieldID(offlineRegionStatusClass, "downloadState", "I"); + if (offlineRegionStatusDownloadStateId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusCompletedResourceCountId = env->GetFieldID(offlineRegionStatusClass, "completedResourceCount", "J"); + if (offlineRegionStatusCompletedResourceCountId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusCompletedResourceSizeId = env->GetFieldID(offlineRegionStatusClass, "completedResourceSize", "J"); + if (offlineRegionStatusCompletedResourceSizeId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusRequiredResourceCountId = env->GetFieldID(offlineRegionStatusClass, "requiredResourceCount", "J"); + if (offlineRegionStatusRequiredResourceCountId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusRequiredResourceCountIsPreciseId = env->GetFieldID(offlineRegionStatusClass, "requiredResourceCountIsPrecise", "Z"); + if (offlineRegionStatusRequiredResourceCountIsPreciseId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionErrorClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineRegionError"); + if (offlineRegionErrorClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionErrorConstructorId = env->GetMethodID(offlineRegionErrorClass, "<init>", "()V"); + if (offlineRegionErrorConstructorId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionErrorReasonId = env->GetFieldID(offlineRegionErrorClass, "reason", "Ljava/lang/String;"); + if (offlineRegionErrorReasonId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionErrorMessageId = env->GetFieldID(offlineRegionErrorClass, "message", "Ljava/lang/String;"); + if (offlineRegionErrorMessageId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusCallbackClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionStatusCallback"); + if (offlineRegionStatusCallbackClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusOnStatusId = env->GetMethodID(offlineRegionStatusCallbackClass, "onStatus", "(Lcom/mapbox/mapboxsdk/offline/OfflineRegionStatus;)V"); + if (offlineRegionStatusOnStatusId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionStatusOnErrorId = env->GetMethodID(offlineRegionStatusCallbackClass, "onError", "(Ljava/lang/String;)V"); + if (offlineRegionStatusOnErrorId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDeleteCallbackClass = env->FindClass("com/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionDeleteCallback"); + if (offlineRegionDeleteCallbackClass == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDeleteOnDeleteId = env->GetMethodID(offlineRegionDeleteCallbackClass, "onDelete", "()V"); + if (offlineRegionDeleteOnDeleteId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + offlineRegionDeleteOnErrorId = env->GetMethodID(offlineRegionDeleteCallbackClass, "onError", "(Ljava/lang/String;)V"); + if (offlineRegionDeleteOnErrorId == nullptr) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + // Offline definitions end + const std::vector<JNINativeMethod> methods = { {"nativeCreate", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;FIJ)J", reinterpret_cast<void *>(&nativeCreate)}, @@ -2139,6 +2940,37 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { return JNI_ERR; } + // Offline register begin + + const std::vector<JNINativeMethod> offlineManagerMethods = { + {"createDefaultFileSource", "(Ljava/lang/String;Ljava/lang/String;J)J", reinterpret_cast<void *>(&createDefaultFileSource)}, + {"setAccessToken", "(JLjava/lang/String;)V", reinterpret_cast<void *>(&setAccessToken)}, + {"getAccessToken", "(J)Ljava/lang/String;", reinterpret_cast<void *>(&getAccessToken)}, + {"listOfflineRegions", "(JLcom/mapbox/mapboxsdk/offline/OfflineManager$ListOfflineRegionsCallback;)V", reinterpret_cast<void *>(&listOfflineRegions)}, + {"createOfflineRegion", "(JLcom/mapbox/mapboxsdk/offline/OfflineRegionDefinition;Lcom/mapbox/mapboxsdk/offline/OfflineRegionMetadata;Lcom/mapbox/mapboxsdk/offline/OfflineManager$CreateOfflineRegionCallback;)V", reinterpret_cast<void *>(&createOfflineRegion)}, + {"setOfflineMapboxTileCountLimit", "(JJ)V", reinterpret_cast<void *>(&setOfflineMapboxTileCountLimit)} + }; + + if (env->RegisterNatives(offlineManagerClass, offlineManagerMethods.data(), offlineManagerMethods.size()) < 0) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + const std::vector<JNINativeMethod> offlineRegionMethods = { + {"destroyOfflineRegion", "(J)V", reinterpret_cast<void *>(&destroyOfflineRegion)}, + {"setOfflineRegionObserver", "(Lcom/mapbox/mapboxsdk/offline/OfflineRegion;Lcom/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionObserver;)V", reinterpret_cast<void *>(&setOfflineRegionObserver)}, + {"setOfflineRegionDownloadState", "(Lcom/mapbox/mapboxsdk/offline/OfflineRegion;I)V", reinterpret_cast<void *>(&setOfflineRegionDownloadState)}, + {"getOfflineRegionStatus", "(Lcom/mapbox/mapboxsdk/offline/OfflineRegion;Lcom/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionStatusCallback;)V", reinterpret_cast<void *>(&getOfflineRegionStatus)}, + {"deleteOfflineRegion", "(Lcom/mapbox/mapboxsdk/offline/OfflineRegion;Lcom/mapbox/mapboxsdk/offline/OfflineRegion$OfflineRegionDeleteCallback;)V", reinterpret_cast<void *>(&deleteOfflineRegion)} + }; + + if (env->RegisterNatives(offlineRegionClass, offlineRegionMethods.data(), offlineRegionMethods.size()) < 0) { + env->ExceptionDescribe(); + return JNI_ERR; + } + + // Offline register end + latLngClass = reinterpret_cast<jclass>(env->NewGlobalRef(latLngClass)); if (latLngClass == nullptr) { env->ExceptionDescribe(); @@ -2313,6 +3145,264 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { env->DeleteGlobalRef(httpContextClass); } + // Offline global definitions begin + + offlineManagerClass = reinterpret_cast<jclass>(env->NewGlobalRef(offlineManagerClass)); + if (offlineManagerClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(httpRequestClass); + } + + listOfflineRegionsCallbackClass = reinterpret_cast<jclass>(env->NewGlobalRef(listOfflineRegionsCallbackClass)); + if (listOfflineRegionsCallbackClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + } + + offlineRegionClass = reinterpret_cast<jclass>(env->NewGlobalRef(offlineRegionClass)); + if (offlineRegionClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + } + + offlineRegionDefinitionClass = reinterpret_cast<jclass>(env->NewGlobalRef(offlineRegionDefinitionClass)); + if (offlineRegionDefinitionClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + env->DeleteGlobalRef(offlineRegionClass); + } + + offlineRegionMetadataClass = reinterpret_cast<jclass>(env->NewGlobalRef(offlineRegionMetadataClass)); + if (offlineRegionMetadataClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + env->DeleteGlobalRef(offlineRegionClass); + env->DeleteGlobalRef(offlineRegionDefinitionClass); + } + + createOfflineRegionCallbackClass = reinterpret_cast<jclass>(env->NewGlobalRef(createOfflineRegionCallbackClass)); + if (createOfflineRegionCallbackClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + env->DeleteGlobalRef(offlineRegionClass); + env->DeleteGlobalRef(offlineRegionDefinitionClass); + env->DeleteGlobalRef(offlineRegionMetadataClass); + } + + offlineRegionObserverClass = reinterpret_cast<jclass>(env->NewGlobalRef(offlineRegionObserverClass)); + if (offlineRegionObserverClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + env->DeleteGlobalRef(offlineRegionClass); + env->DeleteGlobalRef(offlineRegionDefinitionClass); + env->DeleteGlobalRef(offlineRegionMetadataClass); + env->DeleteGlobalRef(createOfflineRegionCallbackClass); + } + + offlineRegionStatusClass = reinterpret_cast<jclass>(env->NewGlobalRef(offlineRegionStatusClass)); + if (offlineRegionStatusClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + env->DeleteGlobalRef(offlineRegionClass); + env->DeleteGlobalRef(offlineRegionDefinitionClass); + env->DeleteGlobalRef(offlineRegionMetadataClass); + env->DeleteGlobalRef(createOfflineRegionCallbackClass); + env->DeleteGlobalRef(offlineRegionObserverClass); + } + + offlineRegionErrorClass = reinterpret_cast<jclass>(env->NewGlobalRef(offlineRegionErrorClass)); + if (offlineRegionErrorClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + env->DeleteGlobalRef(offlineRegionClass); + env->DeleteGlobalRef(offlineRegionDefinitionClass); + env->DeleteGlobalRef(offlineRegionMetadataClass); + env->DeleteGlobalRef(createOfflineRegionCallbackClass); + env->DeleteGlobalRef(offlineRegionObserverClass); + env->DeleteGlobalRef(offlineRegionStatusClass); + } + + offlineRegionStatusCallbackClass = reinterpret_cast<jclass>(env->NewGlobalRef(offlineRegionStatusCallbackClass)); + if (offlineRegionStatusCallbackClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + env->DeleteGlobalRef(offlineRegionClass); + env->DeleteGlobalRef(offlineRegionDefinitionClass); + env->DeleteGlobalRef(offlineRegionMetadataClass); + env->DeleteGlobalRef(createOfflineRegionCallbackClass); + env->DeleteGlobalRef(offlineRegionObserverClass); + env->DeleteGlobalRef(offlineRegionStatusClass); + env->DeleteGlobalRef(offlineRegionErrorClass); + } + + offlineRegionDeleteCallbackClass = reinterpret_cast<jclass>(env->NewGlobalRef(offlineRegionDeleteCallbackClass)); + if (offlineRegionDeleteCallbackClass == nullptr) { + env->ExceptionDescribe(); + env->DeleteGlobalRef(latLngClass); + env->DeleteGlobalRef(latLngBoundsClass); + env->DeleteGlobalRef(iconClass); + env->DeleteGlobalRef(markerClass); + env->DeleteGlobalRef(polylineClass); + env->DeleteGlobalRef(polygonClass); + env->DeleteGlobalRef(runtimeExceptionClass); + env->DeleteGlobalRef(nullPointerExceptionClass); + env->DeleteGlobalRef(arrayListClass); + env->DeleteGlobalRef(projectedMetersClass); + env->DeleteGlobalRef(pointFClass); + env->DeleteGlobalRef(rectFClass); + env->DeleteGlobalRef(httpContextClass); + env->DeleteGlobalRef(offlineManagerClass); + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + env->DeleteGlobalRef(offlineRegionClass); + env->DeleteGlobalRef(offlineRegionDefinitionClass); + env->DeleteGlobalRef(offlineRegionMetadataClass); + env->DeleteGlobalRef(createOfflineRegionCallbackClass); + env->DeleteGlobalRef(offlineRegionObserverClass); + env->DeleteGlobalRef(offlineRegionStatusClass); + env->DeleteGlobalRef(offlineRegionErrorClass); + env->DeleteGlobalRef(offlineRegionStatusCallbackClass); + } + + // Offline global definitions end + char release[PROP_VALUE_MAX] = ""; __system_property_get("ro.build.version.release", release); androidRelease = std::string(release); @@ -2415,6 +3505,68 @@ extern "C" JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) { httpRequestStartId = nullptr; httpRequestCancelId = nullptr; + // Offline delete begins + + env->DeleteGlobalRef(offlineManagerClass); + offlineManagerClassPtrId = nullptr; + offlineManagerClassPtrId = nullptr; + + env->DeleteGlobalRef(listOfflineRegionsCallbackClass); + listOnListMethodId = nullptr; + listOnErrorMethodId = nullptr; + + env->DeleteGlobalRef(offlineRegionClass); + offlineRegionConstructorId = nullptr; + offlineRegionOfflineManagerId = nullptr; + offlineRegionIdId = nullptr; + offlineRegionDefinitionId = nullptr; + offlineRegionMetadataId = nullptr; + offlineRegionPtrId = nullptr; + + env->DeleteGlobalRef(offlineRegionDefinitionClass); + offlineRegionDefinitionConstructorId = nullptr; + offlineRegionDefinitionStyleURLId = nullptr; + offlineRegionDefinitionBoundsId = nullptr; + offlineRegionDefinitionMinZoomId = nullptr; + offlineRegionDefinitionMaxZoomId = nullptr; + offlineRegionDefinitionPixelRatioId = nullptr; + + env->DeleteGlobalRef(offlineRegionMetadataClass); + offlineRegionMetadataConstructorId = nullptr; + offlineRegionMetadataMetadataId = nullptr; + + env->DeleteGlobalRef(createOfflineRegionCallbackClass); + createOnCreateMethodId = nullptr; + createOnErrorMethodId = nullptr; + + env->DeleteGlobalRef(offlineRegionObserverClass); + offlineRegionObserveronStatusChangedId = nullptr; + offlineRegionObserveronErrorId = nullptr; + offlineRegionObserveronLimitId = nullptr; + + env->DeleteGlobalRef(offlineRegionStatusClass); + offlineRegionStatusConstructorId = nullptr; + offlineRegionStatusDownloadStateId = nullptr; + offlineRegionStatusCompletedResourceCountId = nullptr; + offlineRegionStatusCompletedResourceSizeId = nullptr; + offlineRegionStatusRequiredResourceCountId = nullptr; + offlineRegionStatusRequiredResourceCountIsPreciseId = nullptr; + + env->DeleteGlobalRef(offlineRegionErrorClass); + offlineRegionErrorConstructorId = nullptr; + offlineRegionErrorReasonId = nullptr; + offlineRegionErrorMessageId = nullptr; + + env->DeleteGlobalRef(offlineRegionStatusCallbackClass); + offlineRegionStatusOnStatusId = nullptr; + offlineRegionStatusOnErrorId = nullptr; + + env->DeleteGlobalRef(offlineRegionDeleteCallbackClass); + offlineRegionDeleteOnDeleteId = nullptr; + offlineRegionDeleteOnErrorId = nullptr; + + // Offline delete ends + theJVM = nullptr; } } diff --git a/platform/android/src/jni.hpp b/platform/android/src/jni.hpp index ae0624c4b3..2995ea0b35 100644 --- a/platform/android/src/jni.hpp +++ b/platform/android/src/jni.hpp @@ -103,6 +103,66 @@ extern jclass httpRequestClass; extern jmethodID httpRequestStartId; extern jmethodID httpRequestCancelId; +// Offline declarations start + +extern jclass offlineManagerClass; +extern jfieldID offlineManagerClassPtrId; + +extern jclass listOfflineRegionsCallbackClass; +extern jmethodID listOnListMethodId; +extern jmethodID listOnErrorMethodId; + +extern jclass offlineRegionClass; +extern jfieldID offlineRegionOfflineManagerId; +extern jfieldID offlineRegionIdId; +extern jfieldID offlineRegionDefinitionId; +extern jfieldID offlineRegionMetadataId; +extern jfieldID offlineRegionPtrId; + +extern jclass offlineRegionDefinitionClass; +extern jmethodID offlineRegionDefinitionConstructorId; +extern jfieldID offlineRegionDefinitionStyleURLId; +extern jfieldID offlineRegionDefinitionBoundsId; +extern jfieldID offlineRegionDefinitionMinZoomId; +extern jfieldID offlineRegionDefinitionMaxZoomId; +extern jfieldID offlineRegionDefinitionPixelRatioId; + +extern jclass offlineRegionMetadataClass; +extern jmethodID offlineRegionMetadataConstructorId; +extern jfieldID offlineRegionMetadataMetadataId; + +extern jclass createOfflineRegionCallbackClass; +extern jmethodID createOnCreateMethodId; +extern jmethodID createOnErrorMethodId; + +extern jclass offlineRegionObserverClass; +extern jmethodID offlineRegionObserveronStatusChangedId; +extern jmethodID offlineRegionObserveronErrorId; +extern jmethodID offlineRegionObserveronLimitId; + +extern jclass offlineRegionStatusClass; +extern jmethodID offlineRegionStatusConstructorId; +extern jfieldID offlineRegionStatusDownloadStateId; +extern jfieldID offlineRegionStatusCompletedResourceCountId; +extern jfieldID offlineRegionStatusCompletedResourceSizeId; +extern jfieldID offlineRegionStatusRequiredResourceCountId; +extern jfieldID offlineRegionStatusRequiredResourceCountIsIndeterminateId; + +extern jclass offlineRegionErrorClass; +extern jmethodID offlineRegionErrorConstructorId; +extern jfieldID offlineRegionErrorReasonId; +extern jfieldID offlineRegionErrorMessageId; + +extern jclass offlineRegionStatusCallbackClass; +extern jmethodID offlineRegionStatusOnStatusId; +extern jmethodID offlineRegionStatusOnErrorId; + +extern jclass offlineRegionDeleteCallbackClass; +extern jmethodID offlineRegionDeleteOnDeleteId; +extern jmethodID offlineRegionDeleteOnErrorId; + +// Offline declarations end + extern bool throw_jni_error(JNIEnv *env, const char *msg); extern bool attach_jni_thread(JavaVM* vm, JNIEnv** env, std::string threadName); extern void detach_jni_thread(JavaVM* vm, JNIEnv** env, bool detach); |