diff --git a/.gitignore b/.gitignore index 8a6bf9c..ec83ee6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ android/captures/ android/.externalNativeBuild/ *.keystore *.jks +android/app/src/main/assets/dgissdk.key # IDE .vscode/ diff --git a/README.md b/README.md index 4dc055d..4c18852 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,39 @@ Wi-Fi/LAN. Run unit tests before building — `./gradlew testDebugUnitTest`, then `./gradlew :app:assembleDebug`. +## 2GIS map SDK + +The app depends on the real 2GIS MapKit SDK (`ru.dgis.sdk:sdk-map` + +`compose-map`, from `https://artifactory.2gis.dev/sdk-maven-release`, wired +up in `settings.gradle.kts`/`app/build.gradle.kts`). It needs a **separate** +key from the `DGIS_API_KEY` above: + +- `DGIS_API_KEY` (the `b4df01a...` value) is a 2GIS **public REST/JS API** + key — not used by the native SDK at all currently. +- The native MapKit SDK instead needs a **`dgissdk.key` file**, issued + per-app (tied to the package name `com.guidecity.app`) from + https://dev.2gis.com/. Place it at `android/app/src/main/assets/dgissdk.key` + (gitignored — don't commit it). + +Without that key file, `DgisSdkProvider` fails to initialize (logged, not +crashed) and `DgisMapView` falls back to a placeholder canvas rendering +(user location + nearby place dots, no real map tiles). Once a real key is +in place, the real map should render automatically — no code changes needed. + +Also note: the SDK bundles native libraries per CPU architecture, which +balloons APK size a lot (~19MB → ~190MB unfiltered). `app/build.gradle.kts` +restricts `ndk.abiFilters` to `arm64-v8a` only (~69MB) since that covers +virtually all real devices today — remove that filter if you need to test +on an x86 emulator or 32-bit device. + ## Notes / current scope This is iteration 1: Moscow only (Gorky Park, Red Square area, Moscow-City), no user accounts (favorites are local/Room-only), no speed/heading-aware auto content-length selection yet (reserved API params exist, unused). +Nearby-search radius caps at 10km. Place markers aren't yet plotted on the +real 2GIS map (only the placeholder does that) — follow-up work. -The 2GIS API key above is used directly via `local.properties`/`BuildConfig` -for development convenience — don't ship it as-is in a public repo. +The 2GIS REST/JS API key above is used directly via +`local.properties`/`BuildConfig` for development convenience — don't ship it +as-is in a public repo. Same goes for `dgissdk.key` once you have one. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 92914c7..12d819b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -42,6 +42,14 @@ android { "API_BASE_URL", "\"${localProp("API_BASE_URL", "http://10.0.2.2:8000/")}\"", ) + + // The 2GIS SDK ships native libs for every ABI, which balloons the APK + // (~19MB -> ~190MB unfiltered). Almost all real devices are arm64 + // today, and this keeps debug builds small enough to send anywhere + // (e.g. under Telegram bot's 50MB upload limit). + ndk { + abiFilters += "arm64-v8a" + } } buildTypes { @@ -110,9 +118,12 @@ dependencies { implementation(libs.play.services.location) implementation(libs.kotlinx.coroutines.play.services) - // 2GIS MapKit SDK: add once the exact Maven coordinates/repository are - // confirmed from https://docs.2gis.com/ — see map/DgisMapView.kt for the - // isolated integration point in the meantime. + // 2GIS MapKit SDK. Requires a real dgissdk.key in app/src/main/assets/, + // obtained per-app from dev.2gis.com — see README for details. Without + // it, DgisSdkProvider's init fails gracefully and DgisMapView falls back + // to its placeholder rendering. + implementation(libs.dgis.sdk.map) + implementation(libs.dgis.compose.map) testImplementation(libs.junit) testImplementation(libs.mockk) diff --git a/android/app/src/main/java/com/guidecity/app/map/DgisMapView.kt b/android/app/src/main/java/com/guidecity/app/map/DgisMapView.kt index 6c3af65..12040a6 100644 --- a/android/app/src/main/java/com/guidecity/app/map/DgisMapView.kt +++ b/android/app/src/main/java/com/guidecity/app/map/DgisMapView.kt @@ -7,37 +7,64 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.unit.dp import com.guidecity.app.data.remote.dto.NearbyPlaceDto import com.guidecity.app.location.LatLon import com.guidecity.app.theme.Mocha +import ru.dgis.sdk.Context as DgisContext +import ru.dgis.sdk.coordinates.Bearing +import ru.dgis.sdk.coordinates.GeoPoint +import ru.dgis.sdk.coordinates.Latitude +import ru.dgis.sdk.coordinates.Longitude +import ru.dgis.sdk.compose.map.MapComposable +import ru.dgis.sdk.compose.map.MapComposableState +import ru.dgis.sdk.map.CameraPosition +import ru.dgis.sdk.map.MapOptions +import ru.dgis.sdk.map.Tilt +import ru.dgis.sdk.map.Zoom import kotlin.math.cos /** - * Isolated integration point for the 2GIS MapKit SDK. + * Renders the real 2GIS MapKit map when the SDK initialized successfully + * (i.e. a valid `dgissdk.key` is present — see [DgisSdkProvider]); otherwise + * falls back to a lightweight placeholder (user location centered, nearby + * places plotted by relative lat/lon offset) so the rest of the app — + * permission flow, nearby fetch, navigation to place detail — stays fully + * functional and demoable without a real key. * - * This currently renders a lightweight placeholder (user location centered, - * nearby places plotted by relative lat/lon offset) so the rest of the app - * — permission flow, nearby fetch, navigation to place detail — is fully - * functional and demoable before the real SDK is wired in. - * - * To integrate the real map: replace the Canvas placeholder below with the - * 2GIS MapKit view (see android/build.gradle.kts and settings.gradle.kts for - * the pending dependency/repository TODOs — the exact Maven coordinates need - * confirming from https://docs.2gis.com/), keeping this function's signature - * so callers (MapScreen, GuideScreen) don't need to change. + * Follow-up not yet implemented: plotting [places] as markers on the real + * map (needs the SDK's MapObjectManager/Marker API) — for now the real map + * only centers on [userLocation]; the placeholder still shows place dots. */ @Composable fun DgisMapView( + sdkContext: DgisContext?, userLocation: LatLon?, places: List, onPlaceClick: (placeId: Int) -> Unit, modifier: Modifier = Modifier, ) { + if (sdkContext != null && userLocation != null) { + val mapState = remember(userLocation) { + MapComposableState( + MapOptions().apply { + position = CameraPosition( + point = GeoPoint(Latitude(userLocation.lat), Longitude(userLocation.lon)), + zoom = Zoom(15f), + tilt = Tilt(), + bearing = Bearing(), + ) + }, + ) + } + MapComposable(modifier = modifier.fillMaxSize(), state = mapState) + return + } + Box( modifier = modifier .fillMaxSize() diff --git a/android/app/src/main/java/com/guidecity/app/map/DgisSdkProvider.kt b/android/app/src/main/java/com/guidecity/app/map/DgisSdkProvider.kt new file mode 100644 index 0000000..e2f67b4 --- /dev/null +++ b/android/app/src/main/java/com/guidecity/app/map/DgisSdkProvider.kt @@ -0,0 +1,39 @@ +package com.guidecity.app.map + +import android.util.Log +import dagger.hilt.android.qualifiers.ApplicationContext +import ru.dgis.sdk.Context as DgisContext +import ru.dgis.sdk.DGis +import javax.inject.Inject +import javax.inject.Singleton + +private const val TAG = "DgisSdkProvider" + +/** + * Lazily initializes the 2GIS MapKit SDK once per process. + * + * Requires a real `dgissdk.key` file in `app/src/main/assets/`, issued + * per-app (tied to the package name) from https://dev.2gis.com/ — the + * `DGIS_API_KEY` used elsewhere in this app (2GIS's public REST/JS API key + * format) is a *different* product and will not work here. Without a valid + * key file, [sdkContext] is null and [DgisMapView] falls back to its + * placeholder rendering instead of crashing. + */ +@Singleton +class DgisSdkProvider @Inject constructor( + @ApplicationContext private val androidContext: android.content.Context, +) { + val sdkContext: DgisContext? by lazy { + runCatching { DGis.initialize(androidContext) } + .onSuccess { Log.i(TAG, "2GIS SDK initialized") } + .onFailure { + Log.e( + TAG, + "2GIS SDK initialization failed — is a valid dgissdk.key present in " + + "app/src/main/assets/? (see README)", + it, + ) + } + .getOrNull() + } +} diff --git a/android/app/src/main/java/com/guidecity/app/ui/map/MapScreen.kt b/android/app/src/main/java/com/guidecity/app/ui/map/MapScreen.kt index fa3ba59..8457c3a 100644 --- a/android/app/src/main/java/com/guidecity/app/ui/map/MapScreen.kt +++ b/android/app/src/main/java/com/guidecity/app/ui/map/MapScreen.kt @@ -85,6 +85,7 @@ fun MapScreen( .padding(padding), ) { DgisMapView( + sdkContext = viewModel.dgisSdkContext, userLocation = resolvedLocation, places = nearbyPlaces, onPlaceClick = {}, diff --git a/android/app/src/main/java/com/guidecity/app/ui/map/MapViewModel.kt b/android/app/src/main/java/com/guidecity/app/ui/map/MapViewModel.kt index f478c58..52cdb4b 100644 --- a/android/app/src/main/java/com/guidecity/app/ui/map/MapViewModel.kt +++ b/android/app/src/main/java/com/guidecity/app/ui/map/MapViewModel.kt @@ -9,6 +9,7 @@ import com.guidecity.app.data.repository.PlacesRepository import com.guidecity.app.location.LatLon import com.guidecity.app.location.LocationProvider import com.guidecity.app.location.SelectedLocationHolder +import com.guidecity.app.map.DgisSdkProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -34,8 +35,12 @@ class MapViewModel @Inject constructor( private val locationProvider: LocationProvider, private val selectedLocationHolder: SelectedLocationHolder, private val placesRepository: PlacesRepository, + dgisSdkProvider: DgisSdkProvider, ) : ViewModel() { + /** Null until a valid `dgissdk.key` is present — see [DgisSdkProvider]. */ + val dgisSdkContext = dgisSdkProvider.sdkContext + private val _uiState = MutableStateFlow(MapUiState.CheckingPermission) val uiState: StateFlow = _uiState.asStateFlow() diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 1c04b3a..3fafd63 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -21,6 +21,7 @@ androidxTestExtJunit = "1.2.1" espressoCore = "3.6.1" mockk = "1.13.12" kotlinxCoroutinesTest = "1.8.1" +dgisSdk = "13.5.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -62,6 +63,9 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } +dgis-sdk-map = { group = "ru.dgis.sdk", name = "sdk-map", version.ref = "dgisSdk" } +dgis-compose-map = { group = "ru.dgis.sdk", name = "compose-map", version.ref = "dgisSdk" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 66bc726..7558f1e 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -11,9 +11,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - // TODO: add the 2GIS MapKit maven repository here once its exact URL is - // confirmed from the 2GIS developer portal (see android/README section - // on the map SDK integration point). + maven { url = uri("https://artifactory.2gis.dev/sdk-maven-release") } } }