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 index 535107c52..5bd0dd4bf 100644 --- 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 @@ -4,11 +4,10 @@ import android.annotation.SuppressLint; import android.content.Context; import android.os.Handler; import android.os.Looper; -import android.support.annotation.AnyThread; import android.support.annotation.Keep; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.UiThread; - import com.mapbox.mapboxsdk.LibraryLoader; import com.mapbox.mapboxsdk.Mapbox; import com.mapbox.mapboxsdk.R; @@ -54,7 +53,7 @@ public class OfflineManager { private final FileSource fileSource; // Makes sure callbacks come back to the main thread - private Handler handler; + private final Handler handler = new Handler(Looper.getMainLooper()); // This object is implemented as a singleton @SuppressLint("StaticFieldLeak") @@ -157,15 +156,6 @@ public class OfflineManager { return instance; } - @AnyThread - private Handler getHandler() { - if (handler == null) { - handler = new Handler(Looper.getMainLooper()); - } - - return handler; - } - /** * Retrieve all regions in the offline database. *
@@ -181,7 +171,7 @@ public class OfflineManager { @Override public void onList(final OfflineRegion[] offlineRegions) { - getHandler().post(new Runnable() { + handler.post(new Runnable() { @Override public void run() { fileSource.deactivate(); @@ -192,7 +182,7 @@ public class OfflineManager { @Override public void onError(final String error) { - getHandler().post(new Runnable() { + handler.post(new Runnable() { @Override public void run() { fileSource.deactivate(); @@ -234,7 +224,7 @@ public class OfflineManager { public void run() { String errorMessage = null; if (src.canWrite()) { - getHandler().post(new Runnable() { + handler.post(new Runnable() { @Override public void run() { // path writable, merge and update schema in place if necessary @@ -246,7 +236,7 @@ public class OfflineManager { final File dst = new File(FileSource.getInternalCachePath(context), src.getName()); try { copyTempDatabaseFile(src, dst); - getHandler().post(new Runnable() { + handler.post(new Runnable() { @Override public void run() { // merge and update schema using the copy @@ -264,7 +254,7 @@ public class OfflineManager { if (errorMessage != null) { final String finalErrorMessage = errorMessage; - getHandler().post(new Runnable() { + handler.post(new Runnable() { @Override public void run() { callback.onError(finalErrorMessage); @@ -275,6 +265,219 @@ public class OfflineManager { }).start(); } + /** + * Delete existing database and re-initialize. + *
+ * When the operation is complete or encounters an error, the given callback will be + * executed on the database thread; it is the responsibility of the SDK bindings + * to re-execute a user-provided callback on the main thread. + *
+ * + * @param callback the callback to be invoked when the database was reset or when the operation erred. + */ + public void resetDatabase(@Nullable final FileSourceCallback callback) { + fileSource.activate(); + nativeResetDatabase(new FileSourceCallback() { + @Override + public void onSuccess() { + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onSuccess(); + } + } + }); + } + + @Override + public void onError(@NonNull final String message) { + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onError(message); + } + } + }); + } + }); + } + + /** + * Forces revalidation of the ambient cache. + *+ * Forces Mapbox GL Native to revalidate resources stored in the ambient + * cache with the tile server before using them, making sure they + * are the latest version. This is more efficient than cleaning the + * cache because if the resource is considered valid after the server + * lookup, it will not get downloaded again. + *
+ * Resources overlapping with offline regions will not be affected + * by this call. + *
+ * + * @param callback the callback to be invoked when the ambient cache was invalidated or when the operation erred. + */ + public void invalidateAmbientCache(@Nullable final FileSourceCallback callback) { + fileSource.activate(); + nativeInvalidateAmbientCache(new FileSourceCallback() { + @Override + public void onSuccess() { + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onSuccess(); + } + } + }); + } + + @Override + public void onError(@NonNull final String message) { + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onError(message); + } + } + }); + } + }); + } + + /** + * Erase resources from the ambient cache, freeing storage space. + *+ * Erases the ambient cache, freeing resources. This operation can be + * potentially slow because it will trigger a VACUUM on SQLite, + * forcing the database to move pages on the filesystem. + *
+ *+ * Resources overlapping with offline regions will not be affected + * by this call. + *
+ * + * @param callback the callback to be invoked when the ambient cache was cleared or when the operation erred. + */ + public void clearAmbientCache(@Nullable final FileSourceCallback callback) { + fileSource.activate(); + nativeClearAmbientCache(new FileSourceCallback() { + @Override + public void onSuccess() { + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onSuccess(); + } + } + }); + } + + @Override + public void onError(@NonNull final String message) { + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onError(message); + } + } + }); + } + }); + } + + /** + * Sets the maximum size in bytes for the ambient cache. + *+ * This call is potentially expensive because it will try + * to trim the data in case the database is larger than the + * size defined. The size of offline regions are not affected + * by this settings, but the ambient cache will always try + * to not exceed the maximum size defined, taking into account + * the current size for the offline regions. + *
+ *+ * Note that if you use the SDK's offline functionality, your ability to set the ambient cache size will be limited. + * Space that offline regions take up detract from the space available for ambient caching, and the ambient cache + * size does not block offline downloads. For example: if the maximum cache size is set to 50 MB and 40 MB are + * already used by offline regions, the ambient cache size will effectively be 10 MB. + *
+ *+ * Setting the size to 0 will disable the cache if there is no + * offline region on the database. + *
+ * <[ + *+ * This method should always be called at the start of an app, before setting the style and loading a map. + * Otherwise, the map will instantiate with the default cache size of 50 MB. + *
+ * + * @param size the maximum size of the ambient cache + * @param callback the callback to be invoked when the the maximum size has been set or when the operation erred. + */ + public void setMaximumAmbientCacheSize(long size, @Nullable final FileSourceCallback callback) { + fileSource.activate(); + nativeSetMaximumAmbientCacheSize(size, new FileSourceCallback() { + @Override + public void onSuccess() { + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onSuccess(); + } + } + }); + } + + @Override + public void onError(@NonNull final String message) { + fileSource.activate(); + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onError(message); + } + } + }); + } + }); + } + + /** + * This callback receives an asynchronous response indicating if an operation has succeeded or failed. + */ + @Keep + public interface FileSourceCallback { + + /** + * Receives the success of an operation + */ + void onSuccess(); + + /** + * Receives an error message if an operation was not successful + * + * @param message the error message + */ + void onError(@NonNull String message); + + } + private static void copyTempDatabaseFile(@NonNull File sourceFile, File destFile) throws IOException { if (!destFile.exists() && !destFile.createNewFile()) { throw new IOException("Unable to copy database file for merge."); @@ -308,7 +511,7 @@ public class OfflineManager { if (isTemporaryFile) { file.delete(); } - getHandler().post(new Runnable() { + handler.post(new Runnable() { @Override public void run() { fileSource.deactivate(); @@ -322,7 +525,7 @@ public class OfflineManager { if (isTemporaryFile) { file.delete(); } - getHandler().post(new Runnable() { + handler.post(new Runnable() { @Override public void run() { fileSource.deactivate(); @@ -365,7 +568,7 @@ public class OfflineManager { @Override public void onCreate(final OfflineRegion offlineRegion) { - getHandler().post(new Runnable() { + handler.post(new Runnable() { @Override public void run() { ConnectivityReceiver.instance(context).deactivate(); @@ -377,7 +580,7 @@ public class OfflineManager { @Override public void onError(final String error) { - getHandler().post(new Runnable() { + handler.post(new Runnable() { @Override public void run() { ConnectivityReceiver.instance(context).deactivate(); @@ -431,6 +634,18 @@ public class OfflineManager { @Keep private native void mergeOfflineRegions(FileSource fileSource, String path, MergeOfflineRegionsCallback callback); + @Keep + private native void nativeResetDatabase(@Nullable FileSourceCallback callback); + + @Keep + private native void nativeInvalidateAmbientCache(@Nullable FileSourceCallback callback); + + @Keep + private native void nativeClearAmbientCache(@Nullable FileSourceCallback callback); + + @Keep + private native void nativeSetMaximumAmbientCacheSize(long size, @Nullable FileSourceCallback callback); + /** * Insert the provided resource into the ambient cache * This method mimics the caching that would take place if the equivalent 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 index 863219854..2217850a2 100644 --- 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 @@ -7,7 +7,6 @@ import android.support.annotation.IntDef; import android.support.annotation.Keep; import android.support.annotation.NonNull; import android.support.annotation.Nullable; - import com.mapbox.mapboxsdk.LibraryLoader; import com.mapbox.mapboxsdk.Mapbox; import com.mapbox.mapboxsdk.net.ConnectivityReceiver; @@ -147,6 +146,25 @@ public class OfflineRegion { void onError(String error); } + /** + * This callback receives an asynchronous response containing a notification when + * an offline region has been invalidated, or a {@link String} error message otherwise. + */ + @Keep + public interface OfflineRegionInvalidateCallback { + /** + * Receives the invalidate notification + */ + void onInvalidate(); + + /** + * Receives the error message + * + * @param error the error message + */ + void onError(String error); + } + /** * This callback receives an asynchronous response containing the newly update * OfflineMetadata in the database, or an error message otherwise. @@ -337,14 +355,14 @@ public class OfflineRegion { * @param callback the callback to invoked. */ public void getStatus(@NonNull final OfflineRegionStatusCallback callback) { - FileSource.getInstance(Mapbox.getApplicationContext()).activate(); + fileSource.activate(); getOfflineRegionStatus(new OfflineRegionStatusCallback() { @Override public void onStatus(final OfflineRegionStatus status) { handler.post(new Runnable() { @Override public void run() { - FileSource.getInstance(Mapbox.getApplicationContext()).deactivate(); + fileSource.deactivate(); callback.onStatus(status); } }); @@ -355,7 +373,7 @@ public class OfflineRegion { handler.post(new Runnable() { @Override public void run() { - FileSource.getInstance(Mapbox.getApplicationContext()).deactivate(); + fileSource.deactivate(); callback.onError(error); } }); @@ -383,14 +401,14 @@ public class OfflineRegion { public void delete(@NonNull final OfflineRegionDeleteCallback callback) { if (!isDeleted) { isDeleted = true; - FileSource.getInstance(Mapbox.getApplicationContext()).activate(); + fileSource.activate(); deleteOfflineRegion(new OfflineRegionDeleteCallback() { @Override public void onDelete() { handler.post(new Runnable() { @Override public void run() { - FileSource.getInstance(Mapbox.getApplicationContext()).deactivate(); + fileSource.deactivate(); callback.onDelete(); OfflineRegion.this.finalize(); } @@ -403,7 +421,7 @@ public class OfflineRegion { @Override public void run() { isDeleted = false; - FileSource.getInstance(Mapbox.getApplicationContext()).deactivate(); + fileSource.deactivate(); callback.onError(error); } }); @@ -412,6 +430,46 @@ public class OfflineRegion { } } + /** + * Invalidate all the tiles from an offline region forcing Mapbox GL to revalidate + * the tiles with the server before using. This is more efficient than deleting the + * offline region and downloading it again because if the data on the cache matches + * the server, no new data gets transmitted. + * + * @param callback the callback to be invoked + */ + public void invalidate(@Nullable final OfflineRegionInvalidateCallback callback) { + fileSource.activate(); + invalidateOfflineRegion(new OfflineRegionInvalidateCallback() { + + @Override + public void onInvalidate() { + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onInvalidate(); + } + } + }); + } + + @Override + public void onError(@NonNull final String message) { + handler.post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (callback != null) { + callback.onError(message); + } + } + }); + } + }); + } + /** * Update an offline region metadata from the database. *
@@ -469,4 +527,7 @@ public class OfflineRegion {
@Keep
private native void updateOfflineRegionMetadata(byte[] metadata, OfflineRegionUpdateMetadataCallback callback);
+ @Keep
+ private native void invalidateOfflineRegion(OfflineRegionInvalidateCallback callback);
+
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java
index b3b7b6183..cdf197411 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java
@@ -11,7 +11,6 @@ import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
-
import com.mapbox.mapboxsdk.MapStrictMode;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.constants.MapboxConstants;
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/CacheTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/CacheTest.kt
new file mode 100644
index 000000000..299e193c9
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/CacheTest.kt
@@ -0,0 +1,89 @@
+package com.mapbox.mapboxsdk.testapp.offline
+
+import android.content.Context
+import android.support.test.rule.ActivityTestRule
+import android.support.test.runner.AndroidJUnit4
+import com.mapbox.mapboxsdk.offline.OfflineManager
+import com.mapbox.mapboxsdk.testapp.activity.FeatureOverviewActivity
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+
+@RunWith(AndroidJUnit4::class)
+class CacheTest {
+
+ @Rule
+ @JvmField
+ var rule = ActivityTestRule(FeatureOverviewActivity::class.java)
+
+ private val context: Context by lazy { rule.activity }
+
+ private val countDownLatch = CountDownLatch(1)
+
+ @Test
+ fun testSetMaximumAmbientCacheSize() {
+ rule.activity.runOnUiThread {
+ OfflineManager.getInstance(context).setMaximumAmbientCacheSize(10000000, object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ countDownLatch.countDown()
+ }
+
+ override fun onError(message: String) {
+ Assert.assertNull("onError should not be called", message)
+ }
+ })
+ }
+ countDownLatch.await()
+ }
+
+ @Test
+ fun testSetClearAmbientCache() {
+ rule.activity.runOnUiThread {
+ OfflineManager.getInstance(context).clearAmbientCache(object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ countDownLatch.countDown()
+ }
+
+ override fun onError(message: String) {
+ Assert.assertNull("onError should not be called", message)
+ }
+ })
+ }
+ countDownLatch.await()
+ }
+
+ @Test
+ fun testSetInvalidateAmbientCache() {
+ rule.activity.runOnUiThread {
+ OfflineManager.getInstance(context).invalidateAmbientCache(object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ countDownLatch.countDown()
+ }
+
+ override fun onError(message: String) {
+ Assert.assertNull("onError should not be called", message)
+ }
+ })
+ }
+ countDownLatch.await()
+ }
+
+ @Test
+ fun testSetResetDatabase() {
+ rule.activity.runOnUiThread {
+ OfflineManager.getInstance(context).resetDatabase(object : OfflineManager.FileSourceCallback {
+ override fun onSuccess() {
+ countDownLatch.countDown()
+ }
+
+ override fun onError(message: String) {
+ Assert.assertNull("onError should not be called", message)
+ }
+ })
+ }
+ countDownLatch.await()
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt
index 3f9893752..6b73623ae 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt
@@ -35,7 +35,7 @@ class OfflineManagerTest : AppCenter() {
@Test(timeout = 30_000)
fun a_copyFileFromAssets() {
val latch = CountDownLatch(1)
- rule.runOnUiThread {
+ rule.activity.runOnUiThread {
FileUtils.CopyFileFromAssetsTask(rule.activity, object : FileUtils.OnFileCopiedFromAssetsListener {
override fun onFileCopiedFromAssets() {
latch.countDown()
@@ -52,7 +52,7 @@ class OfflineManagerTest : AppCenter() {
@Test(timeout = 30_000)
fun b_mergeRegion() {
val latch = CountDownLatch(1)
- rule.runOnUiThread {
+ rule.activity.runOnUiThread {
OfflineManager.getInstance(context).mergeOfflineRegions(
FileSource.getResourcesCachePath(rule.activity) + "/" + TEST_DB_FILE_NAME,
object : OfflineManager.MergeOfflineRegionsCallback {
@@ -72,7 +72,7 @@ class OfflineManagerTest : AppCenter() {
@Test(timeout = 30_000)
fun c_listRegion() {
val latch = CountDownLatch(1)
- rule.runOnUiThread {
+ rule.activity.runOnUiThread {
OfflineManager.getInstance(context).listOfflineRegions(object : OfflineManager.ListOfflineRegionsCallback {
override fun onList(offlineRegions: Array