diff --git a/README.md b/README.md index 48db414..a852011 100755 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This is a Kotlin Multiplatform library that provides geolocation to common code. ## Features - **Geolocation tracking** - track user geolocation from common code; +- **Compose Multiplatform** support; ## Requirements - Gradle version 6.0+ @@ -35,7 +36,10 @@ allprojects { project build.gradle ```groovy dependencies { - commonMainApi("dev.icerock.moko:geo:0.5.0") + commonMainApi("dev.icerock.moko:geo:0.6.0") + + // Compose Multiplatform + commonMainApi("dev.icerock.moko:geo-compose:0.6.0") } ``` diff --git a/build.gradle.kts b/build.gradle.kts index cc539b0..e54e2fa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,8 @@ buildscript { classpath(libs.mokoGradlePlugin) classpath(libs.mobileMultiplatformGradlePlugin) classpath(libs.kotlinSerializationGradlePlugin) + classpath(libs.composeJetBrainsGradlePlugin) + classpath(libs.detektGradlePlugin) } } diff --git a/geo-compose/build.gradle.kts b/geo-compose/build.gradle.kts new file mode 100644 index 0000000..5422fa1 --- /dev/null +++ b/geo-compose/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("dev.icerock.moko.gradle.multiplatform.mobile") + id("dev.icerock.moko.gradle.publication") + id("dev.icerock.moko.gradle.stub.javadoc") + id("dev.icerock.moko.gradle.detekt") + id("org.jetbrains.compose") +} + +android { + namespace = "dev.icerock.moko.geo.compose" + + defaultConfig { + minSdk = 21 + } +} + +dependencies { + commonMainApi(projects.geo) + commonMainApi(compose.runtime) + + androidMainImplementation(libs.appCompat) + androidMainImplementation(compose.foundation) +} diff --git a/geo-compose/src/androidMain/kotlin/dev/icerock/moko/geo/compose/BindLocationTrackerEffect.android.kt b/geo-compose/src/androidMain/kotlin/dev/icerock/moko/geo/compose/BindLocationTrackerEffect.android.kt new file mode 100644 index 0000000..5ff264a --- /dev/null +++ b/geo-compose/src/androidMain/kotlin/dev/icerock/moko/geo/compose/BindLocationTrackerEffect.android.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.geo.compose + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleOwner +import dev.icerock.moko.geo.LocationTracker + +@Suppress("FunctionNaming") +@Composable +actual fun BindLocationTrackerEffect(locationTracker: LocationTracker) { + val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current + val context: Context = LocalContext.current + + LaunchedEffect(locationTracker, lifecycleOwner, context) { + val fragmentManager: FragmentManager = (context as FragmentActivity).supportFragmentManager + + locationTracker.bind(lifecycleOwner.lifecycle, context, fragmentManager) + } +} diff --git a/geo-compose/src/androidMain/kotlin/dev/icerock/moko/geo/compose/LocationTrackerFactory.android.kt b/geo-compose/src/androidMain/kotlin/dev/icerock/moko/geo/compose/LocationTrackerFactory.android.kt new file mode 100644 index 0000000..9451cff --- /dev/null +++ b/geo-compose/src/androidMain/kotlin/dev/icerock/moko/geo/compose/LocationTrackerFactory.android.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.geo.compose + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.google.android.gms.location.LocationRequest +import dev.icerock.moko.geo.LocationTracker +import dev.icerock.moko.permissions.PermissionsController + +@Composable +actual fun rememberLocationTrackerFactory(accuracy: LocationTrackerAccuracy): LocationTrackerFactory { + val context: Context = LocalContext.current + return remember(context) { + object : LocationTrackerFactory { + override fun createLocationTracker(): LocationTracker { + return LocationTracker( + permissionsController = PermissionsController( + applicationContext = context.applicationContext + ), + priority = accuracy.toPriority() + ) + } + + override fun createLocationTracker( + permissionsController: PermissionsController + ): LocationTracker { + return LocationTracker( + permissionsController = permissionsController, + priority = accuracy.toPriority() + ) + } + } + } +} + +private fun LocationTrackerAccuracy.toPriority(): Int { + return when (this) { + LocationTrackerAccuracy.Best -> LocationRequest.PRIORITY_HIGH_ACCURACY + LocationTrackerAccuracy.Medium -> LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY + LocationTrackerAccuracy.LowPower -> LocationRequest.PRIORITY_LOW_POWER + } +} diff --git a/geo-compose/src/commonMain/kotlin/dev/icerock/moko/geo/compose/BindLocationTrackerEffect.kt b/geo-compose/src/commonMain/kotlin/dev/icerock/moko/geo/compose/BindLocationTrackerEffect.kt new file mode 100644 index 0000000..0335683 --- /dev/null +++ b/geo-compose/src/commonMain/kotlin/dev/icerock/moko/geo/compose/BindLocationTrackerEffect.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.geo.compose + +import androidx.compose.runtime.Composable +import dev.icerock.moko.geo.LocationTracker + +@Suppress("FunctionNaming") +@Composable +expect fun BindLocationTrackerEffect(locationTracker: LocationTracker) diff --git a/geo-compose/src/commonMain/kotlin/dev/icerock/moko/geo/compose/LocationTrackerFactory.kt b/geo-compose/src/commonMain/kotlin/dev/icerock/moko/geo/compose/LocationTrackerFactory.kt new file mode 100644 index 0000000..31c3488 --- /dev/null +++ b/geo-compose/src/commonMain/kotlin/dev/icerock/moko/geo/compose/LocationTrackerFactory.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.geo.compose + +import androidx.compose.runtime.Composable +import dev.icerock.moko.geo.LocationTracker +import dev.icerock.moko.permissions.PermissionsController + +interface LocationTrackerFactory { + fun createLocationTracker(): LocationTracker + fun createLocationTracker(permissionsController: PermissionsController): LocationTracker +} + +enum class LocationTrackerAccuracy { + Best, + Medium, + LowPower +} + +@Composable +expect fun rememberLocationTrackerFactory(accuracy: LocationTrackerAccuracy): LocationTrackerFactory diff --git a/geo-compose/src/iosMain/kotlin/dev/icerock/moko/geo/compose/BindLocationTrackerEffect.ios.kt b/geo-compose/src/iosMain/kotlin/dev/icerock/moko/geo/compose/BindLocationTrackerEffect.ios.kt new file mode 100644 index 0000000..7062c66 --- /dev/null +++ b/geo-compose/src/iosMain/kotlin/dev/icerock/moko/geo/compose/BindLocationTrackerEffect.ios.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.geo.compose + +import androidx.compose.runtime.Composable +import dev.icerock.moko.geo.LocationTracker + +// on iOS side we should not do anything to prepare LocationTracker to work +@Suppress("FunctionNaming") +@Composable +actual fun BindLocationTrackerEffect(locationTracker: LocationTracker) = Unit diff --git a/geo-compose/src/iosMain/kotlin/dev/icerock/moko/geo/compose/LocationTrackerFactory.ios.kt b/geo-compose/src/iosMain/kotlin/dev/icerock/moko/geo/compose/LocationTrackerFactory.ios.kt new file mode 100644 index 0000000..97abd22 --- /dev/null +++ b/geo-compose/src/iosMain/kotlin/dev/icerock/moko/geo/compose/LocationTrackerFactory.ios.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.geo.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import dev.icerock.moko.geo.LocationTracker +import dev.icerock.moko.permissions.PermissionsController +import platform.CoreLocation.CLLocationAccuracy +import platform.CoreLocation.kCLLocationAccuracyBest +import platform.CoreLocation.kCLLocationAccuracyKilometer +import platform.CoreLocation.kCLLocationAccuracyReduced + +@Composable +actual fun rememberLocationTrackerFactory(accuracy: LocationTrackerAccuracy): LocationTrackerFactory { + return remember { + object : LocationTrackerFactory { + override fun createLocationTracker(): LocationTracker { + return LocationTracker( + permissionsController = dev.icerock.moko.permissions.ios.PermissionsController(), + accuracy = accuracy.toIosAccuracy() + ) + } + + override fun createLocationTracker( + permissionsController: PermissionsController + ): LocationTracker { + return LocationTracker( + permissionsController = permissionsController, + accuracy = accuracy.toIosAccuracy() + ) + } + } + } +} + +private fun LocationTrackerAccuracy.toIosAccuracy(): CLLocationAccuracy { + return when (this) { + LocationTrackerAccuracy.Best -> kCLLocationAccuracyBest + LocationTrackerAccuracy.Medium -> kCLLocationAccuracyKilometer + LocationTrackerAccuracy.LowPower -> kCLLocationAccuracyReduced + } +} diff --git a/geo/src/androidMain/kotlin/dev/icerock/moko/geo/LocationTracker.kt b/geo/src/androidMain/kotlin/dev/icerock/moko/geo/LocationTracker.kt index a0dd622..6f3d9ac 100644 --- a/geo/src/androidMain/kotlin/dev/icerock/moko/geo/LocationTracker.kt +++ b/geo/src/androidMain/kotlin/dev/icerock/moko/geo/LocationTracker.kt @@ -15,6 +15,7 @@ import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationCallback import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices import dev.icerock.moko.permissions.Permission import dev.icerock.moko.permissions.PermissionsController import kotlinx.coroutines.CoroutineScope @@ -44,7 +45,7 @@ actual class LocationTracker( fun bind(lifecycle: Lifecycle, context: Context, fragmentManager: FragmentManager) { permissionsController.bind(lifecycle, fragmentManager) - locationProviderClient = FusedLocationProviderClient(context) + locationProviderClient = LocationServices.getFusedLocationProviderClient(context) @SuppressLint("MissingPermission") if (isStarted) { @@ -63,7 +64,7 @@ actual class LocationTracker( override fun onLocationResult(locationResult: LocationResult) { super.onLocationResult(locationResult) - val lastLocation = locationResult.lastLocation + val lastLocation = locationResult.lastLocation ?: return val speedAccuracy = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) null else lastLocation.speedAccuracyMetersPerSecond.toDouble() @@ -104,7 +105,8 @@ actual class LocationTracker( azimuth = azimuth, speed = speed, altitude = altitude, - timestampMs = lastLocation.time + timestampMs = lastLocation.time, + isMock = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) lastLocation.isMock else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) lastLocation.isFromMockProvider else null ) trackerScope.launch { diff --git a/geo/src/commonMain/kotlin/dev/icerock/moko/geo/ExtendedLocation.kt b/geo/src/commonMain/kotlin/dev/icerock/moko/geo/ExtendedLocation.kt index b581c08..7f06676 100644 --- a/geo/src/commonMain/kotlin/dev/icerock/moko/geo/ExtendedLocation.kt +++ b/geo/src/commonMain/kotlin/dev/icerock/moko/geo/ExtendedLocation.kt @@ -13,5 +13,6 @@ data class ExtendedLocation( val azimuth: Azimuth, val speed: Speed, val altitude: Altitude, - val timestampMs: Long + val timestampMs: Long, + val isMock:Boolean? = null ) : Parcelable diff --git a/geo/src/iosMain/kotlin/dev/icerock/moko/geo/LocationTracker.kt b/geo/src/iosMain/kotlin/dev/icerock/moko/geo/LocationTracker.kt index a943764..6a5ea6f 100644 --- a/geo/src/iosMain/kotlin/dev/icerock/moko/geo/LocationTracker.kt +++ b/geo/src/iosMain/kotlin/dev/icerock/moko/geo/LocationTracker.kt @@ -7,6 +7,7 @@ package dev.icerock.moko.geo import dev.icerock.moko.permissions.Permission import dev.icerock.moko.permissions.PermissionsController import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -23,7 +24,7 @@ actual class LocationTracker( ) { private val locationsChannel = Channel(Channel.BUFFERED) private val extendedLocationsChannel = Channel(Channel.BUFFERED) - private val trackerScope = CoroutineScope(UIDispatcher()) + private val trackerScope = CoroutineScope(Dispatchers.Main) private val tracker = Tracker( locationsChannel = locationsChannel, extendedLocationsChannel = extendedLocationsChannel, diff --git a/geo/src/iosMain/kotlin/dev/icerock/moko/geo/UIDispatcher.kt b/geo/src/iosMain/kotlin/dev/icerock/moko/geo/UIDispatcher.kt deleted file mode 100644 index 6ab2179..0000000 --- a/geo/src/iosMain/kotlin/dev/icerock/moko/geo/UIDispatcher.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.geo - -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Delay -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.Runnable -import platform.darwin.DISPATCH_TIME_NOW -import platform.darwin.NSEC_PER_MSEC -import platform.darwin.dispatch_after -import platform.darwin.dispatch_async -import platform.darwin.dispatch_get_main_queue -import platform.darwin.dispatch_time -import kotlin.coroutines.CoroutineContext - -// temporary solution for channel -@UseExperimental(InternalCoroutinesApi::class) -internal class UIDispatcher : CoroutineDispatcher(), Delay { - private val mQueue = dispatch_get_main_queue() - - override fun dispatch(context: CoroutineContext, block: Runnable) { - dispatch_async(mQueue) { - block.run() - } - } - - override fun scheduleResumeAfterDelay( - timeMillis: Long, - continuation: CancellableContinuation - ) { - dispatch_after( - `when` = dispatch_time( - DISPATCH_TIME_NOW, - timeMillis * NSEC_PER_MSEC.toLong() - ), - queue = mQueue - ) { - val result = continuation.tryResume(Unit) - if (result != null) { - continuation.completeResume(result) - } - } - } - - override fun invokeOnTimeout( - timeMillis: Long, - block: Runnable, - context: CoroutineContext - ): DisposableHandle { - var disposed = false - dispatch_after( - `when` = dispatch_time( - DISPATCH_TIME_NOW, - timeMillis * NSEC_PER_MSEC.toLong() - ), - queue = mQueue - ) { - if (disposed) return@dispatch_after - - block.run() - } - return object : DisposableHandle { - override fun dispose() { - disposed = true - } - } - } -} diff --git a/gradle.properties b/gradle.properties index 438110d..92088cf 100755 --- a/gradle.properties +++ b/gradle.properties @@ -3,14 +3,12 @@ org.gradle.configureondemand=false org.gradle.parallel=true kotlin.code.style=official -kotlin.native.enableDependencyPropagation=false -kotlin.mpp.enableGranularSourceSetsMetadata=true -kotlin.mpp.enableCompatibilityMetadataVariant=true +kotlin.mpp.androidSourceSetLayoutVersion=2 android.useAndroidX=true -moko.android.targetSdk=31 -moko.android.compileSdk=31 +moko.android.targetSdk=33 +moko.android.compileSdk=33 moko.android.minSdk=16 moko.publish.name=MOKO geo diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1166b21..c686586 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,15 @@ [versions] -kotlinVersion = "1.6.10" -androidAppCompatVersion = "1.2.0" -androidLifecycleVersion = "2.1.0" -playServicesLocationVersion = "18.0.0" -coroutinesVersion = "1.6.0-native-mt" +kotlinVersion = "1.8.10" +androidAppCompatVersion = "1.6.1" +androidLifecycleVersion = "2.2.0" +playServicesLocationVersion = "21.0.1" +coroutinesVersion = "1.6.4" +composeJetBrainsVersion = "1.3.1" mokoParcelizeVersion = "0.8.0" -mokoPermissionsVersion = "0.11.0" -mokoMvvmVersion = "0.12.0" -mokoGeoVersion = "0.5.0" -mokoResourcesVersion = "0.18.0" +mokoPermissionsVersion = "0.15.0" +mokoMvvmVersion = "0.16.0" +mokoGeoVersion = "0.6.0" +mokoResourcesVersion = "0.21.1" [libraries] appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } @@ -24,9 +25,11 @@ mokoResources = { module = "dev.icerock.moko:resources", version.ref = "mokoReso kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinVersion" } -androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.0.4" } -googleServicesGradlePlugin = { module = "com.google.gms:google-services", version = "4.3.8" } -firebaseGradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.2.0" } -mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.1.0" } -mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", version = "0.14.1" } +androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.4.2" } +googleServicesGradlePlugin = { module = "com.google.gms:google-services", version = "4.3.15" } +firebaseGradlePlugin = { module = "com.google.firebase:firebase-crashlytics-gradle", version = "2.9.4" } +mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.2.0" } +mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", version = "0.14.2" } kotlinSerializationGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlinVersion" } +composeJetBrainsGradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeJetBrainsVersion" } +detektGradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version = "1.22.0" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 00e33ed..774fae8 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TrackerViewModel.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TrackerViewModel.kt index 39f136e..ac4ac28 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TrackerViewModel.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TrackerViewModel.kt @@ -41,6 +41,8 @@ class TrackerViewModel( ${it.speed} + ${it.isMock} + timestamp=${it.timestampMs} """.trimIndent() } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9a3c65b..35b9bd0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,5 +13,6 @@ dependencyResolutionManagement { } include(":geo") +include(":geo-compose") include(":sample:android-app") include(":sample:mpp-library")