Add getOfflineRegion Kotlin API (#2516)

This commit is contained in:
JRW
2024-08-08 15:49:49 +01:00
committed by GitHub
parent f4fc63bdc0
commit 9432d805cd
9 changed files with 274 additions and 4 deletions

View File

@@ -136,6 +136,17 @@ public:
*/
virtual void listOfflineRegions(std::function<void(expected<OfflineRegions, std::exception_ptr>)>);
/**
* 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<void(expected<std::optional<OfflineRegion>, std::exception_ptr>)>);
/**
* Create an offline region in the database.
*

View File

@@ -63,6 +63,36 @@ void OfflineManager::listOfflineRegions(jni::JNIEnv& env_,
}
});
}
void OfflineManager::getOfflineRegion(jni::JNIEnv& env_,
const jni::Object<FileSource>& jFileSource_,
const int64_t regionID,
const jni::Object<GetOfflineRegionCallback>& callback_) {
auto globalCallback = jni::NewGlobal<jni::EnvAttachingDeleter>(env_, callback_);
auto globalFilesource = jni::NewGlobal<jni::EnvAttachingDeleter>(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<decltype(globalCallback)>(std::move(globalCallback)),
jFileSource = std::make_shared<decltype(globalFilesource)>(std::move(globalFilesource)),
regionID](mbgl::expected<std::optional<mbgl::OfflineRegion>, std::exception_ptr> result) mutable {
// Reattach, the callback comes from a different thread
android::UniqueEnv env = android::AttachEnv();
if (result.has_exception<std::logic_error>() || result.has_exception<std::runtime_error>()) {
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<FileSource>& jFileSource_,
@@ -206,6 +236,7 @@ void OfflineManager::FileSourceCallback::onError(jni::JNIEnv& env,
void OfflineManager::registerNative(jni::JNIEnv& env) {
jni::Class<ListOfflineRegionsCallback>::Singleton(env);
jni::Class<GetOfflineRegionCallback>::Singleton(env);
jni::Class<CreateOfflineRegionCallback>::Singleton(env);
jni::Class<MergeOfflineRegionsCallback>::Singleton(env);
jni::Class<FileSourceCallback>::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<OfflineManager::GetOfflineRegionCallback>& callback, std::exception_ptr error) {
static auto& javaClass = jni::Class<OfflineManager::GetOfflineRegionCallback>::Singleton(env);
static auto method = javaClass.GetMethod<void(jni::String)>(env, "onError");
callback.Call(env, method, jni::Make<jni::String>(env, mbgl::util::toString(error)));
}
void OfflineManager::GetOfflineRegionCallback::onRegionNotFound(
jni::JNIEnv& env,
const jni::Object<FileSource>& jFileSource,
const jni::Object<OfflineManager::GetOfflineRegionCallback>& callback) {
static auto& javaClass = jni::Class<OfflineManager::GetOfflineRegionCallback>::Singleton(env);
static auto method = javaClass.GetMethod<void(void)>(env, "onRegionNotFound");
callback.Call(env, method);
}
void OfflineManager::GetOfflineRegionCallback::onRegion(
jni::JNIEnv& env,
const jni::Object<FileSource>& jFileSource,
const jni::Object<OfflineManager::GetOfflineRegionCallback>& callback,
mbgl::OfflineRegion& region) {
static auto& javaClass = jni::Class<OfflineManager::GetOfflineRegionCallback>::Singleton(env);
static auto method = javaClass.GetMethod<void(jni::Object<OfflineRegion>)>(env, "onRegion");
callback.Call(env, method, OfflineRegion::New(env, jFileSource, std::move(region)));
}
// OfflineManager::CreateOfflineRegionCallback //
void OfflineManager::CreateOfflineRegionCallback::onError(

View File

@@ -32,6 +32,26 @@ public:
const jni::Object<OfflineManager::ListOfflineRegionsCallback>&,
mbgl::OfflineRegions&);
};
class GetOfflineRegionCallback {
public:
static constexpr auto Name() {
return "org/maplibre/android/offline/"
"OfflineManager$GetOfflineRegionCallback";
}
static void onError(jni::JNIEnv&,
const jni::Object<OfflineManager::GetOfflineRegionCallback>&,
std::exception_ptr);
static void onRegionNotFound(jni::JNIEnv&,
const jni::Object<FileSource>&,
const jni::Object<OfflineManager::GetOfflineRegionCallback>&);
static void onRegion(jni::JNIEnv&,
const jni::Object<FileSource>&,
const jni::Object<OfflineManager::GetOfflineRegionCallback>&,
mbgl::OfflineRegion&);
};
class CreateOfflineRegionCallback {
public:
@@ -91,6 +111,11 @@ public:
const jni::Object<FileSource>&,
const jni::Object<ListOfflineRegionsCallback>& callback);
void getOfflineRegion(jni::JNIEnv&,
const jni::Object<FileSource>&,
const int64_t regionID,
const jni::Object<GetOfflineRegionCallback>& callback);
void createOfflineRegion(jni::JNIEnv&,
const jni::Object<FileSource>& jFileSource_,
const jni::Object<OfflineRegionDefinition>& definition,

View File

@@ -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)

View File

@@ -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<OfflineRegion>?) {
@@ -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<OfflineRegion>?) {
@@ -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()
}
}

View File

@@ -64,6 +64,8 @@ public:
expected<OfflineRegions, std::exception_ptr> listRegions();
expected<std::optional<OfflineRegion>, std::exception_ptr> getRegion(const int64_t regionID);
expected<OfflineRegion, std::exception_ptr> createRegion(const OfflineRegionDefinition&,
const OfflineRegionMetadata&);

View File

@@ -76,6 +76,11 @@ public:
callback(db->listRegions());
}
void getRegion(const int64_t regionID,
const std::function<void(expected<std::optional<OfflineRegion>, std::exception_ptr>)>& callback) {
callback(db->getRegion(regionID));
}
void createRegion(const OfflineRegionDefinition& definition,
const OfflineRegionMetadata& metadata,
const std::function<void(expected<OfflineRegion, std::exception_ptr>)>& 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<void(expected<std::optional<OfflineRegion>, std::exception_ptr>)> callback) {
impl->actor().invoke(&DatabaseFileSourceThread::getRegion, regionID, std::move(callback));
}
void DatabaseFileSource::createOfflineRegion(
const OfflineRegionDefinition& definition,
const OfflineRegionMetadata& metadata,

View File

@@ -835,6 +835,26 @@ expected<OfflineRegions, std::exception_ptr> OfflineDatabase::listRegions() try
return unexpected<std::exception_ptr>(std::current_exception());
}
expected<std::optional<OfflineRegion>, 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<std::string>(0);
const auto description = query.get<std::vector<uint8_t>>(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::exception_ptr>(std::current_exception());
}
expected<OfflineRegion, std::exception_ptr> OfflineDatabase::createRegion(const OfflineRegionDefinition& definition,
const OfflineRegionMetadata& metadata) try {
checkFlags();

View File

@@ -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);