diff --git a/include/mbgl/storage/database_file_source.hpp b/include/mbgl/storage/database_file_source.hpp index f591e2751..3c3bcbeea 100644 --- a/include/mbgl/storage/database_file_source.hpp +++ b/include/mbgl/storage/database_file_source.hpp @@ -136,6 +136,17 @@ public: */ virtual void listOfflineRegions(std::function)>); + /** + * Retrieve given region in the offline database. + * + * The query will be executed asynchronously and the results passed to the + * given callback, which 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. + */ + virtual void getOfflineRegion(int64_t regionID, + std::function, std::exception_ptr>)>); + /** * Create an offline region in the database. * diff --git a/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.cpp b/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.cpp index 56a0c7ff6..f008f3815 100644 --- a/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.cpp +++ b/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.cpp @@ -63,6 +63,36 @@ void OfflineManager::listOfflineRegions(jni::JNIEnv& env_, } }); } +void OfflineManager::getOfflineRegion(jni::JNIEnv& env_, + const jni::Object& jFileSource_, + const int64_t regionID, + const jni::Object& callback_) { + auto globalCallback = jni::NewGlobal(env_, callback_); + auto globalFilesource = jni::NewGlobal(env_, jFileSource_); + + fileSource->getOfflineRegion( + regionID, + [ + // Keep a shared ptr to a global reference of the callback and file + // source so they are not GC'd in the meanwhile + callback = std::make_shared(std::move(globalCallback)), + jFileSource = std::make_shared(std::move(globalFilesource)), + regionID](mbgl::expected, std::exception_ptr> result) mutable { + // Reattach, the callback comes from a different thread + android::UniqueEnv env = android::AttachEnv(); + + if (result.has_exception() || result.has_exception()) { + OfflineManager::GetOfflineRegionCallback::onError(*env, *callback, result.error()); + } else { + auto region = result.value(); + if (region) { + OfflineManager::GetOfflineRegionCallback::onRegion(*env, *jFileSource, *callback, *region); + } else { + OfflineManager::GetOfflineRegionCallback::onRegionNotFound(*env, *jFileSource, *callback); + } + } + }); +} void OfflineManager::createOfflineRegion(jni::JNIEnv& env_, const jni::Object& jFileSource_, @@ -206,6 +236,7 @@ void OfflineManager::FileSourceCallback::onError(jni::JNIEnv& env, void OfflineManager::registerNative(jni::JNIEnv& env) { jni::Class::Singleton(env); + jni::Class::Singleton(env); jni::Class::Singleton(env); jni::Class::Singleton(env); jni::Class::Singleton(env); @@ -223,6 +254,7 @@ void OfflineManager::registerNative(jni::JNIEnv& env) { "finalize", METHOD(&OfflineManager::setOfflineMapboxTileCountLimit, "setOfflineMapboxTileCountLimit"), METHOD(&OfflineManager::listOfflineRegions, "listOfflineRegions"), + METHOD(&OfflineManager::getOfflineRegion, "getOfflineRegion"), METHOD(&OfflineManager::createOfflineRegion, "createOfflineRegion"), METHOD(&OfflineManager::mergeOfflineRegions, "mergeOfflineRegions"), METHOD(&OfflineManager::resetDatabase, "nativeResetDatabase"), @@ -264,6 +296,37 @@ void OfflineManager::ListOfflineRegionsCallback::onList( callback.Call(env, method, jregions); } +// OfflineManager::GetOfflineRegionCallback // + +void OfflineManager::GetOfflineRegionCallback::onError( + jni::JNIEnv& env, const jni::Object& callback, std::exception_ptr error) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onError"); + + callback.Call(env, method, jni::Make(env, mbgl::util::toString(error))); +} + +void OfflineManager::GetOfflineRegionCallback::onRegionNotFound( + jni::JNIEnv& env, + const jni::Object& jFileSource, + const jni::Object& callback) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod(env, "onRegionNotFound"); + + callback.Call(env, method); +} + +void OfflineManager::GetOfflineRegionCallback::onRegion( + jni::JNIEnv& env, + const jni::Object& jFileSource, + const jni::Object& callback, + mbgl::OfflineRegion& region) { + static auto& javaClass = jni::Class::Singleton(env); + static auto method = javaClass.GetMethod)>(env, "onRegion"); + + callback.Call(env, method, OfflineRegion::New(env, jFileSource, std::move(region))); +} + // OfflineManager::CreateOfflineRegionCallback // void OfflineManager::CreateOfflineRegionCallback::onError( diff --git a/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.hpp b/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.hpp index dbc5c2b77..9b5f5299f 100644 --- a/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.hpp +++ b/platform/android/MapLibreAndroid/src/cpp/offline/offline_manager.hpp @@ -32,6 +32,26 @@ public: const jni::Object&, mbgl::OfflineRegions&); }; + class GetOfflineRegionCallback { + public: + static constexpr auto Name() { + return "org/maplibre/android/offline/" + "OfflineManager$GetOfflineRegionCallback"; + } + + static void onError(jni::JNIEnv&, + const jni::Object&, + std::exception_ptr); + + static void onRegionNotFound(jni::JNIEnv&, + const jni::Object&, + const jni::Object&); + + static void onRegion(jni::JNIEnv&, + const jni::Object&, + const jni::Object&, + mbgl::OfflineRegion&); + }; class CreateOfflineRegionCallback { public: @@ -91,6 +111,11 @@ public: const jni::Object&, const jni::Object& callback); + void getOfflineRegion(jni::JNIEnv&, + const jni::Object&, + const int64_t regionID, + const jni::Object& callback); + void createOfflineRegion(jni::JNIEnv&, const jni::Object& jFileSource_, const jni::Object& definition, diff --git a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineManager.kt b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineManager.kt index 4bf9ac8ab..5d021809b 100644 --- a/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineManager.kt +++ b/platform/android/MapLibreAndroid/src/main/java/org/maplibre/android/offline/OfflineManager.kt @@ -60,6 +60,32 @@ class OfflineManager private constructor(context: Context) { fun onError(error: String) } + /** + * This callback receives an asynchronous response containing the + * OfflineRegion in the database or an error message otherwise. + */ + @Keep + interface GetOfflineRegionCallback { + /** + * Receives the offline region. + * + * @param offlineRegion the offline region if it exists + */ + fun onRegion(offlineRegion: OfflineRegion) + + /** + * Receives notice if the requested regionId is not in the database + */ + fun onRegionNotFound() + + /** + * Receives the error message. + * + * @param error the error message + */ + fun onError(error: String) + } + /** * This callback receives an asynchronous response containing the newly created * OfflineRegion in the database or an error message otherwise. @@ -146,6 +172,44 @@ class OfflineManager private constructor(context: Context) { ) } + /** + * Retrieve given region in the offline database. + * + * The query will be executed asynchronously and the results passed to the given + * callback on the main thread. + * + * @param callback the callback to be invoked + */ + fun getOfflineRegion(regionID: Long, callback: GetOfflineRegionCallback) { + fileSource.activate() + getOfflineRegion( + fileSource, + regionID, + object : GetOfflineRegionCallback { + override fun onRegion(offlineRegion: OfflineRegion) { + handler.post { + fileSource.deactivate() + callback.onRegion(offlineRegion) + } + } + + override fun onRegionNotFound() { + handler.post { + fileSource.deactivate() + callback.onRegionNotFound() + } + } + + override fun onError(error: String) { + handler.post { + fileSource.deactivate() + callback.onError(error) + } + } + } + ) + } + /** * Merge offline regions from a secondary database into the main offline database. * @@ -516,6 +580,9 @@ class OfflineManager private constructor(context: Context) { @Keep private external fun listOfflineRegions(fileSource: FileSource, callback: ListOfflineRegionsCallback) + @Keep + private external fun getOfflineRegion(fileSource: FileSource, regionID: Long, callback: GetOfflineRegionCallback) + @Keep private external fun createOfflineRegion(fileSource: FileSource, definition: OfflineRegionDefinition, metadata: ByteArray, callback: CreateOfflineRegionCallback) diff --git a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/offline/OfflineManagerTest.kt b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/offline/OfflineManagerTest.kt index c8ebafc2d..9dca17033 100644 --- a/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/offline/OfflineManagerTest.kt +++ b/platform/android/MapLibreAndroidTestApp/src/androidTest/java/org/maplibre/android/testapp/offline/OfflineManagerTest.kt @@ -100,7 +100,7 @@ class OfflineManagerTest : AppCenter() { // List regions (ensure merge yielded one region only) - val latch3 = CountDownLatch(1) + val latch3 = CountDownLatch(2) rule.activity.runOnUiThread { OfflineManager.getInstance(context).listOfflineRegions(object : OfflineManager.ListOfflineRegionsCallback { override fun onList(offlineRegions: Array?) { @@ -114,6 +114,29 @@ class OfflineManagerTest : AppCenter() { } }) } + + val regionID = 1L + rule.activity.runOnUiThread { + OfflineManager.getInstance(context).getOfflineRegion( + regionID, + object: OfflineManager.GetOfflineRegionCallback { + override fun onRegion(offlineRegion: OfflineRegion) { + Assert.assertNotNull(offlineRegion) + Assert.assertEquals(regionID, offlineRegion!!.id) + Assert.assertNotNull(offlineRegion.definition) + latch3.countDown() + } + + override fun onRegionNotFound() { + throw RuntimeException("Region should be in database") + } + + override fun onError(error: String) { + throw RuntimeException("Unable to get region in offline database. $error") + } + } + ) + } latch3.await() // Invalidate region @@ -150,7 +173,7 @@ class OfflineManagerTest : AppCenter() { // List regions (ensure deletion was successful) - val latch6 = CountDownLatch(1) + val latch6 = CountDownLatch(2) rule.activity.runOnUiThread { OfflineManager.getInstance(context).listOfflineRegions(object : OfflineManager.ListOfflineRegionsCallback { override fun onList(offlineRegions: Array?) { @@ -163,6 +186,25 @@ class OfflineManagerTest : AppCenter() { } }) } + + rule.activity.runOnUiThread { + OfflineManager.getInstance(context).getOfflineRegion( + regionID, + object: OfflineManager.GetOfflineRegionCallback { + override fun onRegion(offlineRegion: OfflineRegion) { + throw RuntimeException("Region should not be in database") + } + + override fun onRegionNotFound() { + latch6.countDown() + } + + override fun onError(error: String) { + throw RuntimeException("Unable to get offline region from db. $error") + } + } + ) + } latch6.await() } } diff --git a/platform/default/include/mbgl/storage/offline_database.hpp b/platform/default/include/mbgl/storage/offline_database.hpp index a438508f2..7c3af65f3 100644 --- a/platform/default/include/mbgl/storage/offline_database.hpp +++ b/platform/default/include/mbgl/storage/offline_database.hpp @@ -64,6 +64,8 @@ public: expected listRegions(); + expected, std::exception_ptr> getRegion(const int64_t regionID); + expected createRegion(const OfflineRegionDefinition&, const OfflineRegionMetadata&); diff --git a/platform/default/src/mbgl/storage/database_file_source.cpp b/platform/default/src/mbgl/storage/database_file_source.cpp index ba52ea87f..fa674c555 100644 --- a/platform/default/src/mbgl/storage/database_file_source.cpp +++ b/platform/default/src/mbgl/storage/database_file_source.cpp @@ -76,6 +76,11 @@ public: callback(db->listRegions()); } + void getRegion(const int64_t regionID, + const std::function, std::exception_ptr>)>& callback) { + callback(db->getRegion(regionID)); + } + void createRegion(const OfflineRegionDefinition& definition, const OfflineRegionMetadata& metadata, const std::function)>& callback) { @@ -265,6 +270,11 @@ void DatabaseFileSource::listOfflineRegions( impl->actor().invoke(&DatabaseFileSourceThread::listRegions, std::move(callback)); } +void DatabaseFileSource::getOfflineRegion( + const int64_t regionID, std::function, std::exception_ptr>)> callback) { + impl->actor().invoke(&DatabaseFileSourceThread::getRegion, regionID, std::move(callback)); +} + void DatabaseFileSource::createOfflineRegion( const OfflineRegionDefinition& definition, const OfflineRegionMetadata& metadata, diff --git a/platform/default/src/mbgl/storage/offline_database.cpp b/platform/default/src/mbgl/storage/offline_database.cpp index 79f17bf25..ec4291244 100644 --- a/platform/default/src/mbgl/storage/offline_database.cpp +++ b/platform/default/src/mbgl/storage/offline_database.cpp @@ -835,6 +835,26 @@ expected OfflineDatabase::listRegions() try return unexpected(std::current_exception()); } +expected, std::exception_ptr> OfflineDatabase::getRegion(const int64_t regionID) try { + mapbox::sqlite::Query query{getStatement("SELECT definition, description FROM regions WHERE id = ?1")}; + query.bind(1, regionID); + if (query.run()) { + const auto definition = query.get(0); + const auto description = query.get>(1); + + if (definition.empty() && description.empty()) { + throw std::runtime_error("Region " + std::to_string(regionID) + " not found in database"); + } + + OfflineRegion result(regionID, decodeOfflineRegionDefinition(definition), description); + return {std::move(result)}; + } + return std::nullopt; +} catch (...) { + handleError("get region"); + return unexpected(std::current_exception()); +} + expected OfflineDatabase::createRegion(const OfflineRegionDefinition& definition, const OfflineRegionMetadata& metadata) try { checkFlags(); diff --git a/test/storage/offline_database.test.cpp b/test/storage/offline_database.test.cpp index 8a3bec098..bbb44f910 100644 --- a/test/storage/offline_database.test.cpp +++ b/test/storage/offline_database.test.cpp @@ -465,8 +465,8 @@ TEST(OfflineDatabase, UpdateMetadata) { OfflineRegionMetadata newmetadata{{4, 5, 6}}; db.updateMetadata(region->getID(), newmetadata); - auto regions = db.listRegions().value(); - EXPECT_EQ(regions.at(0).getMetadata(), newmetadata); + auto newRegion = db.getRegion(region->getID()).value(); + EXPECT_EQ(newRegion->getMetadata(), newmetadata); EXPECT_EQ(0u, log.uncheckedCount()); } @@ -502,6 +502,36 @@ TEST(OfflineDatabase, ListRegions) { EXPECT_EQ(0u, log.uncheckedCount()); } +TEST(OfflineDatabase, GetRegion) { + FixtureLog log; + OfflineDatabase db(":memory:", fixture::tileServerOptions); + OfflineTilePyramidRegionDefinition definition{ + "http://example.com/style", LatLngBounds::hull({1, 2}, {3, 4}), 5, 6, 2.0, false}; + OfflineRegionMetadata metadata{{1, 2, 3}}; + + auto createdRegion = db.createRegion(definition, metadata); + ASSERT_TRUE(createdRegion); + auto regionId = createdRegion->getID(); + auto region = db.getRegion(regionId).value(); + + EXPECT_EQ(regionId, region->getID()); + std::visit(overloaded{[&](OfflineTilePyramidRegionDefinition& def) { + EXPECT_EQ(definition.styleURL, def.styleURL); + EXPECT_EQ(definition.bounds, def.bounds); + EXPECT_EQ(definition.minZoom, def.minZoom); + EXPECT_EQ(definition.maxZoom, def.maxZoom); + EXPECT_EQ(definition.pixelRatio, def.pixelRatio); + EXPECT_EQ(definition.includeIdeographs, def.includeIdeographs); + }, + [&](auto&) { + EXPECT_FALSE(false); + }}, + region->getDefinition()); + EXPECT_EQ(metadata, region->getMetadata()); + + EXPECT_EQ(0u, log.uncheckedCount()); +} + TEST(OfflineDatabase, GetRegionDefinition) { FixtureLog log; OfflineDatabase db(":memory:", fixture::tileServerOptions);