Add SDK map module and migrate map fetching to Apollo with Hilt wiring.
This includes the new sdk module, MapService + mapper implementation, Apollo provider module, Hilt app integration, and the project .gitignore update. Made-with: Cursor
This commit is contained in:
@@ -16,6 +16,11 @@ alwaysApply: true
|
||||
- When referencing legacy implementation details, copy behavior intentionally into the new project rather than editing old files.
|
||||
- If a task appears to require modifying the old project, stop and propose an equivalent change in `airmq-android-2026` instead.
|
||||
|
||||
## Naming convention
|
||||
|
||||
- Use `AirMQ` everywhere for product naming.
|
||||
- Never use mixed-case variants in text, docs, comments, identifiers, filenames, or symbols.
|
||||
|
||||
## Baseline architecture and platform stack
|
||||
|
||||
Use this stack as the default foundation for all implementation work in `airmq-android-2026`:
|
||||
@@ -23,7 +28,7 @@ Use this stack as the default foundation for all implementation work in `airmq-a
|
||||
1. Jetpack Compose for UI.
|
||||
2. Compose Navigation 3 as the in-app navigation system.
|
||||
3. Firebase for mobile backend capabilities (for example auth, analytics, crash reporting, messaging, or remote config as needed).
|
||||
4. Google Maps platform for map rendering and map-related interactions.
|
||||
4. OpenStreetMap platform for map rendering and map-related interactions.
|
||||
5. Apollo GraphQL for GraphQL schema integration, client generation, and network operations.
|
||||
|
||||
If a requested change conflicts with this baseline, ask for explicit approval before introducing an alternative.
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,9 +7,13 @@
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.idea/inspectionProfiles/
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
**/build/
|
||||
.kotlin/
|
||||
/.idea/**/workspace.xml
|
||||
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@@ -9,6 +9,7 @@
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/sdk" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.hilt.android)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
@@ -40,8 +42,15 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// AirMQ SDK
|
||||
implementation(project(":sdk"))
|
||||
|
||||
// Android
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.compose.ui)
|
||||
@@ -49,13 +58,18 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.hilt.android)
|
||||
implementation(libs.androidx.hilt.navigation.compose)
|
||||
ksp(libs.hilt.compiler)
|
||||
|
||||
// Firebase
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.analytics)
|
||||
implementation(libs.firebase.crashlytics)
|
||||
implementation(libs.firebase.messaging)
|
||||
implementation(libs.google.maps.compose)
|
||||
implementation(libs.play.services.maps)
|
||||
implementation(libs.apollo.runtime)
|
||||
implementation(libs.osmdroid.android)
|
||||
|
||||
// Tests
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.db3.airmq
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("org.db3.airmq", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".AirMQApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
query MapLocations {
|
||||
locations {
|
||||
_id
|
||||
name
|
||||
city
|
||||
latitude
|
||||
longitude
|
||||
isOnline
|
||||
}
|
||||
}
|
||||
7
app/src/main/kotlin/org/db3/airmq/AirMQApplication.kt
Normal file
7
app/src/main/kotlin/org/db3/airmq/AirMQApplication.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package org.db3.airmq
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class AirMQApplication : Application()
|
||||
@@ -6,16 +6,18 @@ import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.db3.airmq.features.navigation.AirMqNavGraph
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.db3.airmq.features.navigation.AirMQNavGraph
|
||||
import org.db3.airmq.ui.theme.AirMQTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
AirMQTheme {
|
||||
AirMqNavGraph(modifier = Modifier.fillMaxSize())
|
||||
AirMQNavGraph(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
package org.db3.airmq.features.map
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
class AirMqMapApi {
|
||||
fun fetchMapItems(): List<MapItem> {
|
||||
val payload = JSONObject()
|
||||
.put("query", LOCATIONS_QUERY)
|
||||
.put("variables", JSONObject())
|
||||
|
||||
val connection = (URL(API_URL).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
connectTimeout = 15_000
|
||||
readTimeout = 15_000
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
doInput = true
|
||||
doOutput = true
|
||||
}
|
||||
|
||||
return try {
|
||||
OutputStreamWriter(connection.outputStream).use { writer ->
|
||||
writer.write(payload.toString())
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
val responseCode = connection.responseCode
|
||||
val body = readResponseBody(connection, responseCode)
|
||||
if (responseCode !in 200..299) {
|
||||
throw IllegalStateException("Map API request failed: HTTP $responseCode")
|
||||
}
|
||||
|
||||
parseMapItems(body)
|
||||
} finally {
|
||||
connection.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMapItems(rawJson: String): List<MapItem> {
|
||||
val root = JSONObject(rawJson)
|
||||
val errors = root.optJSONArray("errors")
|
||||
if (errors != null && errors.length() > 0) {
|
||||
throw IllegalStateException(firstGraphQlError(errors))
|
||||
}
|
||||
|
||||
val data = root.optJSONObject("data")
|
||||
?: throw IllegalStateException("Map API response is missing data")
|
||||
val locations = data.optJSONArray("locations") ?: JSONArray()
|
||||
|
||||
return buildList {
|
||||
for (index in 0 until locations.length()) {
|
||||
val location = locations.optJSONObject(index) ?: continue
|
||||
val id = location.optString("_id")
|
||||
val latitude = location.optDouble("latitude", Double.NaN)
|
||||
val longitude = location.optDouble("longitude", Double.NaN)
|
||||
if (id.isBlank() || latitude.isNaN() || longitude.isNaN()) {
|
||||
continue
|
||||
}
|
||||
|
||||
add(
|
||||
MapItem(
|
||||
id = id,
|
||||
title = location.optString("name").ifBlank { id },
|
||||
city = location.optString("city").ifBlank { null },
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
isOnline = location.optBoolean("isOnline", false)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readResponseBody(connection: HttpURLConnection, responseCode: Int): String {
|
||||
val source = if (responseCode in 200..299) {
|
||||
connection.inputStream
|
||||
} else {
|
||||
connection.errorStream ?: connection.inputStream
|
||||
}
|
||||
|
||||
return source.bufferedReader().use(BufferedReader::readText)
|
||||
}
|
||||
|
||||
private fun firstGraphQlError(errors: JSONArray): String {
|
||||
val firstError = errors.optJSONObject(0)
|
||||
return firstError?.optString("message").orEmpty().ifBlank {
|
||||
"Map API returned an unknown GraphQL error"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val API_URL = "https://api.airmq.cc"
|
||||
private const val LOCATIONS_QUERY = """
|
||||
query MapLocations {
|
||||
locations {
|
||||
_id
|
||||
name
|
||||
city
|
||||
latitude
|
||||
longitude
|
||||
isOnline
|
||||
}
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.db3.airmq.features.map
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -24,8 +26,9 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import org.db3.airmq.features.common.AirMqContainedButton
|
||||
import org.db3.airmq.sdk.map.domain.MapItem
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||
import org.osmdroid.util.GeoPoint
|
||||
@@ -34,9 +37,42 @@ import org.osmdroid.views.overlay.Marker
|
||||
|
||||
@Composable
|
||||
fun MapScreen(
|
||||
viewModel: MapViewModel = viewModel()
|
||||
viewModel: MapViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(uiState) {
|
||||
Toast.makeText(context, uiState.items.count().toString(), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AirMQMap(uiState.items)
|
||||
|
||||
if (uiState.isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.2f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(40.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.errorMessage != null) {
|
||||
ErrorOverlay(
|
||||
message = uiState.errorMessage ?: "Unknown error",
|
||||
onRetry = viewModel::refresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AirMQMap(
|
||||
items: List<MapItem>
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
@@ -65,47 +101,27 @@ fun MapScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { mapView },
|
||||
update = { map ->
|
||||
map.overlays.removeAll { it is Marker }
|
||||
uiState.items.forEach { item ->
|
||||
val marker = Marker(map).apply {
|
||||
position = GeoPoint(item.latitude, item.longitude)
|
||||
title = listOfNotNull(item.title, item.city).joinToString(" - ")
|
||||
subDescription = if (item.isOnline) "Online" else "Offline"
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
}
|
||||
map.overlays.add(marker)
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { mapView },
|
||||
update = { map ->
|
||||
map.overlays.removeAll { it is Marker }
|
||||
items.forEach { item ->
|
||||
val marker = Marker(map).apply {
|
||||
position = GeoPoint(item.latitude, item.longitude)
|
||||
title = listOfNotNull(item.title, item.city).joinToString(" - ")
|
||||
subDescription = if (item.isOnline) "Online" else "Offline"
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
}
|
||||
|
||||
uiState.items.firstOrNull()?.let { first ->
|
||||
map.controller.animateTo(GeoPoint(first.latitude, first.longitude))
|
||||
}
|
||||
map.invalidate()
|
||||
map.overlays.add(marker)
|
||||
}
|
||||
)
|
||||
|
||||
if (uiState.isLoading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.2f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(40.dp))
|
||||
items.firstOrNull()?.let { first ->
|
||||
map.controller.animateTo(GeoPoint(first.latitude, first.longitude))
|
||||
}
|
||||
map.invalidate()
|
||||
}
|
||||
|
||||
if (uiState.errorMessage != null) {
|
||||
ErrorOverlay(
|
||||
message = uiState.errorMessage ?: "Unknown error",
|
||||
onRetry = viewModel::refresh
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.db3.airmq.features.map
|
||||
|
||||
import org.db3.airmq.sdk.map.domain.MapItem
|
||||
|
||||
data class MapUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val items: List<MapItem> = emptyList(),
|
||||
|
||||
@@ -2,14 +2,18 @@ package org.db3.airmq.features.map
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import org.db3.airmq.sdk.map.MapService
|
||||
|
||||
class MapViewModel(
|
||||
private val mapApi: AirMqMapApi = AirMqMapApi()
|
||||
@HiltViewModel
|
||||
class MapViewModel @Inject constructor(
|
||||
private val mapService: MapService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(MapUiState(isLoading = true))
|
||||
@@ -22,7 +26,7 @@ class MapViewModel(
|
||||
fun refresh() {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val result = runCatching { mapApi.fetchMapItems() }
|
||||
val result = runCatching { mapService.fetchMapItems() }
|
||||
_uiState.value = result.fold(
|
||||
onSuccess = { items ->
|
||||
MapUiState(
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package org.db3.airmq
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.android.library) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.apollo) apply false
|
||||
alias(libs.plugins.hilt.android) apply false
|
||||
}
|
||||
@@ -20,4 +20,6 @@ kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.disallowKotlinSourceSets=false
|
||||
android.newDsl=false
|
||||
@@ -5,14 +5,19 @@ junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
lifecycleRuntimeKtx = "2.10.0"
|
||||
lifecycleViewmodel = "2.10.0"
|
||||
activityCompose = "1.12.4"
|
||||
kotlin = "2.0.21"
|
||||
composeBom = "2024.09.00"
|
||||
navigationCompose = "2.9.3"
|
||||
firebaseBom = "34.4.0"
|
||||
mapsCompose = "6.12.1"
|
||||
playServicesMaps = "19.2.0"
|
||||
apollo = "4.3.3"
|
||||
osmdroid = "6.1.20"
|
||||
apollo = "5.0.0-alpha.4"
|
||||
hilt = "2.57.2"
|
||||
hiltNavigationCompose = "1.2.0"
|
||||
okhttpLogging = "4.12.0"
|
||||
ksp = "2.0.21-1.0.27"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -20,6 +25,9 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
||||
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodel" }
|
||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodel" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
@@ -34,12 +42,20 @@ firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.r
|
||||
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
|
||||
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
|
||||
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }
|
||||
google-maps-compose = { group = "com.google.maps.android", name = "maps-compose", version.ref = "mapsCompose" }
|
||||
play-services-maps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playServicesMaps" }
|
||||
osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version.ref = "osmdroid" }
|
||||
apollo-runtime = { group = "com.apollographql.apollo", name = "apollo-runtime", version.ref = "apollo" }
|
||||
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
|
||||
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttpLogging" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-library = { id = "com.android.library", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
apollo = { id = "com.apollographql.apollo", version.ref = "apollo" }
|
||||
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
||||
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
|
||||
|
||||
49
sdk/build.gradle.kts
Normal file
49
sdk/build.gradle.kts
Normal file
@@ -0,0 +1,49 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.apollo)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.db3.airmq.sdk"
|
||||
compileSdk {
|
||||
version = release(36) {
|
||||
minorApiLevel = 1
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
apollo {
|
||||
service("service") {
|
||||
packageName.set("org.db3.airmq.sdk")
|
||||
schemaFiles.from(file("../app/src/main/graphql/schema.graphqls"))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.apollo.runtime)
|
||||
implementation(libs.hilt.android)
|
||||
ksp(libs.hilt.compiler)
|
||||
}
|
||||
1
sdk/consumer-rules.pro
Normal file
1
sdk/consumer-rules.pro
Normal file
@@ -0,0 +1 @@
|
||||
# Consumer ProGuard rules for sdk module.
|
||||
1
sdk/proguard-rules.pro
vendored
Normal file
1
sdk/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
2
sdk/src/main/AndroidManifest.xml
Normal file
2
sdk/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
9
sdk/src/main/graphql/MapLocations.graphql
Normal file
9
sdk/src/main/graphql/MapLocations.graphql
Normal file
@@ -0,0 +1,9 @@
|
||||
query MapMarkers {
|
||||
getMarkers {
|
||||
_id
|
||||
name
|
||||
latitude
|
||||
longitude
|
||||
text
|
||||
}
|
||||
}
|
||||
33
sdk/src/main/kotlin/org/db3/airmq/sdk/di/SDKModule.kt
Normal file
33
sdk/src/main/kotlin/org/db3/airmq/sdk/di/SDKModule.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
package org.db3.airmq.sdk.di
|
||||
|
||||
import com.apollographql.apollo.ApolloClient
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
import org.db3.airmq.sdk.map.ApolloMapService
|
||||
import org.db3.airmq.sdk.map.MapService
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object SDKModule {
|
||||
private const val API_URL = "https://api.airmq.cc"
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApolloClient(): ApolloClient {
|
||||
return ApolloClient.Builder()
|
||||
.serverUrl(API_URL)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class SDKBindModule {
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindMapService(impl: ApolloMapService): MapService
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.db3.airmq.sdk.map
|
||||
|
||||
import javax.inject.Inject
|
||||
import org.db3.airmq.sdk.MapMarkersQuery
|
||||
import org.db3.airmq.sdk.map.domain.MapItem
|
||||
|
||||
class ApolloMapItemMapper @Inject constructor() {
|
||||
fun toDomain(marker: MapMarkersQuery.GetMarker, index: Int): MapItem? {
|
||||
val latitude = marker.latitude ?: return null
|
||||
val longitude = marker.longitude ?: return null
|
||||
val id = marker._id?.ifBlank { null } ?: "marker-$index"
|
||||
return MapItem(
|
||||
id = id,
|
||||
title = marker.name?.ifBlank { null } ?: marker.text?.ifBlank { null } ?: id,
|
||||
city = null,
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
isOnline = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.db3.airmq.sdk.map
|
||||
|
||||
import android.util.Log
|
||||
import com.apollographql.apollo.ApolloClient
|
||||
import javax.inject.Inject
|
||||
import org.db3.airmq.sdk.MapMarkersQuery
|
||||
import org.db3.airmq.sdk.map.domain.MapItem
|
||||
|
||||
class ApolloMapService @Inject constructor(
|
||||
private val apolloClient: ApolloClient,
|
||||
private val mapper: ApolloMapItemMapper
|
||||
) : MapService {
|
||||
override suspend fun fetchMapItems(): List<MapItem> {
|
||||
Log.d(TAG, "Executing MapMarkers Apollo query")
|
||||
val response = apolloClient.query(MapMarkersQuery()).execute()
|
||||
|
||||
response.errors?.firstOrNull()?.let { gqlError ->
|
||||
Log.e(TAG, "MapMarkers Apollo query failed: ${gqlError.message}")
|
||||
throw IllegalStateException(gqlError.message)
|
||||
}
|
||||
|
||||
val mappedItems = response.data?.getMarkers
|
||||
.orEmpty()
|
||||
.filterNotNull()
|
||||
.mapIndexedNotNull { index, marker -> mapper.toDomain(marker, index) }
|
||||
Log.d(TAG, "MapMarkers Apollo query returned ${mappedItems.size} items")
|
||||
return mappedItems
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "ApolloMapService"
|
||||
}
|
||||
}
|
||||
7
sdk/src/main/kotlin/org/db3/airmq/sdk/map/MapService.kt
Normal file
7
sdk/src/main/kotlin/org/db3/airmq/sdk/map/MapService.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package org.db3.airmq.sdk.map
|
||||
|
||||
import org.db3.airmq.sdk.map.domain.MapItem
|
||||
|
||||
interface MapService {
|
||||
suspend fun fetchMapItems(): List<MapItem>
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.db3.airmq.features.map
|
||||
package org.db3.airmq.sdk.map.domain
|
||||
|
||||
data class MapItem(
|
||||
val id: String,
|
||||
@@ -24,4 +24,5 @@ dependencyResolutionManagement {
|
||||
|
||||
rootProject.name = "AirMQ"
|
||||
include(":app")
|
||||
include(":sdk")
|
||||
|
||||
Reference in New Issue
Block a user