Skip to content

Commit

Permalink
Add distance to coffee shops
Browse files Browse the repository at this point in the history
  • Loading branch information
StrixG committed Dec 25, 2023
1 parent f955b45 commit fb8e5c9
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 11 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ dependencies {

implementation(libs.bundles.retrofit)
implementation(libs.yandex.mapkit)
implementation(libs.play.services.location)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.obrekht.coffeeshops.app.utils

import android.content.Context
import com.obrekht.coffeeshops.R
import java.text.NumberFormat

fun Long.formatDistance(context: Context): String {
val value = when {
this > 1000 -> this / 1000.0
else -> this
}
val unit = when {
this > 1000 -> context.getString(R.string.unit_short_kilometers)
else -> context.getString(R.string.unit_short_meters)
}
return NumberFormat.getNumberInstance().apply {
maximumFractionDigits = 1
}.format(value) + " $unit"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.obrekht.coffeeshops.app.utils

import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat

fun Context.hasLocationPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this,
ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(
this,
ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,21 @@ import com.obrekht.coffeeshops.coffeeshops.data.model.toEntity
import com.obrekht.coffeeshops.coffeeshops.data.remote.CoffeeShopsApiService
import com.obrekht.coffeeshops.coffeeshops.ui.model.CoffeeShop
import com.obrekht.coffeeshops.core.data.model.EmptyBodyException
import com.obrekht.coffeeshops.geolocation.data.repository.GeoLocationRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
import retrofit2.HttpException
import javax.inject.Inject

class DefaultCoffeeShopsRepository @Inject constructor(
private val coffeeShopsDao: CoffeeShopsDao,
private val coffeeShopsApiService: CoffeeShopsApiService
private val coffeeShopsApiService: CoffeeShopsApiService,
private val geoLocationRepository: GeoLocationRepository
) : CoffeeShopsRepository {

override suspend fun refreshAll() {
geoLocationRepository.refreshCurrentLocation()

val response = coffeeShopsApiService.getAll()
if (!response.isSuccessful) {
throw HttpException(response)
Expand All @@ -26,16 +30,23 @@ class DefaultCoffeeShopsRepository @Inject constructor(
}

override fun getCoffeeShopsStream(): Flow<List<CoffeeShop>> {
return coffeeShopsDao.observeAll().map { coffeeShopList ->
coffeeShopList.map {
CoffeeShop(it.id, it.name, it.point, null)
return coffeeShopsDao.observeAll()
.combine(geoLocationRepository.currentLocation) { coffeeShopList, _ ->
coffeeShopList.map {
val distance = geoLocationRepository.getDistanceToLocation(
it.point.latitude, it.point.longitude
)
CoffeeShop(it.id, it.name, it.point, distance?.toLong())
}
}
}
}

override suspend fun getCoffeeShopById(id: Long): CoffeeShop? {
return coffeeShopsDao.getById(id)?.run {
CoffeeShop(id, name, point, point.latitude.toLong())
val distance = geoLocationRepository.getDistanceToLocation(
point.latitude, point.longitude
)
CoffeeShop(id, name, point, distance?.toLong())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.obrekht.coffeeshops.R
import com.obrekht.coffeeshops.app.utils.formatDistance
import com.obrekht.coffeeshops.coffeeshops.ui.model.CoffeeShop
import com.obrekht.coffeeshops.databinding.ItemCoffeeShopBinding

Expand Down Expand Up @@ -48,7 +49,8 @@ class CoffeeShopViewHolder(
with(binding) {
name.text = coffeeShop.name
distance.text = coffeeShop.distance?.let {
itemView.context.getString(R.string.distance_to_me_meters, it)
val distanceString = it.formatDistance(itemView.context)
itemView.context.getString(R.string.distance_to_me, distanceString)
} ?: itemView.context.getString(R.string.unknown_distance)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.obrekht.coffeeshops.coffeeshops.ui.nearby

import android.Manifest
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
Expand All @@ -14,6 +16,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import com.obrekht.coffeeshops.R
import com.obrekht.coffeeshops.app.utils.hasLocationPermission
import com.obrekht.coffeeshops.app.utils.setOnApplyWindowInsetsListener
import com.obrekht.coffeeshops.core.ui.model.SnackbarAction
import com.obrekht.coffeeshops.databinding.FragmentNearbyCoffeeShopsBinding
Expand All @@ -30,6 +33,16 @@ class NearbyCoffeeShopsFragment : Fragment(R.layout.fragment_nearby_coffee_shops

private var adapter: NearbyCoffeeShopsAdapter? = null

private val locationPermissionRequest = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
if (permissions.any { it.value }) {
viewModel.refreshCurrentLocation()
} else {
showErrorSnackbar(R.string.error_location_permission_denied)
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentNearbyCoffeeShopsBinding.bind(view)

Expand All @@ -43,6 +56,25 @@ class NearbyCoffeeShopsFragment : Fragment(R.layout.fragment_nearby_coffee_shops
windowInsets
}

when {
requireContext().hasLocationPermission() -> {
viewModel.refreshCurrentLocation()
}

requireActivity().shouldShowRequestPermissionRationale(
Manifest.permission.ACCESS_COARSE_LOCATION
) -> {
showErrorSnackbar(
R.string.error_location_permission_rationale,
SnackbarAction(getString(R.string.request)) {
requestLocationPermission()
}
)
}

else -> requestLocationPermission()
}

adapter = NearbyCoffeeShopsAdapter { coffeeShop, _ ->
navigateToMap(coffeeShop.id)
}
Expand Down Expand Up @@ -104,6 +136,15 @@ class NearbyCoffeeShopsFragment : Fragment(R.layout.fragment_nearby_coffee_shops
findNavController().navigate(action)
}

private fun requestLocationPermission() {
locationPermissionRequest.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}

private fun showErrorSnackbar(
@StringRes messageResId: Int,
snackbarAction: SnackbarAction? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import com.obrekht.coffeeshops.auth.data.repository.AuthRepository
import com.obrekht.coffeeshops.coffeeshops.data.repository.CoffeeShopsRepository
import com.obrekht.coffeeshops.coffeeshops.ui.model.CoffeeShop
import com.obrekht.coffeeshops.geolocation.data.repository.GeoLocationRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -18,7 +19,8 @@ import javax.inject.Inject
@HiltViewModel
class NearbyCoffeeShopsViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val coffeeShopsRepository: CoffeeShopsRepository
private val geoLocationRepository: GeoLocationRepository,
private val coffeeShopsRepository: CoffeeShopsRepository,
) : ViewModel() {

private val _uiState = MutableStateFlow(UiState())
Expand Down Expand Up @@ -56,6 +58,10 @@ class NearbyCoffeeShopsViewModel @Inject constructor(
}
}

fun refreshCurrentLocation() {
geoLocationRepository.refreshCurrentLocation()
}

private fun setLoadingState(isLoading: Boolean) {
_uiState.update {
it.copy(isLoading = isLoading)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.obrekht.coffeeshops.geolocation.data.di

import com.obrekht.coffeeshops.geolocation.data.repository.GeoLocationRepository
import com.obrekht.coffeeshops.geolocation.data.repository.PlayServicesGeoLocationRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton


@Module
@InstallIn(SingletonComponent::class)
abstract class GeoLocationRepositoryModule {

@Singleton
@Binds
abstract fun bindGeoLocationRepository(
repository: PlayServicesGeoLocationRepository
): GeoLocationRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.obrekht.coffeeshops.geolocation.data.repository

import android.location.Location
import kotlinx.coroutines.flow.StateFlow

interface GeoLocationRepository {

val currentLocation: StateFlow<Location>

fun refreshCurrentLocation(withLastLocation: Boolean = false)
fun getDistanceToLocation(latitude: Double, longitude: Double): Float?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.obrekht.coffeeshops.geolocation.data.repository

import android.annotation.SuppressLint
import android.content.Context
import android.location.Location
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.google.android.gms.tasks.CancellationTokenSource
import com.obrekht.coffeeshops.app.utils.hasLocationPermission
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject

class PlayServicesGeoLocationRepository @Inject constructor(
@ApplicationContext private val context: Context
): GeoLocationRepository {

private val _currentLocation = MutableStateFlow(Location(null))
override val currentLocation = _currentLocation.asStateFlow()

private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)

init {
refreshCurrentLocation(true)
}

@SuppressLint("MissingPermission")
override fun refreshCurrentLocation(withLastLocation: Boolean) {
if (!context.hasLocationPermission()) return

if (withLastLocation) {
fusedLocationClient.lastLocation.addOnSuccessListener { location: Location? ->
location?.let {
_currentLocation.value = it
}
}
}

fusedLocationClient.getCurrentLocation(
Priority.PRIORITY_HIGH_ACCURACY,
CancellationTokenSource().token
).addOnSuccessListener { location: Location? ->
location?.let {
_currentLocation.value = it
}
}
}

override fun getDistanceToLocation(latitude: Double, longitude: Double): Float? {
val location = _currentLocation.value
if (location.provider == null) {
return null
}

val results = FloatArray(3)
Location.distanceBetween(
location.latitude, location.longitude,
latitude, longitude,
results
)
return results[0]
}
}
7 changes: 6 additions & 1 deletion app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@
<string name="error_unknown">Неизвестная ошибка.</string>
<string name="error_loading_coffee_shops">Не удалось загрузить кофейни.</string>
<string name="error_loading_menu">Не удалось загрузить меню.</string>
<string name="distance_to_me_meters">%1$d м от вас</string>
<string name="distance_to_me">%1$s от вас</string>
<string name="snackbar_retry">Повторить</string>
<string name="proceed_to_payment">Перейти к оплате</string>
<string name="label_menu">Меню</string>
<string name="pay">Оплатить (%1$d ₽)</string>
<string name="label_cart">Ваш заказ</string>
<string name="empty_cart">Ваша корзина пуста.</string>
<string name="unknown_distance">Расстояние неизвестно</string>
<string name="error_location_permission_denied">Location permission denied.</string>
<string name="request">Запросить</string>
<string name="unit_short_meters">м</string>
<string name="unit_short_kilometers">км</string>
<string name="error_location_permission_rationale">Пожалуйста, разрешите доступ к вашему местоположению.</string>
</resources>
7 changes: 6 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<string name="error_unknown">Unknown error.</string>
<string name="error_loading_coffee_shops">Failed to load coffee shops.</string>
<string name="error_loading_menu">Failed to load menu.</string>
<string name="distance_to_me_meters">%1$d m from you</string>
<string name="distance_to_me">%1$s from you</string>
<string name="snackbar_retry">Retry</string>
<string name="proceed_to_payment">Proceed to payment</string>
<string name="price" translatable="false">%1$d ₽</string>
Expand All @@ -28,4 +28,9 @@
<string name="label_cart">Your order</string>
<string name="empty_cart">Your cart is empty.</string>
<string name="unknown_distance">Distance is unknown</string>
<string name="error_location_permission_denied">Location permission denied.</string>
<string name="error_location_permission_rationale">Please, allow access to your location.</string>
<string name="request">Request</string>
<string name="unit_short_meters">m</string>
<string name="unit_short_kilometers">km</string>
</resources>
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ room = "2.6.1"
okhttp-bom = "4.12.0"
retrofit = "2.10.0-SNAPSHOT"
yandex-mapkit = "4.5.0-lite"
play-services = "21.0.1"
# Tests
junit = "4.13.2"
androidx-test-ext-junit = "1.1.5"
Expand Down Expand Up @@ -44,6 +45,7 @@ okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-intercepto
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-kotlinx-serialization = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" }
yandex-mapkit = { module = "com.yandex.android:maps.mobile", version.ref = "yandex-mapkit" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "play-services" }
# Tests
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
Expand Down

0 comments on commit fb8e5c9

Please sign in to comment.