Skip to content

Commit

Permalink
Initial integration of MapCompose map
Browse files Browse the repository at this point in the history
This code is the initial integration of the MapCompose open street map
UI. The soundscape service connection has been moved out into a separate
class and then a MapCompose based UI has been added to the Home screen.
The service now uses lastLocation to give a much quicker 'lock' on the
current position.

An HTTP cache has also been initiated - this likely affects all HTTP and not
just the Tiles for open street map. As a result it may be required to mark
Tile HTTP requests with a cache-control header if we don't want them to go
through the cache?

The API key for thunderforest is stored in local.properties as
tileProviderApiKey so must be configured for each developer i.e.

tileProviderApiKey=xxXXxxXXxxXXxxXXxxXXxxXXxxXXxxXX

with an appropriate key. For builds, there's a new GitHub secret
TILE_PROVIDER_API_KEY that needs setup.
  • Loading branch information
davecraig committed Aug 24, 2024
1 parent 4de5f9d commit 198fcb2
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 98 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/build-app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ jobs:
id: get-version
run: echo "VERSION=$(git describe --tags)" >> $GITHUB_OUTPUT

- name: Setup tile provider API key
env:
TILE_PROVIDER_API_KEY: ${{ secrets.TILE_PROVIDER_API_KEY }}
run: |
echo tileProviderApiKey=$TILE_PROVIDER_API_KEY > local.properties
- name: Decode Google services
env:
ENCODED_STRING: ${{ secrets.GOOGLE_SERVICES }}
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/nightly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ jobs:
with:
fetch-depth: 0

- name: Setup tile provider API key
env:
TILE_PROVIDER_API_KEY: ${{ secrets.TILE_PROVIDER_API_KEY }}
run: |
echo tileProviderApiKey=$TILE_PROVIDER_API_KEY > local.properties
- name: Decode Google services
env:
ENCODED_STRING: ${{ secrets.GOOGLE_SERVICES }}
Expand Down
14 changes: 14 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import java.io.FileInputStream
import java.util.Properties

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
Expand Down Expand Up @@ -33,8 +36,16 @@ android {
versionCode = 36
versionName = "0.0.35"

// Retrieve the tile provider API from local.properties. This is not under version control
// and must be configured by each developer locally. GitHb actions fill in local.properties
// from a secret.
val localProperties = Properties()
localProperties.load(FileInputStream(rootProject.file("local.properties")))
val tileProviderApiKey = localProperties["tileProviderApiKey"]

buildConfigField("String", "VERSION_NAME", "\"${versionName}\"")
buildConfigField("String", "FMOD_LIB", "\"fmod\"")
buildConfigField("String", "TILE_PROVIDER_API_KEY", "\"${tileProviderApiKey}\"")

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down Expand Up @@ -175,6 +186,9 @@ dependencies {
// GPX parser
implementation (libs.android.gpx.parser)

// Open Street Map compose library
implementation (libs.mapcompose)

// Screenshots for tests
//screenshotTestImplementation(libs.androidx.compose.ui.tooling)
}
121 changes: 30 additions & 91 deletions app/src/main/java/org/scottishtecharmy/soundscape/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package org.scottishtecharmy.soundscape

import android.Manifest
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.location.Location
import android.net.http.HttpResponseCache
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.setContent
Expand All @@ -18,57 +15,37 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import org.scottishtecharmy.soundscape.datastore.DataStoreManager
import org.scottishtecharmy.soundscape.datastore.DataStoreManager.PreferencesKeys.FIRST_LAUNCH

import org.scottishtecharmy.soundscape.services.SoundscapeService
import org.scottishtecharmy.soundscape.ui.theme.SoundscapeTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.scottishtecharmy.soundscape.screens.Home
import java.io.File
import java.io.IOException
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var dataStoreManager: DataStoreManager
@Inject
lateinit var soundscapeServiceConnection : SoundscapeServiceConnection

private var soundscapeService: SoundscapeService? = null
data class DeviceLocation(
var latitude : Double,
var longitude : Double,
var orientation : Double,
)

private var serviceBoundState by mutableStateOf(false)
private var displayableLocation by mutableStateOf<String?>(null)
private var displayableOrientation by mutableStateOf<String?>(null)
private var currentDeviceLocation by mutableStateOf<DeviceLocation?>(null)
private var displayableTileString by mutableStateOf<String?>(null)

private var location by mutableStateOf<Location?>(null)
//private var location by mutableStateOf<Location?>(null)
//private var tileXY by mutableStateOf<Pair<Int, Int>?>(null)

// needed to communicate with the service.
private val connection = object : ServiceConnection {

override fun onServiceConnected(className: ComponentName, service: IBinder) {

val binder = service as SoundscapeService.LocalBinder
soundscapeService = binder.getService()
serviceBoundState = true

onServiceConnected()
}

override fun onServiceDisconnected(arg0: ComponentName) {
// This is called when the connection with the service has been disconnected. Clean up.
Log.d(TAG, "onServiceDisconnected")

serviceBoundState = false
soundscapeService = null
}
}

// we need notification permission to be able to display a notification for the foreground service
private val notificationPermissionLauncher =
registerForActivityResult(
Expand Down Expand Up @@ -107,7 +84,10 @@ class MainActivity : AppCompatActivity() {
)
}

installSplashScreen()
Log.d(TAG, "isFirstLaunch: $isFirstLaunch")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installSplashScreen()
}

if(isFirstLaunch) {
// On the first launch, we want to take the user through the OnboardingActivity so
Expand All @@ -121,25 +101,26 @@ class MainActivity : AppCompatActivity() {
return
}

// Install HTTP cache this caches all of the UI tiles (at least?)
try {
val httpCacheDir = File(applicationContext.cacheDir, "http")
val httpCacheSize = (100 * 1024 * 1024).toLong() // 100 MiB
HttpResponseCache.install(httpCacheDir, httpCacheSize)
} catch (e: IOException) {
Log.i("Injection", "HTTP response cache installation failed:$e")
}

Log.d(TAG, "Do we ever get here to check permissions?")
checkAndRequestNotificationPermissions()
Log.d(TAG, "Do we try to bind to Soundscape service?")
soundscapeServiceConnection.tryToBindToServiceIfRunning()
setContent {
SoundscapeTheme {
Home()
}
}

checkAndRequestNotificationPermissions()

tryToBindToServiceIfRunning()
}

override fun onDestroy() {
super.onDestroy()

// If this was the first launch
if(serviceBoundState) {
unbindService(connection)
serviceBoundState = false
}
}

/**
Expand Down Expand Up @@ -189,7 +170,7 @@ class MainActivity : AppCompatActivity() {

fun stopServiceAndExit() {
// service is already running, stop it
soundscapeService?.stopForegroundService()
soundscapeServiceConnection.stopServiceAndExit()
// And exit application
finishAndRemoveTask()
}
Expand All @@ -202,49 +183,7 @@ class MainActivity : AppCompatActivity() {
private fun startSoundscapeService() {
// start the service
startForegroundService(Intent(this, SoundscapeService::class.java))

// bind to the service to update UI
tryToBindToServiceIfRunning()
}

private fun tryToBindToServiceIfRunning() {
Intent(this, SoundscapeService::class.java).also { intent ->
bindService(intent, connection, 0)
}
}

private fun onServiceConnected() {

lifecycleScope.launch {
// observe location updates from the service
soundscapeService?.locationFlow?.map {
it?.let { location ->
"Latitude: ${location.latitude}, Longitude: ${location.longitude} Accuracy: ${location.accuracy}"
}
}?.collectLatest {
displayableLocation = it
}
}

lifecycleScope.launch {
soundscapeService?.orientationFlow?.map {
it?.let {
orientation ->
"Device orientation: ${orientation.headingDegrees}"
}
}?.collect {
displayableOrientation = it
}
}

lifecycleScope.launch {
delay(10000)
val test = soundscapeService?.getTileGrid(application)

println("Number of tiles in grid: ${test?.size}")
}
}

companion object {
private const val TAG = "MainActivity"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.scottishtecharmy.soundscape

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log

import org.scottishtecharmy.soundscape.services.SoundscapeService
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SoundscapeServiceConnection @Inject constructor(@ApplicationContext context: Context) {

var soundscapeService: SoundscapeService? = null
private val appContext = context

private var _serviceBoundState = MutableStateFlow(false)
val serviceBoundState = _serviceBoundState.asStateFlow()

// needed to communicate with the service.
private val connection = object : ServiceConnection {

override fun onServiceConnected(className: ComponentName, service: IBinder) {
// we've bound to ExampleLocationForegroundService, cast the IBinder and get ExampleLocationForegroundService instance.
Log.d(TAG, "onServiceConnected")

val binder = service as SoundscapeService.LocalBinder
soundscapeService = binder.getService()
_serviceBoundState.value = true
}

override fun onServiceDisconnected(arg0: ComponentName) {
// This is called when the connection with the service has been disconnected. Clean up.
Log.d(TAG, "onServiceDisconnected")

_serviceBoundState.value = false
}
}

fun create() {
tryToBindToServiceIfRunning()
}

private fun destroy() {

// If this was the first launch
if(serviceBoundState.value) {
appContext.unbindService(connection)
_serviceBoundState.value = false
}
}

fun stopServiceAndExit() {
// service is already running, stop it
soundscapeService?.stopForegroundService()

destroy()
}

fun tryToBindToServiceIfRunning() {
if(!serviceBoundState.value) {
Intent(appContext, SoundscapeService::class.java).also { intent ->
appContext.bindService(intent, connection, 0)
}
}
}

companion object {
private const val TAG = "MainActivity"
}
}
16 changes: 13 additions & 3 deletions app/src/main/java/org/scottishtecharmy/soundscape/di/HiltModule.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package org.scottishtecharmy.soundscape.di

import android.content.Context

import org.scottishtecharmy.soundscape.datastore.DataStoreManager

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.scottishtecharmy.soundscape.SoundscapeServiceConnection
import org.scottishtecharmy.soundscape.audio.NativeAudioEngine
import org.scottishtecharmy.soundscape.datastore.DataStoreManager
import javax.inject.Singleton


Expand All @@ -34,3 +33,14 @@ class AppNativeAudioEngine {
return audioEngine
}
}

@Module
@InstallIn(SingletonComponent::class)
class AppSoundscapeServiceConnection {
@Provides
@Singleton
fun provideSoundscapeServiceConnection(@ApplicationContext context: Context): SoundscapeServiceConnection {
val serviceConnection = SoundscapeServiceConnection(context)
return serviceConnection
}
}
Loading

0 comments on commit 198fcb2

Please sign in to comment.