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 0c2690aec..6b213bb65 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) {
+ /**
+ *
+ * Sets the minimum zoom level the map can be displayed at.
+ *
+ *
+ * @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) {
+ /**
+ *
+ * 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.
+ *
+ * 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 6c9806f79..953009613 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 000000000..b56ecfc05
--- /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 000000000..9e518f1e6
--- /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 000000000..0e7fb38e1
--- /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 000000000..e7a57379c
--- /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 000000000..bbea4580f
--- /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 000000000..e65a20f18
--- /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 000000000..5a0be6b33
--- /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 4b44efdfe..37bf928a5 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">
-
@@ -91,22 +90,24 @@
android:label="@string/activity_dynamic_marker" />
+ android:label="@string/activity_map_padding"
+ android:screenOrientation="portrait" />
+
-
-
+
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 d1ac656cd..451876e39 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 000000000..64ecc2100
--- /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 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 data = (HashMap) 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 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 000000000..81b681cc5
--- /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 000000000..50df71ad0
--- /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 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 000000000..55a8d22a5
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/drawable/ic_airplanemode_active_black_24dp.xml
@@ -0,0 +1,12 @@
+
+
+
+
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 ce055138b..0b318d781 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 @@
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+
+
+
+
+
+
+
+
+
+
+
+
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 512d19559..92ec45b8c 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_drawer.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_drawer.xml
@@ -72,6 +72,12 @@
android:title="@string/navdrawer_menu_title_individual_tests">