diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml new file mode 100644 index 00000000..1a43e54d --- /dev/null +++ b/.github/workflows/pull_requests.yml @@ -0,0 +1,18 @@ +name: PR Checks +on: [pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2.4.0 + - + name: Configure Java + uses: actions/setup-java@v4.0.0 + with: + java-version: 17 + distribution: oracle + - + name: Tests + run: ./gradlew android-application:testStubDebugUnitTest android-extensions:testDebugUnitTest aprs-android:testDebugUnitTest maps:testDebugUnitTest diff --git a/android-application/build.gradle.kts b/android-application/build.gradle.kts index 949024c4..8fba81fb 100644 --- a/android-application/build.gradle.kts +++ b/android-application/build.gradle.kts @@ -21,7 +21,6 @@ android { minSdk = 21 targetSdk = 34 multiDexEnabled = true - buildConfigField("String", "MAPBOX_ACCESS_TOKEN", optionalStringProperty("mapboxPublic").buildQuote()) buildConfigField("boolean", "USE_GOOGLE_SERVICES", useGoogleServices.toString()) buildConfigField("String", "COMMIT", optionalStringProperty("commit").buildQuote()) versionCode = intProperty("versionCode", 1) @@ -63,6 +62,16 @@ android { proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") } } + flavorDimensions += "function" + productFlavors { + create("functional") { + isDefault = true + dimension = "function" + } + create("stub") { + dimension = "function" + } + } compileOptions { targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8 @@ -108,8 +117,6 @@ dependencies { implementation(libs.google.material.core) - implementation(libs.mapbox.android.sdk) - implementation(libs.bundles.dagger.libraries) kapt(libs.bundles.dagger.kapt) @@ -127,6 +134,9 @@ dependencies { implementation(libs.firebase.config) implementation(libs.firebase.analytics) + implementation(projects.maps) + "functionalImplementation"(projects.mapbox) + testImplementation(libs.junit) testImplementation(libs.coroutines.test) testImplementation(libs.kotlin.test.core) diff --git a/android-application/src/functional/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt b/android-application/src/functional/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt new file mode 100644 index 00000000..6ff2d232 --- /dev/null +++ b/android-application/src/functional/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt @@ -0,0 +1,5 @@ +package com.inkapplications.ack.android.maps + +import com.inkapplications.ack.android.mapbox.MapboxMapRenderer + +val MapsImplementation = MapboxMapRenderer diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureActivity.kt index be980470..5ec0cbb8 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureActivity.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureActivity.kt @@ -1,17 +1,12 @@ package com.inkapplications.ack.android.capture import android.Manifest -import android.content.Context import android.content.Intent -import android.content.pm.PackageManager -import android.view.View import androidx.activity.compose.setContent import androidx.compose.runtime.collectAsState -import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.inkapplications.ack.android.R import com.inkapplications.ack.android.capture.insights.InsightsController import com.inkapplications.ack.android.capture.messages.conversation.startConversationActivity import com.inkapplications.ack.android.capture.messages.create.CreateConversationActivity @@ -21,10 +16,10 @@ import com.inkapplications.ack.android.connection.DriverSelection import com.inkapplications.ack.android.log.LogItemViewState import com.inkapplications.ack.android.log.details.startLogInspectActivity import com.inkapplications.ack.android.log.index.LogIndexController -import com.inkapplications.ack.android.map.MapController import com.inkapplications.ack.android.map.MapEvents import com.inkapplications.ack.android.map.MapViewState -import com.inkapplications.ack.android.map.mapbox.createController +import com.inkapplications.ack.android.maps.CameraPositionDefaults +import com.inkapplications.ack.android.maps.MapViewModel import com.inkapplications.ack.android.settings.SettingsActivity import com.inkapplications.ack.android.station.startStationActivity import com.inkapplications.ack.android.tnc.startConnectTncActivity @@ -34,16 +29,9 @@ import com.inkapplications.ack.structures.station.Callsign import com.inkapplications.android.PermissionGate import com.inkapplications.android.extensions.ExtendedActivity import com.inkapplications.android.startActivity -import com.inkapplications.coroutines.collectOn -import com.mapbox.maps.MapView import dagger.hilt.android.AndroidEntryPoint import kimchi.Kimchi -import kimchi.analytics.intProperty import kimchi.analytics.stringProperty -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -62,9 +50,6 @@ class CaptureActivity: ExtendedActivity(), CaptureNavController, LogIndexControl @Inject lateinit var captureEvents: CaptureEvents - private var mapView: MapView? = null - private var mapScope: CoroutineScope = MainScope() - private val mapViewState = MutableStateFlow(MapViewState()) private val permissionGate = PermissionGate(this) private val backgroundCaptureServiceIntent by lazy { Intent(this, BackgroundCaptureService::class.java) } @@ -72,7 +57,12 @@ class CaptureActivity: ExtendedActivity(), CaptureNavController, LogIndexControl super.onCreate() setContent { - val mapState = mapViewState.collectAsState() + val mapState = mapEvents.viewState.collectAsState(MapViewState( + mapViewModel = MapViewModel( + cameraPosition = CameraPositionDefaults.unknownLocation, + markers = emptyList(), + ) + )) val messagesScreenController = object: MessagesScreenController { override fun onCreateMessageClick() { startActivity(CreateConversationActivity::class) @@ -87,55 +77,21 @@ class CaptureActivity: ExtendedActivity(), CaptureNavController, LogIndexControl logIndexController = this, messagesScreenController = messagesScreenController, insightsController = this, - mapFactory = ::createMapView, controller = this, ) } } - private fun createMapView(context: Context): View { - return if(mapView != null) mapView!! else MapView(context).also { mapView -> - this.mapView = mapView - - mapView.createController(this, ::onMapLoaded, ::onMapItemSelected) - - return mapView - } - } - - private fun onMapItemSelected(id: CaptureId?) { - Kimchi.trackEvent("map_item_select") - mapEvents.selectedItemId.value = id - } - - private fun onMapLoaded(map: MapController) { - mapScope.cancel() - mapScope = MainScope() - - map.setBottomPadding(resources.getDimension(R.dimen.mapbox_logo_padding_bottom)) - - when(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)) { - PackageManager.PERMISSION_GRANTED -> map.zoomTo(mapEvents.initialState) - } - - mapEvents.viewState.collectOn(mapScope) { state -> - Kimchi.trackEvent("map_markers", listOf(intProperty("quantity", state.markers.size))) - mapViewState.emit(state) - map.showMarkers(state.markers) - - if (state.trackPosition) { - map.enablePositionTracking() - } else { - map.disablePositionTracking() - } - } - } - override fun onLogMapItemClick(log: LogItemViewState) { Kimchi.trackEvent("map_log_click") startStationActivity(log.source) } + override fun onMapItemClick(captureId: CaptureId?) { + Kimchi.trackEvent("map_item_select") + mapEvents.selectedItemId.value = captureId + } + override fun onLogListItemClick(item: LogItemViewState) { Kimchi.trackEvent("log_item_click") startLogInspectActivity(item.id) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureNavController.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureNavController.kt index b991d5e4..bfe2de44 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureNavController.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureNavController.kt @@ -2,6 +2,7 @@ package com.inkapplications.ack.android.capture import com.inkapplications.ack.android.connection.DriverSelection import com.inkapplications.ack.android.log.LogItemViewState +import com.inkapplications.ack.data.CaptureId /** * Capture navigation and control panel options. @@ -53,4 +54,9 @@ interface CaptureNavController { * Invoked when the user selects a new radio driver. */ fun onDriverSelected(selection: DriverSelection) + + /** + * Invoked when the user clicks an item on the map + */ + fun onMapItemClick(captureId: CaptureId?) } diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt index fcdccc02..4a3939c9 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt @@ -2,8 +2,6 @@ package com.inkapplications.ack.android.capture -import android.content.Context -import android.view.View import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -49,7 +47,6 @@ fun CaptureScreen( mapState: State, logIndexController: LogIndexController, messagesScreenController: MessagesScreenController, - mapFactory: (Context) -> View, controller: CaptureNavController, insightsController: InsightsController, viewModel: CaptureViewModel = hiltViewModel(), @@ -75,7 +72,6 @@ fun CaptureScreen( mapState = mapState, logIndexController = logIndexController, messagesScreenController = messagesScreenController, - mapFactory = mapFactory, captureController = controller, insightsController = insightsController, ) @@ -382,7 +378,6 @@ private fun CaptureNavHost( mapState: State, logIndexController: LogIndexController, messagesScreenController: MessagesScreenController, - mapFactory: (Context) -> View, captureController: CaptureNavController, insightsController: InsightsController, ) { @@ -394,7 +389,7 @@ private fun CaptureNavHost( composable("map") { MapScreen( state = mapState.value, - mapFactory = mapFactory, + onMapItemClick = captureController::onMapItemClick, onLogItemClick = captureController::onLogMapItemClick, onEnableLocation = captureController::onLocationEnableClick, onDisableLocation = captureController::onLocationDisableClick, diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/LogEvents.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/LogEvents.kt index b7cab036..c2b52916 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/LogEvents.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/log/LogEvents.kt @@ -3,7 +3,11 @@ package com.inkapplications.ack.android.log import com.inkapplications.ack.android.locale.LocaleSettings import com.inkapplications.ack.android.log.details.LogDetailData import com.inkapplications.ack.android.log.index.LogIndexData -import com.inkapplications.ack.android.map.* +import com.inkapplications.ack.android.map.MarkerViewStateFactory +import com.inkapplications.ack.android.maps.CameraPositionDefaults +import com.inkapplications.ack.android.maps.MapCameraPosition +import com.inkapplications.ack.android.maps.MapViewModel +import com.inkapplications.ack.android.maps.ZoomLevels import com.inkapplications.ack.android.settings.SettingsReadAccess import com.inkapplications.ack.android.settings.observeBoolean import com.inkapplications.ack.android.station.StationSettings @@ -51,13 +55,13 @@ class LogEvents @Inject constructor( } } - fun mapState(id: CaptureId): Flow { + fun mapState(id: CaptureId): Flow { return packetStorage.findById(id) .filterNotNull() .map { packet -> - MapState( + MapViewModel( markers = markerViewStateFactory.create(packet)?.let { listOf(it) }.orEmpty(), - mapCameraPosition = (packet.parsed.data as? Mapable)?.coordinates + cameraPosition = (packet.parsed.data as? Mapable)?.coordinates ?.let { MapCameraPosition(it, ZoomLevels.ROADS) } ?: CameraPositionDefaults.unknownLocation, ) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsActivity.kt index c7ea09ad..dcfd67f2 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsActivity.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsActivity.kt @@ -1,13 +1,8 @@ package com.inkapplications.ack.android.log.details import android.app.Activity -import android.content.Context import android.os.Bundle import androidx.activity.compose.setContent -import androidx.lifecycle.lifecycleScope -import com.inkapplications.ack.android.log.LogEvents -import com.inkapplications.ack.android.map.MapController -import com.inkapplications.ack.android.map.mapbox.createController import com.inkapplications.ack.android.station.startStationActivity import com.inkapplications.ack.android.trackNavigation import com.inkapplications.ack.android.ui.theme.AckScreen @@ -15,11 +10,8 @@ import com.inkapplications.ack.data.CaptureId import com.inkapplications.ack.structures.station.Callsign import com.inkapplications.android.extensions.ExtendedActivity import com.inkapplications.android.startActivity -import com.mapbox.maps.MapView import dagger.hilt.android.AndroidEntryPoint import kimchi.Kimchi -import kotlinx.coroutines.flow.collectLatest -import javax.inject.Inject const val EXTRA_LOG_ID = "aprs.station.extra.id" @@ -28,11 +20,6 @@ const val EXTRA_LOG_ID = "aprs.station.extra.id" */ @AndroidEntryPoint class LogDetailsActivity: ExtendedActivity(), LogDetailsController { - @Inject - lateinit var logEvents: LogEvents - - private var mapView: MapView? = null - private val id get() = CaptureId(intent.getLongExtra(EXTRA_LOG_ID, -1)) override fun onCreate(savedInstanceState: Bundle?) { @@ -43,29 +30,12 @@ class LogDetailsActivity: ExtendedActivity(), LogDetailsController { AckScreen { LogDetailsScreen( controller = this, - mapViewFactory = ::createMapView, ) } } } - private fun createMapView(context: Context) = mapView ?: MapView(context).also { mapView -> - this.mapView = mapView - - mapView.createController(this, ::onMapLoaded, ::onMapItemClicked) - } - - private fun onMapLoaded(map: MapController) { - Kimchi.trace("Map Loaded") - lifecycleScope.launchWhenCreated { - logEvents.mapState(id).collectLatest { state -> - map.setCamera(state.mapCameraPosition) - map.showMarkers(state.markers) - } - } - } - - private fun onMapItemClicked(id: CaptureId?) { + override fun onMapItemClicked(id: CaptureId?) { Kimchi.trackEvent("log_details_map_item_click") Kimchi.debug("Map Item Clicked: No-Op") } diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsController.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsController.kt index 7d2354b1..33a3dcdd 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsController.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsController.kt @@ -1,5 +1,6 @@ package com.inkapplications.ack.android.log.details +import com.inkapplications.ack.data.CaptureId import com.inkapplications.ack.structures.station.Callsign /** @@ -15,4 +16,11 @@ interface LogDetailsController { * Invoked when the user clicks on the station details button. */ fun onViewStationDetails(station: Callsign) + + /** + * Invoked when the user clicks on a map marker. + * + * @param id The id of the map marker that was clicked. + */ + fun onMapItemClicked(id: CaptureId?) } diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt index ca597f01..e73af9a4 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt @@ -1,8 +1,8 @@ package com.inkapplications.ack.android.log.details -import android.content.Context -import android.view.View import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -14,9 +14,9 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel import com.inkapplications.ack.android.R +import com.inkapplications.ack.android.map.MarkerMap import com.inkapplications.ack.android.ui.IconRow import com.inkapplications.ack.android.ui.NavigationRow import com.inkapplications.ack.android.ui.TelemetryTable @@ -29,13 +29,12 @@ import com.inkapplications.ack.android.ui.theme.AckTheme fun LogDetailsScreen( viewModel: LogDetailsViewModel = hiltViewModel(), controller: LogDetailsController, - mapViewFactory: (Context) -> View, ) { val viewState = viewModel.detailsState.collectAsState().value when (viewState) { is LogDetailsState.Initial -> {} - is LogDetailsState.Loaded -> Details(viewState, controller, mapViewFactory) + is LogDetailsState.Loaded -> Details(viewState, controller) } } @@ -43,14 +42,17 @@ fun LogDetailsScreen( private fun Details( viewState: LogDetailsState.Loaded, controller: LogDetailsController, - mapViewFactory: (Context) -> View, ) { - Column { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { if (viewState.mapable) { Column { Box { - AndroidView( - factory = mapViewFactory, + MarkerMap( + viewModel = viewState.mapViewState, + onMapItemClicked = controller::onMapItemClicked, + interactive = false, modifier = Modifier.aspectRatio(16f / 9f), ) IconButton( diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsState.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsState.kt index 4f53e478..8efcd062 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsState.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsState.kt @@ -1,6 +1,7 @@ package com.inkapplications.ack.android.log.details import androidx.compose.ui.graphics.vector.ImageVector +import com.inkapplications.ack.android.maps.MapViewModel import com.inkapplications.ack.structures.TelemetryValues import com.inkapplications.ack.structures.station.Callsign @@ -23,6 +24,7 @@ sealed interface LogDetailsState { val receiveIconDescription: String, val timestamp: String, val mapable: Boolean, + val mapViewState: MapViewModel, val comment: String? = null, val temperature: String? = null, val wind: String? = null, diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactory.kt index 27a15bf2..d044baa7 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactory.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactory.kt @@ -8,6 +8,8 @@ import androidx.compose.material.icons.filled.Storage import com.inkapplications.ack.android.R import com.inkapplications.ack.android.locale.format import com.inkapplications.ack.android.log.SummaryFactory +import com.inkapplications.ack.android.maps.* +import com.inkapplications.ack.data.CapturedPacket import com.inkapplications.ack.data.PacketOrigin import com.inkapplications.ack.structures.PacketData.TelemetryReport import com.inkapplications.ack.structures.PacketData.Weather @@ -15,6 +17,7 @@ import com.inkapplications.ack.structures.capabilities.Commented import com.inkapplications.ack.structures.capabilities.Mapable import com.inkapplications.ack.structures.capabilities.Report import com.inkapplications.android.extensions.StringResources +import com.inkapplications.android.extensions.ViewStateFactory import com.inkapplications.android.extensions.format.DateTimeFormatter import javax.inject.Inject @@ -23,6 +26,7 @@ import javax.inject.Inject */ class LogDetailsViewStateFactory @Inject constructor( private val summaryFactory: SummaryFactory, + private val markerViewStateFactory: ViewStateFactory, private val timeFormatter: DateTimeFormatter, private val stringResources: StringResources, ) { @@ -35,6 +39,12 @@ class LogDetailsViewStateFactory @Inject constructor( .let { stringResources.getString(R.string.log_details_received_format, it) }, comment = (packetData as? Commented)?.comment, mapable = packetData is Mapable && packetData.coordinates != null, + mapViewState = MapViewModel( + markers = markerViewStateFactory.create(data.packet)?.let { listOf(it) }.orEmpty(), + cameraPosition = (data.packet.parsed.data as? Mapable)?.coordinates + ?.let { MapCameraPosition(it, ZoomLevels.ROADS) } + ?: CameraPositionDefaults.unknownLocation, + ), temperature = (packetData as? Weather)?.temperature?.format(data.metric), wind = (packetData as? Weather)?.let { summaryFactory.createWindSummary(it.windData, data.metric) }, altitude = (packetData as? Report)?.altitude?.format(data.metric), diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapDataRepository.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapDataRepository.kt index 71e4cedb..bf21d8e8 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapDataRepository.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/map/MapDataRepository.kt @@ -3,6 +3,7 @@ package com.inkapplications.ack.android.map import com.inkapplications.ack.android.locale.LocaleSettings import com.inkapplications.ack.android.log.CombinedLogItemViewStateFactory import com.inkapplications.ack.android.log.LogItemViewState +import com.inkapplications.ack.android.maps.MarkerViewState import com.inkapplications.ack.android.settings.SettingsReadAccess import com.inkapplications.ack.android.settings.observeBoolean import com.inkapplications.ack.android.settings.observeInt diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapEvents.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapEvents.kt index ad7c3699..d0fa7a8b 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapEvents.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/map/MapEvents.kt @@ -1,5 +1,13 @@ package com.inkapplications.ack.android.map +import android.Manifest.permission.ACCESS_COARSE_LOCATION +import android.content.Context +import android.content.pm.PackageManager.PERMISSION_GRANTED +import androidx.core.content.ContextCompat +import com.inkapplications.ack.android.maps.CameraPositionDefaults +import com.inkapplications.ack.android.maps.MapCameraPosition +import com.inkapplications.ack.android.maps.MapViewModel +import com.inkapplications.ack.android.maps.ZoomLevels import com.inkapplications.ack.data.CaptureId import com.inkapplications.android.extensions.location.LocationAccess import kotlinx.coroutines.flow.* @@ -12,26 +20,33 @@ import javax.inject.Singleton @Singleton class MapEvents @Inject constructor( private val mapData: MapDataRepository, - private val location: LocationAccess, + location: LocationAccess, + context: Context, ) { val trackingEnabled = MutableStateFlow(false) val selectedItemId = MutableStateFlow(null) private val selectedItem = selectedItemId.flatMapLatest { it?.let { mapData.findLogItem(it) } ?: flowOf(null) } + private val lastLocation = if (ContextCompat.checkSelfPermission(context, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED) { + location.lastKnownLocation?.location + } else null - val initialState get() = - location.lastKnownLocation - ?.let { MapCameraPosition(it.location, ZoomLevels.RIVERS) } - ?: CameraPositionDefaults.unknownLocation - val viewState: Flow = mapData.findMarkers() - .map { MapViewState(markers = it) } - .combine(selectedItem) { viewModel, selectedItem -> - viewModel.copy(selectedItem = selectedItem) - } - .combine(trackingEnabled) { viewModel, tracking -> - viewModel.copy(trackPosition = tracking) - } - .distinctUntilChanged() + val viewState = combine( + mapData.findMarkers(), + trackingEnabled, + selectedItem, + ) { markers, tracking, selected -> + MapViewState( + MapViewModel( + cameraPosition = (lastLocation ?: markers.firstOrNull()?.coordinates) + ?.let { MapCameraPosition(it, ZoomLevels.RIVERS) } + ?: CameraPositionDefaults.unknownLocation, + markers = markers, + enablePositionTracking = tracking + ), + selectedItem = selected, + ) + }.distinctUntilChanged() } diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapModule.kt index fb288137..29b6aa38 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapModule.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/map/MapModule.kt @@ -1,5 +1,6 @@ package com.inkapplications.ack.android.map +import com.inkapplications.ack.android.maps.MarkerViewState import com.inkapplications.ack.android.settings.SettingsProvider import com.inkapplications.ack.data.CapturedPacket import com.inkapplications.android.extensions.ViewStateFactory diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt index 44681124..e0ca4804 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt @@ -1,7 +1,5 @@ package com.inkapplications.ack.android.map -import android.content.Context -import android.view.View import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -15,26 +13,29 @@ import androidx.compose.material.icons.filled.MyLocation import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp -import androidx.compose.ui.viewinterop.AndroidView import com.inkapplications.ack.android.R import com.inkapplications.ack.android.log.AprsLogItem import com.inkapplications.ack.android.log.LogItemViewState import com.inkapplications.ack.android.ui.theme.AckScreen import com.inkapplications.ack.android.ui.theme.AckTheme +import com.inkapplications.ack.data.CaptureId @Composable fun MapScreen( state: MapViewState, - mapFactory: (Context) -> View, + onMapItemClick: (CaptureId?) -> Unit, onLogItemClick: (LogItemViewState) -> Unit, onEnableLocation: () -> Unit, onDisableLocation: () -> Unit, bottomContentProtection: Dp, ) = AckScreen { - AndroidView( - factory = mapFactory, + MarkerMap( + viewModel = state.mapViewModel, + onMapItemClicked = onMapItemClick, + bottomProtection = dimensionResource(R.dimen.mapbox_logo_padding_bottom), modifier = Modifier.fillMaxSize() ) val logState = state.selectedItem @@ -53,7 +54,7 @@ fun MapScreen( .padding(bottom = bottomContentProtection), contentAlignment = Alignment.BottomEnd, ) { - LocationStateButton(state.trackPosition, onEnableLocation, onDisableLocation) + LocationStateButton(state.mapViewModel.enablePositionTracking, onEnableLocation, onDisableLocation) } } diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapState.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapState.kt deleted file mode 100644 index a6c4545f..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.android.map - -/** - * State container for possible map data/settings. - */ -data class MapState( - /** - * Collection of marker states to add to the map. - */ - val markers: List = emptyList(), - - /** - * Camera position of the map. - */ - val mapCameraPosition: MapCameraPosition = CameraPositionDefaults.unknownLocation, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapViewState.kt index 6144bfc7..1d9a3a61 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapViewState.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/map/MapViewState.kt @@ -1,6 +1,7 @@ package com.inkapplications.ack.android.map import com.inkapplications.ack.android.log.LogItemViewState +import com.inkapplications.ack.android.maps.MapViewModel /** * Model the state of the entire map page. @@ -9,9 +10,8 @@ import com.inkapplications.ack.android.log.LogItemViewState * separately. */ data class MapViewState( - val markers: Collection = emptyList(), + val mapViewModel: MapViewModel, val selectedItem: LogItemViewState? = null, - val trackPosition: Boolean = false, ) { val selectedItemVisible = selectedItem != null } diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerMap.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerMap.kt new file mode 100644 index 00000000..af13da3c --- /dev/null +++ b/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerMap.kt @@ -0,0 +1,25 @@ +package com.inkapplications.ack.android.map + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.inkapplications.ack.android.maps.MapViewModel +import com.inkapplications.ack.android.maps.MapsImplementation +import com.inkapplications.ack.data.CaptureId + + +@Composable +fun MarkerMap( + viewModel: MapViewModel, + onMapItemClicked: (CaptureId?) -> Unit, + bottomProtection: Dp = 0.dp, + interactive: Boolean = true, + modifier: Modifier, +) = MapsImplementation.renderMarkerMap( + viewModel = viewModel, + onMapItemClicked = onMapItemClicked, + bottomProtection = 0.dp, + interactive = interactive, + modifier = modifier, +) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewStateFactory.kt index 246ea03d..b4df2c53 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewStateFactory.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewStateFactory.kt @@ -1,5 +1,6 @@ package com.inkapplications.ack.android.map +import com.inkapplications.ack.android.maps.MarkerViewState import com.inkapplications.ack.android.symbol.SymbolFactory import com.inkapplications.ack.data.CapturedPacket import com.inkapplications.ack.structures.PacketData diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/mapbox/MapboxMapViews.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/mapbox/MapboxMapViews.kt deleted file mode 100644 index 5fa2bbf1..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/mapbox/MapboxMapViews.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.inkapplications.ack.android.map.mapbox - -import android.content.Context -import android.content.res.Configuration -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import com.inkapplications.ack.android.map.MapCameraPosition -import com.inkapplications.ack.android.map.MapController -import com.inkapplications.ack.android.map.MarkerViewState -import com.inkapplications.ack.data.CaptureId -import com.mapbox.maps.MapView -import com.mapbox.maps.Style - -/** - * Create a Map controller by initializing a Mapbox Map. - */ -inline fun MapView.createController( - activity: Context, - crossinline onInit: (MapController) -> Unit, - noinline onSelect: (CaptureId?) -> Unit, -) { - val map = getMapboxMap() - val styleUri = if (activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { - Style.DARK - } else { - Style.LIGHT - } - - map.loadStyleUri(styleUri) { style -> - val controller = MapboxMapController(this, map, style, activity.resources, onSelect = onSelect) - controller.initDefaults() - onInit(controller) - } -} - -@Composable -fun MarkerMap( - markers: Collection, - cameraPosition: MapCameraPosition, - onMapItemClicked: (CaptureId?) -> Unit, - modifier: Modifier = Modifier, -) { - var controllerState by remember { mutableStateOf(null) } - AndroidView( - factory = { context -> - MapView(context).apply { - createController( - activity = context, - onInit = { controller -> - controllerState = controller - controller.setCamera(cameraPosition) - controller.showMarkers(markers) - }, - onSelect = onMapItemClicked, - ) - } - }, - update = { mapView -> - controllerState?.let { controller -> - controller.setCamera(cameraPosition) - controller.showMarkers(markers) - } - }, - modifier = modifier, - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/MapboxInitializer.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/MapboxInitializer.kt deleted file mode 100644 index 126ee8d9..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/MapboxInitializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.inkapplications.ack.android.startup - -import com.inkapplications.ack.android.BuildConfig -import com.mapbox.maps.ResourceOptionsManager - -/** - * Initialize Mapbox. - */ -val MapboxInitializer = ApplicationInitializer { - ResourceOptionsManager.getDefault(this, BuildConfig.MAPBOX_ACCESS_TOKEN) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupModule.kt index f1a3a9c6..1cb92e51 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupModule.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupModule.kt @@ -1,6 +1,8 @@ package com.inkapplications.ack.android.startup +import android.app.Application import com.inkapplications.ack.android.capture.service.CaptureServiceNotifications +import com.inkapplications.ack.android.maps.MapsImplementation import dagger.Binds import dagger.Module import dagger.Provides @@ -22,7 +24,11 @@ class StartupModule { fun initializers( captureService: CaptureServiceNotifications ): Set = setOf( - MapboxInitializer, + object: ApplicationInitializer { + override suspend fun initialize(application: Application) { + MapsImplementation.initialize(application) + } + }, KimchiInitializer, captureService, ) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/InsightViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/InsightViewState.kt index b9927e65..2c717b88 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/InsightViewState.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/station/InsightViewState.kt @@ -1,8 +1,5 @@ package com.inkapplications.ack.android.station -import com.inkapplications.ack.android.map.CameraPositionDefaults -import com.inkapplications.ack.android.map.MapCameraPosition -import com.inkapplications.ack.android.map.MarkerViewState import com.inkapplications.ack.structures.TelemetryValues /** @@ -10,8 +7,6 @@ import com.inkapplications.ack.structures.TelemetryValues */ data class InsightViewState( val name: String = "", - val markers: List = emptyList(), - val mapCameraPosition: MapCameraPosition = CameraPositionDefaults.unknownLocation, val comment: String? = null, val temperature: String? = null, val wind: String? = null, diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationActivity.kt index 1a69e494..af967d04 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationActivity.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/station/StationActivity.kt @@ -13,7 +13,6 @@ import com.inkapplications.android.extensions.ExtendedActivity import com.inkapplications.android.startActivity import dagger.hilt.android.AndroidEntryPoint import kimchi.Kimchi -import javax.inject.Inject const val EXTRA_CALLSIGN = "aprs.station.extra.callsign" @@ -22,9 +21,6 @@ const val EXTRA_CALLSIGN = "aprs.station.extra.callsign" */ @AndroidEntryPoint class StationActivity: ExtendedActivity(), StationScreenController { - @Inject - lateinit var stationEvents: StationEvents - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Kimchi.trackScreen("station") diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationEvents.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationEvents.kt index 6d769aae..4d5349a2 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationEvents.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/station/StationEvents.kt @@ -1,7 +1,6 @@ package com.inkapplications.ack.android.station import com.inkapplications.ack.android.locale.LocaleSettings -import com.inkapplications.ack.android.log.LogItemViewStateFactory import com.inkapplications.ack.android.settings.SettingsReadAccess import com.inkapplications.ack.android.settings.observeBoolean import com.inkapplications.ack.android.settings.observeInt @@ -11,14 +10,15 @@ import com.inkapplications.ack.structures.station.Callsign import dagger.Reusable import kimchi.logger.EmptyLogger import kimchi.logger.KimchiLogger -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest import javax.inject.Inject @Reusable class StationEvents @Inject constructor( private val aprs: PacketStorage, - private val stationInsightViewStateFactory: StationInsightViewStateFactory, - private val logItemViewStateFactory: LogItemViewStateFactory, private val settings: SettingsReadAccess, private val localeSettings: LocaleSettings, private val stationSettings: StationSettings, @@ -37,25 +37,4 @@ class StationEvents @Inject constructor( ) } } - - fun stationState(callsign: Callsign): Flow { - logger.trace("Observing Callsign: $callsign") - - return settings.observeInt(stationSettings.recentStationEvents) - .flatMapLatest { aprs.findBySource(callsign, it.toLong()) } - .combine(settings.observeBoolean(localeSettings.preferMetric)) { packets, metric -> - StationData( - packets = packets, - metric = metric, - ) - } - .map { data -> - StationViewState.Loaded( - insight = stationInsightViewStateFactory.create(data), - packets = data.packets.map { packet -> - logItemViewStateFactory.create(packet.id, packet.parsed, data.metric) - } - ) - } - } } diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationInsightViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationInsightViewStateFactory.kt index 89635dc4..ca25391c 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationInsightViewStateFactory.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/station/StationInsightViewStateFactory.kt @@ -2,14 +2,8 @@ package com.inkapplications.ack.android.station import com.inkapplications.ack.android.locale.format import com.inkapplications.ack.android.log.SummaryFactory -import com.inkapplications.ack.android.map.CameraPositionDefaults -import com.inkapplications.ack.android.map.MapCameraPosition -import com.inkapplications.ack.android.map.MarkerViewState -import com.inkapplications.ack.android.map.ZoomLevels -import com.inkapplications.ack.data.CapturedPacket import com.inkapplications.ack.structures.PacketData import com.inkapplications.ack.structures.capabilities.Commented -import com.inkapplications.ack.structures.capabilities.Mapable import com.inkapplications.ack.structures.capabilities.Report import com.inkapplications.android.extensions.ViewStateFactory import dagger.Reusable @@ -20,21 +14,13 @@ import javax.inject.Inject */ @Reusable class StationInsightViewStateFactory @Inject constructor( - private val markerViewStateFactory: ViewStateFactory, private val summaryFactory: SummaryFactory, ): ViewStateFactory { override fun create(data: StationData): InsightViewState { - val firstMapable = data.packets.firstOrNull { it.parsed.data is Mapable } val firstWeatherData = data.packets.firstNotNullOfOrNull { it.parsed.data as? PacketData.Weather } return InsightViewState( name = data.packets.first().parsed.route.source.callsign.canonical, - markers = firstMapable?.let { markerViewStateFactory.create(it) } - ?.let { listOf(it) } - .orEmpty(), - mapCameraPosition = (firstMapable?.parsed?.data as? Mapable)?.coordinates - ?.let { MapCameraPosition(it, ZoomLevels.ROADS) } - ?: CameraPositionDefaults.unknownLocation, comment = data.packets.firstNotNullOfOrNull { (it.parsed.data as? Commented)?.comment }, temperature = firstWeatherData?.temperature ?.format(data.metric), diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt index a79e51e9..72c37b14 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.inkapplications.ack.android.R import com.inkapplications.ack.android.log.AprsLogItem -import com.inkapplications.ack.android.map.mapbox.MarkerMap +import com.inkapplications.ack.android.map.MarkerMap import com.inkapplications.ack.android.ui.IconRow import com.inkapplications.ack.android.ui.NavigationRow import com.inkapplications.ack.android.ui.TelemetryTable @@ -41,13 +41,13 @@ private fun StationDetails( Column( modifier = Modifier.verticalScroll(rememberScrollState()), ) { - if (viewState.insight.markers.isNotEmpty()) { + if (viewState.mapState.markers.isNotEmpty()) { Column { Box { MarkerMap( - markers = viewState.insight.markers, - cameraPosition = viewState.insight.mapCameraPosition, + viewModel = viewState.mapState, onMapItemClicked = controller::onMapItemClicked, + interactive = false, modifier = Modifier.aspectRatio(16f / 9f), ) IconButton( diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewModel.kt index c2a3a416..149fcd05 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewModel.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewModel.kt @@ -3,7 +3,11 @@ package com.inkapplications.ack.android.station import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.inkapplications.ack.android.log.LogItemViewStateFactory +import com.inkapplications.ack.android.maps.* +import com.inkapplications.ack.data.CapturedPacket +import com.inkapplications.ack.structures.capabilities.Mapable import com.inkapplications.ack.structures.station.Callsign +import com.inkapplications.android.extensions.ViewStateFactory import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map @@ -19,6 +23,7 @@ class StationViewModel @Inject constructor( stationEvents: StationEvents, stationInsightViewStateFactory: StationInsightViewStateFactory, logItemViewStateFactory: LogItemViewStateFactory, + markerViewStateFactory: ViewStateFactory, ): androidx.lifecycle.ViewModel() { private val stationCallsign = savedState.get(EXTRA_CALLSIGN)!!.let(::Callsign) val stationState = stationEvents.stationData(stationCallsign) @@ -27,7 +32,18 @@ class StationViewModel @Inject constructor( insight = stationInsightViewStateFactory.create(data), packets = data.packets.map { packet -> logItemViewStateFactory.create(packet.id, packet.parsed, data.metric) - } + }, + mapState = data.packets.firstOrNull { it.parsed.data is Mapable } + .let { firstMapable -> + MapViewModel( + markers = firstMapable?.let { markerViewStateFactory.create(it) } + ?.let { listOf(it) } + .orEmpty(), + cameraPosition = (firstMapable?.parsed?.data as? Mapable)?.coordinates + ?.let { MapCameraPosition(it, ZoomLevels.ROADS) } + ?: CameraPositionDefaults.unknownLocation, + ) + } ) } .stateIn(viewModelScope, SharingStarted.Eagerly, StationViewState.Initial) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewState.kt index 51d66da2..7b821662 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewState.kt +++ b/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewState.kt @@ -1,11 +1,13 @@ package com.inkapplications.ack.android.station import com.inkapplications.ack.android.log.LogItemViewState +import com.inkapplications.ack.android.maps.MapViewModel sealed interface StationViewState { object Initial: StationViewState data class Loaded( val insight: InsightViewState, val packets: List, + val mapState: MapViewModel, ): StationViewState } diff --git a/android-application/src/stub/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt b/android-application/src/stub/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt new file mode 100644 index 00000000..eea6979e --- /dev/null +++ b/android-application/src/stub/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt @@ -0,0 +1,3 @@ +package com.inkapplications.ack.android.maps + +val MapsImplementation = DummyMapRenderer diff --git a/android-application/src/test/java/com/inkapplications/ack/android/TestDoubles.kt b/android-application/src/test/java/com/inkapplications/ack/android/TestDoubles.kt index 28ea6954..6542e778 100644 --- a/android-application/src/test/java/com/inkapplications/ack/android/TestDoubles.kt +++ b/android-application/src/test/java/com/inkapplications/ack/android/TestDoubles.kt @@ -1,6 +1,6 @@ package com.inkapplications.ack.android -import com.inkapplications.ack.android.map.MarkerViewState +import com.inkapplications.ack.android.maps.MarkerViewState import com.inkapplications.ack.android.settings.BooleanSetting import com.inkapplications.ack.android.settings.IntSetting import com.inkapplications.ack.android.settings.SettingsReadAccess diff --git a/android-application/src/test/java/com/inkapplications/ack/android/log/details/LogDetailsViewModelFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactoryTest.kt similarity index 97% rename from android-application/src/test/java/com/inkapplications/ack/android/log/details/LogDetailsViewModelFactoryTest.kt rename to android-application/src/test/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactoryTest.kt index c9d74704..203f927c 100644 --- a/android-application/src/test/java/com/inkapplications/ack/android/log/details/LogDetailsViewModelFactoryTest.kt +++ b/android-application/src/test/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactoryTest.kt @@ -24,10 +24,10 @@ import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue -class StationViewStateFactoryTest { +class LogDetailsViewStateFactoryTest { val dummySummaryFactory = SummaryFactory(ParrotStringResources) - val factoryWithNullMarkers = LogDetailsViewStateFactory(dummySummaryFactory, EpochFormatterFake, ParrotStringResources) - val factoryWithDummyMarkers = LogDetailsViewStateFactory(dummySummaryFactory, EpochFormatterFake, ParrotStringResources) + val factoryWithNullMarkers = LogDetailsViewStateFactory(dummySummaryFactory, NullMarkerFactoryMock, EpochFormatterFake, ParrotStringResources) + val factoryWithDummyMarkers = LogDetailsViewStateFactory(dummySummaryFactory, DummyMarkerFactoryMock, EpochFormatterFake, ParrotStringResources) @Test fun nameFormatting() { diff --git a/android-application/src/test/java/com/inkapplications/ack/android/station/StationViewStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/station/StationInsightViewStateFactoryTest.kt similarity index 84% rename from android-application/src/test/java/com/inkapplications/ack/android/station/StationViewStateFactoryTest.kt rename to android-application/src/test/java/com/inkapplications/ack/android/station/StationInsightViewStateFactoryTest.kt index 27958ce9..731f1df4 100644 --- a/android-application/src/test/java/com/inkapplications/ack/android/station/StationViewStateFactoryTest.kt +++ b/android-application/src/test/java/com/inkapplications/ack/android/station/StationInsightViewStateFactoryTest.kt @@ -1,8 +1,10 @@ package com.inkapplications.ack.android.station -import com.inkapplications.ack.android.* +import com.inkapplications.ack.android.ParrotStringResources import com.inkapplications.ack.android.log.SummaryFactory -import com.inkapplications.ack.android.map.MarkerViewState +import com.inkapplications.ack.android.testRoute +import com.inkapplications.ack.android.toTestCapturedPacket +import com.inkapplications.ack.android.toTestPacket import com.inkapplications.ack.data.CaptureId import com.inkapplications.ack.data.CapturedPacket import com.inkapplications.ack.data.PacketOrigin @@ -21,15 +23,13 @@ import kotlinx.datetime.Instant import org.junit.Test import kotlin.test.assertEquals import kotlin.test.assertNull -import kotlin.test.assertTrue -class StationViewStateFactoryTest { +class StationInsightViewStateFactoryTest { val dummySummaryFactory = SummaryFactory(ParrotStringResources) - val dummyMarker = MarkerViewState(CaptureId(0), GeoCoordinates(0.latitude, 0.longitude), null) @Test fun unknownPacket() { - val factory = StationInsightViewStateFactory(NullMarkerFactoryMock, dummySummaryFactory) + val factory = StationInsightViewStateFactory(dummySummaryFactory) val packet = CapturedPacket( id = CaptureId(1), received = Instant.fromEpochMilliseconds(2), @@ -51,7 +51,6 @@ class StationViewStateFactoryTest { val result = factory.create(data) assertEquals("KE0YOG", result.name) - assertTrue(result.markers.isEmpty(), "No map markers for unknown packet") assertNull(result.temperature, "No temperature for non weather packet") assertNull(result.wind, "No wind data for non weather packet") assertNull(result.comment, "No comment for unknown packet") @@ -62,7 +61,7 @@ class StationViewStateFactoryTest { @Test fun positionlessWeatherPacket() { - val factory = StationInsightViewStateFactory(NullMarkerFactoryMock, dummySummaryFactory) + val factory = StationInsightViewStateFactory(dummySummaryFactory) val packet = PacketData.Weather( temperature = 72.fahrenheit, windData = WindData( @@ -79,7 +78,6 @@ class StationViewStateFactoryTest { val result = factory.create(data) assertEquals("KE0YOG", result.name) - assertTrue(result.markers.isEmpty(), "No map markers for positionless weather") assertEquals("72ºF", result.temperature) assertEquals("12º|34mph|56mph", result.wind) assertNull(result.comment, "No comment for positionless weather") @@ -90,7 +88,7 @@ class StationViewStateFactoryTest { @Test fun weatherPacket() { - val factory = StationInsightViewStateFactory(DummyMarkerFactoryMock, dummySummaryFactory) + val factory = StationInsightViewStateFactory(dummySummaryFactory) val packet = PacketData.Weather( coordinates = GeoCoordinates(1.0.latitude, 2.0.longitude), temperature = 72.fahrenheit, @@ -104,7 +102,6 @@ class StationViewStateFactoryTest { val result = factory.create(data) assertEquals("KE0YOG", result.name) - assertEquals(1, result.markers.size) assertEquals("72ºF", result.temperature) assertEquals("12º|34mph|56mph", result.wind) assertNull(result.comment, "No comment for weather packet") @@ -115,7 +112,7 @@ class StationViewStateFactoryTest { @Test fun emptyWeatherPacket() { - val factory = StationInsightViewStateFactory(DummyMarkerFactoryMock, dummySummaryFactory) + val factory = StationInsightViewStateFactory(dummySummaryFactory) val packet = PacketData.Weather( coordinates = GeoCoordinates(1.latitude, 2.longitude), ).toTestPacket().toTestCapturedPacket() @@ -127,7 +124,6 @@ class StationViewStateFactoryTest { val result = factory.create(data) assertEquals("KE0YOG", result.name) - assertEquals(1, result.markers.size) assertNull(result.temperature, "No temperature for empty weather packet") assertNull(result.wind, "No wind data for empty non weather packet") assertNull(result.comment, "No comment for unknown packet") @@ -138,7 +134,7 @@ class StationViewStateFactoryTest { @Test fun positionPacket() { - val factory = StationInsightViewStateFactory(DummyMarkerFactoryMock, dummySummaryFactory) + val factory = StationInsightViewStateFactory(dummySummaryFactory) val packet = PacketData.Position( coordinates = GeoCoordinates(1.latitude, 2.longitude), symbol = symbolOf('/', 'a'), @@ -152,7 +148,6 @@ class StationViewStateFactoryTest { val result = factory.create(data) assertEquals("KE0YOG", result.name) - assertEquals(1, result.markers.size) assertNull(result.temperature, "No temperature for non weather packet") assertNull(result.wind, "No wind data for non weather packet") assertEquals("test", result.comment) diff --git a/build.gradle.kts b/build.gradle.kts index b6d7f769..477125ae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,12 +6,14 @@ subprojects { mavenCentral() google() maven(url = "https://jitpack.io") - maven(url = "https://api.mapbox.com/downloads/v2/releases/maven") { - authentication { - create("basic") + if (!optionalStringProperty("mapboxPrivate").isNullOrBlank()) { + maven(url = "https://api.mapbox.com/downloads/v2/releases/maven") { + authentication { + create("basic") + } + credentials.username = "mapbox" + credentials.password = stringProperty("mapboxPrivate", "") } - credentials.username = "mapbox" - credentials.password = stringProperty("mapboxPrivate", "") } mavenLocal() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d4cd7804..3e9ea0aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,11 +69,11 @@ version = "1.2.0" module = "androidx.preference:preference-ktx" [libraries.androidx-compose-ui] -version = "1.4.3" +version = "1.6.1" module = "androidx.compose.ui:ui" [libraries.androidx-compose-foundation] -version = "1.4.3" +version = "1.6.1" module = "androidx.compose.foundation:foundation" [libraries.androidx-compose-material-core] @@ -104,6 +104,10 @@ module = "androidx.compose.material:material-icons-extended" version = "2.0.4" module = "com.android.tools:desugar_jdk_libs" +[libraries.androidx-annotation] +version = "1.7.1" +module = "androidx.annotation:annotation" + ## # Ink Dependencies ## diff --git a/mapbox/build.gradle.kts b/mapbox/build.gradle.kts new file mode 100644 index 00000000..51967dec --- /dev/null +++ b/mapbox/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("com.android.library") + kotlin("android") +} + +android { + namespace = "com.inkapplications.ack.android.mapbox" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + buildConfigField("String", "MAPBOX_ACCESS_TOKEN", optionalStringProperty("mapboxPublic").buildQuote()) + } + buildFeatures { + compose = true + buildConfig = true + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } + compileOptions { + targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 + } +} + +dependencies { + api(projects.maps) + implementation(projects.aprsAndroid) + implementation(libs.androidx.annotation) + api(libs.androidx.compose.foundation) + implementation(libs.mapbox.android.sdk) + implementation(libs.watermelon.android) +} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/mapbox/MapboxMapController.kt b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapController.kt similarity index 92% rename from android-application/src/main/java/com/inkapplications/ack/android/map/mapbox/MapboxMapController.kt rename to mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapController.kt index 346d60d9..fb5c627a 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/mapbox/MapboxMapController.kt +++ b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapController.kt @@ -1,4 +1,4 @@ -package com.inkapplications.ack.android.map.mapbox +package com.inkapplications.ack.android.mapbox import android.Manifest import android.annotation.SuppressLint @@ -7,8 +7,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import androidx.annotation.RequiresPermission import com.google.gson.JsonObject -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.map.* +import com.inkapplications.ack.android.maps.* import com.inkapplications.ack.data.CaptureId import com.inkapplications.android.continuePropagation import com.mapbox.geojson.Point @@ -22,6 +21,7 @@ import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager import com.mapbox.maps.plugin.attribution.attribution import com.mapbox.maps.plugin.gestures.addOnMapClickListener +import com.mapbox.maps.plugin.gestures.gestures import com.mapbox.maps.plugin.locationcomponent.location import com.mapbox.maps.plugin.logo.logo import com.mapbox.maps.plugin.scalebar.scalebar @@ -57,11 +57,13 @@ class MapboxMapController( map.addOnMapClickListener { continuePropagation { onSelect(null) } } - style.addImage(defaultMarkerId, BitmapFactory.decodeResource(resources, R.drawable.symbol_14)) + style.addImage(defaultMarkerId, BitmapFactory.decodeResource(resources, R.drawable.default_marker)) } override fun initDefaults() { view.scalebar.enabled = false + view.gestures.rotateEnabled = false + view.gestures.pitchEnabled = false setCamera(CameraPositionDefaults.unknownLocation) view.location.updateSettings { pulsingEnabled = true @@ -124,6 +126,10 @@ class MapboxMapController( view.location.enabled = false } + override fun setPanEnabled(boolean: Boolean) { + view.gestures.scrollEnabled = boolean + } + private fun createImage(image: Bitmap, style: Style): String { val id = UUID.randomUUID().toString() diff --git a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapRenderer.kt b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapRenderer.kt new file mode 100644 index 00000000..4aacd35d --- /dev/null +++ b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapRenderer.kt @@ -0,0 +1,74 @@ +package com.inkapplications.ack.android.mapbox + +import android.annotation.SuppressLint +import android.app.Application +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.viewinterop.AndroidView +import com.inkapplications.ack.android.maps.MapController +import com.inkapplications.ack.android.maps.MapRenderer +import com.inkapplications.ack.android.maps.MapViewModel +import com.inkapplications.ack.data.CaptureId +import com.mapbox.maps.MapView +import com.mapbox.maps.ResourceOptionsManager +import kotlinx.coroutines.delay + +object MapboxMapRenderer: MapRenderer { + @SuppressLint("MissingPermission") + @Composable + override fun renderMarkerMap( + viewModel: MapViewModel, + onMapItemClicked: (CaptureId?) -> Unit, + bottomProtection: Dp, + interactive: Boolean, + modifier: Modifier + ) { + var controllerState by remember { mutableStateOf(null) } + val bottomProtectionPx = with(LocalDensity.current) { + bottomProtection.toPx() + } + AndroidView( + factory = { context -> + MapView(context).apply { + createController( + activity = context, + onInit = { controller -> + controllerState = controller + controller.setCamera(viewModel.cameraPosition) + controller.showMarkers(viewModel.markers) + controller.setBottomPadding(bottomProtectionPx) + controller.setPanEnabled(interactive) + if (viewModel.enablePositionTracking) { + controller.enablePositionTracking() + } else { + controller.disablePositionTracking() + } + }, + onSelect = onMapItemClicked, + ) + } + }, + update = { mapView -> + controllerState?.let { controller -> + controller.setCamera(viewModel.cameraPosition) + controller.showMarkers(viewModel.markers) + controller.setBottomPadding(bottomProtectionPx) + controller.setPanEnabled(interactive) + if (viewModel.enablePositionTracking) { + controller.enablePositionTracking() + } else { + controller.disablePositionTracking() + } + } + }, + modifier = modifier, + ) + } + + override suspend fun initialize(application: Application) { + ResourceOptionsManager.getDefault(application, BuildConfig.MAPBOX_ACCESS_TOKEN) + delay(500) // Fix race condition in MapView initialization + } +} diff --git a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapViews.kt b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapViews.kt new file mode 100644 index 00000000..df3785df --- /dev/null +++ b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapViews.kt @@ -0,0 +1,30 @@ +package com.inkapplications.ack.android.mapbox + +import android.content.Context +import android.content.res.Configuration +import com.inkapplications.ack.android.maps.MapController +import com.inkapplications.ack.data.CaptureId +import com.mapbox.maps.MapView +import com.mapbox.maps.Style + +/** + * Create a Map controller by initializing a Mapbox Map. + */ +internal inline fun MapView.createController( + activity: Context, + crossinline onInit: (MapController) -> Unit, + noinline onSelect: (CaptureId?) -> Unit, +) { + val map = getMapboxMap() + val styleUri = if (activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { + Style.DARK + } else { + Style.LIGHT + } + + map.loadStyleUri(styleUri) { style -> + val controller = MapboxMapController(this, map, style, activity.resources, onSelect = onSelect) + controller.initDefaults() + onInit(controller) + } +} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/mapbox/MapboxZoomLevels.kt b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxZoomLevels.kt similarity index 60% rename from android-application/src/main/java/com/inkapplications/ack/android/map/mapbox/MapboxZoomLevels.kt rename to mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxZoomLevels.kt index 7356d9a5..26855660 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/mapbox/MapboxZoomLevels.kt +++ b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxZoomLevels.kt @@ -1,11 +1,11 @@ -package com.inkapplications.ack.android.map.mapbox +package com.inkapplications.ack.android.mapbox -import com.inkapplications.ack.android.map.ZoomLevels +import com.inkapplications.ack.android.maps.ZoomLevels /** * Mapbox adaptation of zoom levels. */ -val ZoomLevels.mapboxValue: Double get() = when (this) { +internal val ZoomLevels.mapboxValue: Double get() = when (this) { ZoomLevels.MIN -> 0.0 ZoomLevels.CONTINENT -> 2.0 ZoomLevels.ISLANDS -> 4.0 diff --git a/mapbox/src/main/res/drawable-nodpi/default_marker.png b/mapbox/src/main/res/drawable-nodpi/default_marker.png new file mode 100644 index 00000000..fd82b6d9 Binary files /dev/null and b/mapbox/src/main/res/drawable-nodpi/default_marker.png differ diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts new file mode 100644 index 00000000..fe36fc0e --- /dev/null +++ b/maps/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("com.android.library") + kotlin("android") +} + +android { + namespace = "com.inkapplications.ack.android.maps" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + } + + buildFeatures { + compose = true + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } + compileOptions { + targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation(libs.androidx.annotation) + api(projects.aprsAndroid) + api(libs.androidx.compose.foundation) +} diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/DummyMapRenderer.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/DummyMapRenderer.kt new file mode 100644 index 00000000..f9b7403f --- /dev/null +++ b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/DummyMapRenderer.kt @@ -0,0 +1,19 @@ +package com.inkapplications.ack.android.maps + +import android.app.Application +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.inkapplications.ack.data.CaptureId + +object DummyMapRenderer: MapRenderer { + @Composable + override fun renderMarkerMap( + viewModel: MapViewModel, + onMapItemClicked: (CaptureId?) -> Unit, + bottomProtection: Dp, + interactive: Boolean, + modifier: Modifier + ) {} + override suspend fun initialize(application: Application) {} +} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapCameraPosition.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapCameraPosition.kt similarity index 92% rename from android-application/src/main/java/com/inkapplications/ack/android/map/MapCameraPosition.kt rename to maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapCameraPosition.kt index 5cf05258..1c7926e5 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapCameraPosition.kt +++ b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapCameraPosition.kt @@ -1,4 +1,4 @@ -package com.inkapplications.ack.android.map +package com.inkapplications.ack.android.maps import inkapplications.spondee.spatial.GeoCoordinates import inkapplications.spondee.spatial.latitude diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapController.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapController.kt similarity index 89% rename from android-application/src/main/java/com/inkapplications/ack/android/map/MapController.kt rename to maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapController.kt index 13b18f1a..abceb0b1 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapController.kt +++ b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapController.kt @@ -1,4 +1,4 @@ -package com.inkapplications.ack.android.map +package com.inkapplications.ack.android.maps import android.Manifest.permission import androidx.annotation.RequiresPermission @@ -45,5 +45,10 @@ interface MapController { * Disable the device's current location from being tracked and displayed. */ fun disablePositionTracking() + + /** + * Enable or disable panning the map view. + */ + fun setPanEnabled(boolean: Boolean) } diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapRenderer.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapRenderer.kt new file mode 100644 index 00000000..b041b870 --- /dev/null +++ b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapRenderer.kt @@ -0,0 +1,20 @@ +package com.inkapplications.ack.android.maps + +import android.app.Application +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.inkapplications.ack.data.CaptureId + +interface MapRenderer { + @Composable + fun renderMarkerMap( + viewModel: MapViewModel, + onMapItemClicked: (CaptureId?) -> Unit, + bottomProtection: Dp, + interactive: Boolean, + modifier: Modifier, + ) + + suspend fun initialize(application: Application) +} diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapViewModel.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapViewModel.kt new file mode 100644 index 00000000..59f15e24 --- /dev/null +++ b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapViewModel.kt @@ -0,0 +1,7 @@ +package com.inkapplications.ack.android.maps + +data class MapViewModel( + val cameraPosition: MapCameraPosition, + val markers: Collection, + val enablePositionTracking: Boolean = false, +) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewState.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MarkerViewState.kt similarity index 87% rename from android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewState.kt rename to maps/src/main/kotlin/com/inkapplications/ack/android/maps/MarkerViewState.kt index e6cbe70c..712949f1 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewState.kt +++ b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MarkerViewState.kt @@ -1,4 +1,4 @@ -package com.inkapplications.ack.android.map +package com.inkapplications.ack.android.maps import android.graphics.Bitmap import com.inkapplications.ack.data.CaptureId diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/ZoomLevels.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/ZoomLevels.kt similarity index 77% rename from android-application/src/main/java/com/inkapplications/ack/android/map/ZoomLevels.kt rename to maps/src/main/kotlin/com/inkapplications/ack/android/maps/ZoomLevels.kt index 1efcf7c3..d2e34c91 100644 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/ZoomLevels.kt +++ b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/ZoomLevels.kt @@ -1,4 +1,4 @@ -package com.inkapplications.ack.android.map +package com.inkapplications.ack.android.maps /** * Shorthand for common zoom levels. diff --git a/settings.gradle.kts b/settings.gradle.kts index 174bc84d..1f5faf7c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,3 +5,5 @@ rootProject.name = "ack-android" include("android-extensions") include("aprs-android") include("android-application") +include("maps") +include("mapbox")