Skip to content

Commit 54d84c0

Browse files
committed
feat: Enhance location services UX and add city management feedback
This update significantly improves the handling of location permissions and GPS states, providing a more robust and user-friendly experience. Key enhancements include: - Implemented a dedicated UI flow for scenarios where GPS is disabled but permissions are granted, guiding users to enable location services. - Refined permission request lifecycle and state management in `WeatherScreen` and `MainViewModel` for greater stability. - Optimized location data fetching in `LocationTrackerImpl` by prioritizing current location over last known. - Introduced user feedback via Snackbars in `ManageCitiesScreen` for events like adding duplicate cities or reaching location limits, leveraging a `SharedFlow` in `MainViewModel`. - Addressed UI flickering and state synchronization issues related to GPS and permission changes. - Enabled `android:allowBackup="true"` in `AndroidManifest.xml` to support data restoration. - General code cleanup and import optimization related to these features.
1 parent f2a2d9a commit 54d84c0

File tree

13 files changed

+833
-431
lines changed

13 files changed

+833
-431
lines changed

.idea/deploymentTargetSelector.xml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ to provide "Feels Like" temperature predictions.
8484
<th>Settings Screen</th>
8585
</tr>
8686
<tr>
87-
<td><img src="screenshots/main_screen.jpg" width="350"/></td>
88-
<td><img src="screenshots/manage_locations.jpg" width="350"/></td>
89-
<td><img src="screenshots/settings_screen.jpg" width="350"/></td>
87+
<td><img src="screenshots/main_screen.jpg" width="350" alt="Main Screen"/></td>
88+
<td><img src="screenshots/manage_locations.jpg" width="350" alt="Manage Locations"/></td>
89+
<td><img src="screenshots/settings_screen.jpg" width="350" alt="Settings Screen"/></td>
9090
</tr>
9191
</table>
9292

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515

1616
<application
1717
android:name=".WeatherMLApplication"
18-
android:allowBackup="false"
19-
tools:replace="android:allowBackup"
18+
android:allowBackup="true"
2019
android:dataExtractionRules="@xml/data_extraction_rules"
2120
android:fullBackupContent="@xml/backup_rules"
2221
android:icon="@mipmap/ic_launcher"

app/src/main/java/com/artemzarubin/weatherml/data/location/LocationTrackerImpl.kt

Lines changed: 86 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,114 @@
11
package com.artemzarubin.weatherml.data.location
22

33
import android.Manifest
4-
import android.app.Application // Needs Application context
4+
import android.app.Application
55
import android.content.Context
66
import android.content.pm.PackageManager
77
import android.location.Location
8-
import android.location.LocationManager // To check if GPS is enabled
8+
import android.location.LocationManager
99
import android.util.Log
1010
import androidx.core.content.ContextCompat
1111
import com.artemzarubin.weatherml.domain.location.LocationTracker
12+
import com.artemzarubin.weatherml.util.Resource
13+
import com.google.android.gms.location.CurrentLocationRequest
1214
import com.google.android.gms.location.FusedLocationProviderClient
1315
import com.google.android.gms.location.Priority
1416
import com.google.android.gms.tasks.CancellationTokenSource
15-
import kotlinx.coroutines.suspendCancellableCoroutine
17+
import dagger.hilt.android.qualifiers.ApplicationContext
18+
import kotlinx.coroutines.channels.awaitClose
19+
import kotlinx.coroutines.flow.Flow
20+
import kotlinx.coroutines.flow.callbackFlow
1621
import javax.inject.Inject
17-
import kotlin.coroutines.resume
18-
19-
// import android.util.Log // For debugging
22+
import javax.inject.Singleton
2023

24+
@Singleton
2125
class LocationTrackerImpl @Inject constructor(
22-
private val application: Application, // Hilt can provide Application context
23-
private val fusedLocationClient: FusedLocationProviderClient // Hilt will provide this
26+
private val locationClient: FusedLocationProviderClient,
27+
@ApplicationContext private val application: Application
2428
) : LocationTracker {
2529

26-
override suspend fun getCurrentLocation(): Location? {
27-
// Check if location permissions are granted
28-
val hasAccessFineLocationPermission = ContextCompat.checkSelfPermission(
29-
application,
30-
Manifest.permission.ACCESS_FINE_LOCATION
31-
) == PackageManager.PERMISSION_GRANTED
30+
override fun getCurrentLocation(): Flow<Resource<Location?>> {
31+
return callbackFlow {
32+
Log.d("LocationTrackerImpl", "getCurrentLocation Flow started")
3233

33-
val hasAccessCoarseLocationPermission = ContextCompat.checkSelfPermission(
34-
application,
35-
Manifest.permission.ACCESS_COARSE_LOCATION
36-
) == PackageManager.PERMISSION_GRANTED
34+
if (ContextCompat.checkSelfPermission(
35+
application,
36+
Manifest.permission.ACCESS_FINE_LOCATION
37+
) != PackageManager.PERMISSION_GRANTED &&
38+
ContextCompat.checkSelfPermission(
39+
application,
40+
Manifest.permission.ACCESS_COARSE_LOCATION
41+
) != PackageManager.PERMISSION_GRANTED
42+
) {
43+
Log.w("LocationTrackerImpl", "Location permission not granted.")
44+
trySend(Resource.Error("Location permission not granted."))
45+
channel.close()
46+
return@callbackFlow
47+
}
3748

38-
// Check if GPS is enabled
39-
val locationManager =
40-
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
41-
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
49+
val locationManager =
50+
application.getSystemService(Context.LOCATION_SERVICE) as LocationManager
51+
val isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
52+
val isNetworkEnabled =
4253
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
4354

44-
if (!hasAccessFineLocationPermission && !hasAccessCoarseLocationPermission) {
45-
Log.d("LocationTracker", "Location permission not granted.")
46-
return null // Permissions not granted
47-
}
55+
if (!isGpsEnabled && !isNetworkEnabled) {
56+
Log.w("LocationTrackerImpl", "GPS and Network providers are disabled.")
57+
trySend(Resource.Error("GPS is disabled. Please enable location services."))
58+
channel.close()
59+
return@callbackFlow
60+
}
4861

49-
if (!isGpsEnabled) {
50-
Log.d("LocationTracker", "GPS or Network location is not enabled.")
51-
// TODO: Optionally, prompt user to enable location services
52-
return null // GPS not enabled
53-
}
62+
trySend(Resource.Loading(message = "Fetching current location..."))
63+
Log.d(
64+
"LocationTrackerImpl",
65+
"Requesting current location update using FusedLocationProviderClient.getCurrentLocation()."
66+
)
5467

55-
// Permissions are granted and GPS is enabled, try to get location
56-
// Using suspendCancellableCoroutine to bridge GMS Task API with coroutines
57-
return suspendCancellableCoroutine { continuation ->
5868
val cancellationTokenSource = CancellationTokenSource()
59-
fusedLocationClient.getCurrentLocation(
60-
Priority.PRIORITY_HIGH_ACCURACY, // Or PRIORITY_BALANCED_POWER_ACCURACY for coarse
61-
cancellationTokenSource.token
62-
).addOnSuccessListener { location: Location? ->
63-
// Got last known location. In some rare situations this can be null.
64-
// Log.d("LocationTracker", "Location success: $location")
65-
continuation.resume(location)
66-
}.addOnFailureListener { exception ->
67-
Log.e("LocationTracker", "Failed to get location: ${exception.message}")
68-
continuation.resume(null) // Resume with null on failure
69-
}.addOnCanceledListener {
70-
Log.d("LocationTracker", "Location request canceled.")
71-
continuation.cancel() // Cancel the coroutine if the task is canceled
72-
}
69+
// Создаем запрос на текущее местоположение
70+
val currentLocationRequest = CurrentLocationRequest.Builder()
71+
.setPriority(Priority.PRIORITY_HIGH_ACCURACY) // Вы можете выбрать другой приоритет
72+
// .setDurationMillis(10000) // Опционально: максимальное время ожидания
73+
.build()
74+
75+
locationClient.getCurrentLocation(currentLocationRequest, cancellationTokenSource.token)
76+
.addOnSuccessListener { location: Location? ->
77+
if (location != null) {
78+
Log.i("LocationTrackerImpl", "Successfully got CURRENT location: $location")
79+
trySend(Resource.Success(location))
80+
} else {
81+
// Эта ситуация маловероятна при успешном вызове, но возможна,
82+
// если геолокация отключается в момент запроса.
83+
Log.w(
84+
"LocationTrackerImpl",
85+
"FusedLocationProviderClient.getCurrentLocation() returned null despite success listener."
86+
)
87+
trySend(Resource.Error("Failed to get current location (null result). Try enabling high accuracy GPS."))
88+
}
89+
channel.close()
90+
}
91+
.addOnFailureListener { exception ->
92+
Log.e(
93+
"LocationTrackerImpl",
94+
"Failed to get current location using FusedLocationProviderClient.getCurrentLocation()",
95+
exception
96+
)
97+
trySend(Resource.Error("Failed to get current location: ${exception.message}"))
98+
channel.close()
99+
}
100+
.addOnCanceledListener { // Этот слушатель обычно не вызывается, если вы сами не отменяете CancellationToken
101+
Log.w(
102+
"LocationTrackerImpl",
103+
"Current location request was cancelled by FusedLocationProviderClient."
104+
)
105+
trySend(Resource.Error("Location request cancelled."))
106+
channel.close()
107+
}
73108

74-
// When the coroutine is cancelled, cancel the GMS Task
75-
continuation.invokeOnCancellation {
76-
cancellationTokenSource.cancel()
109+
awaitClose {
110+
Log.d("LocationTrackerImpl", "getCurrentLocation Flow closing. Cancelling token.")
111+
cancellationTokenSource.cancel() // Важно отменить токен при закрытии Flow
77112
}
78113
}
79114
}

app/src/main/java/com/artemzarubin/weatherml/data/remote/dto/CurrentWeatherResponseDto.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ data class CloudsDto(
6363
// DTO for the "coord" object
6464
@Serializable
6565
data class CoordinatesDto(
66-
@SerialName("lon") val longitude: Double?, // <--- МАЄ БУТИ NULLABLE
67-
@SerialName("lat") val latitude: Double?
66+
@SerialName("lon") val longitude: Double? = null, // <--- МАЄ БУТИ NULLABLE
67+
@SerialName("lat") val latitude: Double? = null
6868
)
6969

7070
// DTO for the "sys" object in /weather response

app/src/main/java/com/artemzarubin/weatherml/data/remote/dto/ForecastResponseDto.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ data class ForecastSysDto( // System information within a forecast item
5858
data class CityDto(
5959
@SerialName("id") val id: Int?,
6060
@SerialName("name") val name: String?,
61-
@SerialName("coord") val coordinates: CoordinatesDto?, // <--- МАЄ БУТИ NULLABLE
61+
@SerialName("coord") val coordinates: CoordinatesDto? = null, // <--- МАЄ БУТИ NULLABLE
6262
@SerialName("country") val country: String?,
6363
@SerialName("population") val population: Int?,
6464
@SerialName("timezone") val timezone: Int?,

app/src/main/java/com/artemzarubin/weatherml/di/NetworkModule.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,11 @@ object NetworkModule {
112112
application: Application,
113113
fusedLocationProviderClient: FusedLocationProviderClient
114114
): LocationTracker {
115-
// Hilt will provide Application and FusedLocationProviderClient
116-
return LocationTrackerImpl(application, fusedLocationProviderClient)
115+
116+
return LocationTrackerImpl(
117+
locationClient = fusedLocationProviderClient,
118+
application = application
119+
)
117120
}
118121

119122
@Provides
@@ -136,7 +139,7 @@ object NetworkModule {
136139

137140
@Module
138141
@InstallIn(SingletonComponent::class)
139-
abstract class ConnectivityModule { // Назва може бути іншою, наприклад, AppModule, якщо він вже є
142+
abstract class ConnectivityModule {
140143

141144
@Binds
142145
@Singleton
Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
package com.artemzarubin.weatherml.domain.location
22

33
import android.location.Location
4+
import com.artemzarubin.weatherml.util.Resource
5+
import kotlinx.coroutines.flow.Flow
46

57
interface LocationTracker {
6-
/**
7-
* Retrieves the current device location.
8-
* This function should handle cases where location services are off or permission is denied,
9-
* though permission checks should ideally happen before calling this.
10-
*
11-
* @return The current [Location] object if successful, or null if location cannot be obtained.
12-
* Consider returning a Resource<Location> for better error handling.
13-
*/
14-
suspend fun getCurrentLocation(): Location? // Or Resource<Location>
8+
fun getCurrentLocation(): Flow<Resource<Location?>>
159
}

app/src/main/java/com/artemzarubin/weatherml/domain/repository/WeatherRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@file:Suppress("KDocUnresolvedReference", "KDocUnresolvedReference", "KDocUnresolvedReference")
1+
@file:Suppress("KDocUnresolvedReference", "KDocUnresolvedReference")
22

33
package com.artemzarubin.weatherml.domain.repository
44

0 commit comments

Comments
 (0)