mirror of
https://github.com/hmalik144/Weather-apps.git
synced 2025-12-10 02:05:20 +00:00
- change location retrieval accuracy
- change location retrieval caching from location provider
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -89,6 +89,7 @@ gen-external-apklibs
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/gradle.xml
|
||||
.idea/jarRepositorie
|
||||
.idea/assetWizardSettings.xml
|
||||
|
||||
# Gem/fastlane
|
||||
Gemfile.lock
|
||||
|
||||
@@ -3,26 +3,20 @@ package com.appttude.h_mal.atlas_weather.testSuite
|
||||
import android.os.Build
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.room.Room
|
||||
import androidx.room.util.UUIDUtil
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
|
||||
import com.appttude.h_mal.atlas_weather.data.room.Converter
|
||||
import com.appttude.h_mal.atlas_weather.data.room.WeatherDao
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
import com.appttude.h_mal.atlas_weather.test.BuildConfig
|
||||
import com.appttude.h_mal.atlas_weather.utils.getOrAwaitValue
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.Assert.*
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.Mockito.mock
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
@@ -98,16 +92,16 @@ class RoomDatabaseTests {
|
||||
assertNull(dao.getCurrentFullWeatherSingle(id))
|
||||
}
|
||||
|
||||
private fun createEntity(id: String = CURRENT_LOCATION): EntityItem {
|
||||
private fun createEntity(id: String = CURRENT_LOCATION): WeatherEntity {
|
||||
val weather = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
FullWeather()
|
||||
} else {
|
||||
mockk<FullWeather>()
|
||||
}
|
||||
return EntityItem(id, weather)
|
||||
return WeatherEntity(id, weather)
|
||||
}
|
||||
|
||||
private fun createEntityList(size: Int = 4): List<EntityItem> {
|
||||
private fun createEntityList(size: Int = 4): List<WeatherEntity> {
|
||||
return (0.. size).map {
|
||||
val id = UUID.randomUUID().toString()
|
||||
createEntity(id)
|
||||
|
||||
@@ -4,8 +4,15 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.appttude.h_mal.atlas_weather.model.ViewState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.internal.wait
|
||||
import java.util.concurrent.CancellationException
|
||||
|
||||
open class BaseViewModel: ViewModel() {
|
||||
open class BaseViewModel : ViewModel() {
|
||||
|
||||
private val _uiState = MutableLiveData<ViewState>()
|
||||
val uiState: LiveData<ViewState> = _uiState
|
||||
@@ -22,4 +29,15 @@ open class BaseViewModel: ViewModel() {
|
||||
protected fun <E : Any> onError(error: E) {
|
||||
_uiState.postValue(ViewState.HasError(error))
|
||||
}
|
||||
|
||||
protected var job: Job? = null
|
||||
|
||||
fun cancelOperation() {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
job?.run {
|
||||
cancelAndJoin()
|
||||
onSuccess(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
package com.appttude.h_mal.atlas_weather.base.baseViewModels
|
||||
|
||||
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
|
||||
abstract class WeatherViewModel : BaseViewModel() {
|
||||
|
||||
fun createFullWeather(
|
||||
weather: WeatherResponse,
|
||||
location: String
|
||||
locationName: String
|
||||
): FullWeather {
|
||||
return FullWeather(weather).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = location
|
||||
locationString = locationName
|
||||
}
|
||||
}
|
||||
|
||||
fun createWeatherEntity(
|
||||
locationId: String,
|
||||
locationName: String,
|
||||
weather: FullWeather
|
||||
): EntityItem {
|
||||
): WeatherEntity {
|
||||
weather.apply {
|
||||
locationString = locationId
|
||||
locationString = locationName
|
||||
}
|
||||
|
||||
return EntityItem(locationId, weather)
|
||||
return WeatherEntity(locationName, weather)
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@ package com.appttude.h_mal.atlas_weather.data.location
|
||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.location.Address
|
||||
import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||
import com.google.android.gms.location.LocationServices
|
||||
@@ -20,8 +22,6 @@ import java.util.*
|
||||
class LocationProviderImpl(
|
||||
private val applicationContext: Context
|
||||
) : LocationProvider, LocationHelper(applicationContext) {
|
||||
private var locationManager =
|
||||
applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
|
||||
private val client = LocationServices.getFusedLocationProviderClient(applicationContext)
|
||||
private val geoCoder: Geocoder by lazy { Geocoder(applicationContext, Locale.getDefault()) }
|
||||
|
||||
@@ -70,11 +70,14 @@ class LocationProviderImpl(
|
||||
private suspend fun getAFreshLocation(): Location? {
|
||||
return client.getCurrentLocation(
|
||||
Priority.PRIORITY_HIGH_ACCURACY,
|
||||
object : CancellationToken() {
|
||||
override fun isCancellationRequested(): Boolean = false
|
||||
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken =
|
||||
this
|
||||
}).await()
|
||||
cancellationToken
|
||||
).await()
|
||||
}
|
||||
|
||||
private val cancellationToken = object : CancellationToken() {
|
||||
override fun isCancellationRequested(): Boolean = false
|
||||
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken =
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,20 +2,20 @@ package com.appttude.h_mal.atlas_weather.data.repository
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
|
||||
interface Repository {
|
||||
|
||||
suspend fun getWeatherFromApi(lat: String, long: String): WeatherResponse
|
||||
suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem)
|
||||
suspend fun saveWeatherListToRoom(list: List<EntityItem>)
|
||||
fun loadRoomWeatherLiveData(): LiveData<List<EntityItem>>
|
||||
suspend fun saveCurrentWeatherToRoom(weatherEntity: WeatherEntity)
|
||||
suspend fun saveWeatherListToRoom(list: List<WeatherEntity>)
|
||||
fun loadRoomWeatherLiveData(): LiveData<List<WeatherEntity>>
|
||||
suspend fun loadWeatherList(): List<String>
|
||||
fun loadCurrentWeatherFromRoom(id: String): LiveData<EntityItem>
|
||||
suspend fun loadSingleCurrentWeatherFromRoom(id: String): EntityItem
|
||||
fun loadCurrentWeatherFromRoom(id: String): LiveData<WeatherEntity>
|
||||
suspend fun loadSingleCurrentWeatherFromRoom(id: String): WeatherEntity
|
||||
fun isSearchValid(locationName: String): Boolean
|
||||
fun saveLastSavedAt(locationName: String)
|
||||
suspend fun deleteSavedWeatherEntry(locationName: String): Boolean
|
||||
fun getSavedLocations(): List<String>
|
||||
suspend fun getSingleWeather(locationName: String): EntityItem
|
||||
suspend fun getSingleWeather(locationName: String): WeatherEntity
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherRe
|
||||
import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST
|
||||
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.utils.FALLBACK_TIME
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ class RepositoryImpl(
|
||||
return responseUnwrap { api.getFromApi(lat, long) }
|
||||
}
|
||||
|
||||
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem) {
|
||||
db.getWeatherDao().upsertFullWeather(entityItem)
|
||||
override suspend fun saveCurrentWeatherToRoom(weatherEntity: WeatherEntity) {
|
||||
db.getWeatherDao().upsertFullWeather(weatherEntity)
|
||||
}
|
||||
|
||||
override suspend fun saveWeatherListToRoom(
|
||||
list: List<EntityItem>
|
||||
list: List<WeatherEntity>
|
||||
) {
|
||||
db.getWeatherDao().upsertListOfFullWeather(list)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class RepositoryImpl(
|
||||
return prefs.getAllKeys().toList()
|
||||
}
|
||||
|
||||
override suspend fun getSingleWeather(locationName: String): EntityItem {
|
||||
override suspend fun getSingleWeather(locationName: String): WeatherEntity {
|
||||
return db.getWeatherDao().getCurrentFullWeatherSingle(locationName)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
|
||||
@Database(
|
||||
entities = [EntityItem::class],
|
||||
entities = [WeatherEntity::class],
|
||||
version = 1,
|
||||
exportSchema = false
|
||||
)
|
||||
|
||||
@@ -6,30 +6,30 @@ import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
|
||||
@Dao
|
||||
interface WeatherDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun upsertFullWeather(item: EntityItem)
|
||||
fun upsertFullWeather(item: WeatherEntity)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun upsertListOfFullWeather(items: List<EntityItem>)
|
||||
fun upsertListOfFullWeather(items: List<WeatherEntity>)
|
||||
|
||||
@Query("SELECT * FROM EntityItem WHERE id = :userId LIMIT 1")
|
||||
fun getCurrentFullWeather(userId: String): LiveData<EntityItem>
|
||||
@Query("SELECT * FROM WeatherEntity WHERE id = :userId LIMIT 1")
|
||||
fun getCurrentFullWeather(userId: String): LiveData<WeatherEntity>
|
||||
|
||||
@Query("SELECT * FROM EntityItem WHERE id = :userId LIMIT 1")
|
||||
fun getCurrentFullWeatherSingle(userId: String): EntityItem
|
||||
@Query("SELECT * FROM WeatherEntity WHERE id = :userId LIMIT 1")
|
||||
fun getCurrentFullWeatherSingle(userId: String): WeatherEntity
|
||||
|
||||
@Query("SELECT * FROM EntityItem WHERE id != :id")
|
||||
fun getAllFullWeatherWithoutCurrent(id: String = CURRENT_LOCATION): LiveData<List<EntityItem>>
|
||||
@Query("SELECT * FROM WeatherEntity WHERE id != :id")
|
||||
fun getAllFullWeatherWithoutCurrent(id: String = CURRENT_LOCATION): LiveData<List<WeatherEntity>>
|
||||
|
||||
@Query("SELECT * FROM EntityItem WHERE id != :id")
|
||||
fun getWeatherListWithoutCurrent(id: String = CURRENT_LOCATION): List<EntityItem>
|
||||
@Query("SELECT * FROM WeatherEntity WHERE id != :id")
|
||||
fun getWeatherListWithoutCurrent(id: String = CURRENT_LOCATION): List<WeatherEntity>
|
||||
|
||||
@Query("DELETE FROM EntityItem WHERE id = :userId")
|
||||
@Query("DELETE FROM WeatherEntity WHERE id = :userId")
|
||||
fun deleteEntry(userId: String): Int
|
||||
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
const val CURRENT_LOCATION = "CurrentLocation"
|
||||
|
||||
@Entity
|
||||
data class EntityItem(
|
||||
data class WeatherEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
val id: String,
|
||||
val weather: FullWeather
|
||||
@@ -5,15 +5,18 @@ import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetData
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetError
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetState
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
|
||||
import com.appttude.h_mal.atlas_weather.utils.toSmallDayName
|
||||
import com.squareup.picasso.Picasso
|
||||
@@ -47,10 +50,10 @@ class ServicesHelper(
|
||||
temperatureUnit = "°C"
|
||||
locationString = currentLocation
|
||||
}
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, fullWeather)
|
||||
// Save data if not null
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
repository.saveCurrentWeatherToRoom(weatherEntity)
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
@@ -58,6 +61,77 @@ class ServicesHelper(
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
suspend fun fetchWidgetData(): WidgetState {
|
||||
// Data was loaded within last 5 minutes - no need to retrieve again
|
||||
if (!repository.isSearchValid(CURRENT_LOCATION)) {
|
||||
val data = getWidgetWeatherCollection()
|
||||
data?.let {
|
||||
return WidgetState.HasData(it)
|
||||
}
|
||||
}
|
||||
|
||||
// Try and retrieve location
|
||||
val latLong = try {
|
||||
locationProvider.getCurrentLatLong()
|
||||
} catch (e: IOException) {
|
||||
val data = getWidgetWeatherCollection()
|
||||
data?.let {
|
||||
return WidgetState.HasData(it)
|
||||
}
|
||||
|
||||
val error = WidgetError(
|
||||
icon = R.drawable.ic_baseline_cloud_off_24,
|
||||
errorMessage = "Failed to retrieve location, check location"
|
||||
)
|
||||
return WidgetState.HasError(error)
|
||||
}
|
||||
|
||||
// Get weather from api
|
||||
val weather = try {
|
||||
repository
|
||||
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
|
||||
} catch (e: IOException) {
|
||||
val data = getWidgetWeatherCollection()
|
||||
data?.let {
|
||||
return WidgetState.HasData(it)
|
||||
}
|
||||
|
||||
val error = WidgetError(
|
||||
icon = R.drawable.ic_baseline_cloud_off_24,
|
||||
errorMessage = "Failed to retrieve weather data, check connection"
|
||||
)
|
||||
return WidgetState.HasError(error)
|
||||
}
|
||||
|
||||
val currentLocation = try {
|
||||
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
|
||||
} catch (e: IOException) {
|
||||
val data = getWidgetWeatherCollection()
|
||||
data?.let {
|
||||
return WidgetState.HasData(it)
|
||||
}
|
||||
|
||||
val error = WidgetError(
|
||||
icon = R.drawable.ic_baseline_cloud_off_24,
|
||||
errorMessage = "Failed to retrieve location name"
|
||||
)
|
||||
return WidgetState.HasError(error)
|
||||
}
|
||||
|
||||
val fullWeather = FullWeather(weather).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = currentLocation
|
||||
}
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, fullWeather)
|
||||
// Save data to database
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(weatherEntity)
|
||||
|
||||
val data = createWidgetWeatherCollection(weatherEntity, currentLocation)
|
||||
return WidgetState.HasData(data)
|
||||
}
|
||||
|
||||
suspend fun getWidgetWeather(): WidgetData? {
|
||||
return try {
|
||||
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
|
||||
@@ -127,6 +201,30 @@ class ServicesHelper(
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWidgetWeatherCollection(result: WeatherEntity, locationName: String): WidgetWeatherCollection {
|
||||
val widgetData = result.weather.let {
|
||||
val bitmap = it.current?.icon
|
||||
val temp = it.current?.temp?.toInt().toString()
|
||||
val epoc = System.currentTimeMillis()
|
||||
|
||||
WidgetData(locationName, bitmap, temp, epoc)
|
||||
}
|
||||
|
||||
val list = mutableListOf<InnerWidgetCellData>()
|
||||
|
||||
result.weather.daily?.drop(1)?.dropLast(2)?.forEach { dailyWeather ->
|
||||
val day = dailyWeather.dt?.toSmallDayName()
|
||||
val icon = dailyWeather.icon
|
||||
val temp = dailyWeather.max?.toInt().toString()
|
||||
|
||||
val item = InnerWidgetCellData(day, icon, temp)
|
||||
list.add(item)
|
||||
}
|
||||
list.toList()
|
||||
|
||||
return WidgetWeatherCollection(widgetData, list)
|
||||
}
|
||||
|
||||
private suspend fun getBitmapFromUrl(imageAddress: String?): Bitmap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Picasso.get().load(imageAddress).into(object : Target {
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.appttude.h_mal.atlas_weather.model.forecast
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.Hour
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ data class WeatherDisplay(
|
||||
) {
|
||||
}
|
||||
|
||||
constructor(entity: EntityItem) : this(
|
||||
constructor(entity: WeatherEntity) : this(
|
||||
entity.weather.current?.temp,
|
||||
entity.weather.temperatureUnit,
|
||||
entity.id,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.appttude.h_mal.atlas_weather.model.widget
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
data class WidgetError(
|
||||
@DrawableRes
|
||||
val icon: Int,
|
||||
val errorMessage: String
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.appttude.h_mal.atlas_weather.model.widget
|
||||
|
||||
sealed class WidgetState {
|
||||
class HasData<T : Any>(val data: T) : WidgetState()
|
||||
class HasError<T : Any>(val error: T) : WidgetState()
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import androidx.annotation.RequiresPermission
|
||||
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.WeatherViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -18,8 +18,8 @@ class MainViewModel(
|
||||
) : WeatherViewModel() {
|
||||
|
||||
init {
|
||||
repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever {
|
||||
it?.let {
|
||||
repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever {w ->
|
||||
w?.let {
|
||||
val weather = WeatherDisplay(it)
|
||||
onSuccess(weather)
|
||||
}
|
||||
@@ -29,10 +29,10 @@ class MainViewModel(
|
||||
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
fun fetchData() {
|
||||
onStart()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
job = CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
// Has the search been conducted in the last 5 minutes
|
||||
val entityItem = if (repository.isSearchValid(CURRENT_LOCATION)) {
|
||||
val weatherEntity = if (repository.isSearchValid(CURRENT_LOCATION)) {
|
||||
// Get location
|
||||
val latLong = locationProvider.getCurrentLatLong()
|
||||
// Get weather from api
|
||||
@@ -41,13 +41,13 @@ class MainViewModel(
|
||||
val currentLocation =
|
||||
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
|
||||
val fullWeather = createFullWeather(weather, currentLocation)
|
||||
EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
WeatherEntity(CURRENT_LOCATION, fullWeather)
|
||||
} else {
|
||||
repository.getSingleWeather(CURRENT_LOCATION)
|
||||
}
|
||||
// Save data if not null
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
repository.saveCurrentWeatherToRoom(weatherEntity)
|
||||
} catch (e: Exception) {
|
||||
onError(e.message!!)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.appttude.h_mal.atlas_weather.viewmodel
|
||||
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||
import com.appttude.h_mal.atlas_weather.base.baseViewModels.WeatherViewModel
|
||||
@@ -102,7 +102,7 @@ class WorldViewModel(
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val list = mutableListOf<EntityItem>()
|
||||
val list = mutableListOf<WeatherEntity>()
|
||||
repository.loadWeatherList().forEach { locationName ->
|
||||
// If search not valid move onto next in loop
|
||||
if (!repository.isSearchValid(locationName)) return@forEach
|
||||
@@ -151,7 +151,7 @@ class WorldViewModel(
|
||||
return repository.getWeatherFromApi(lat.toString(), lon.toString())
|
||||
}
|
||||
|
||||
private suspend fun createWeatherEntity(locationName: String): EntityItem {
|
||||
private suspend fun createWeatherEntity(locationName: String): WeatherEntity {
|
||||
val weather = getWeather(locationName)
|
||||
val location =
|
||||
locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon, LocationType.City)
|
||||
|
||||
@@ -22,7 +22,7 @@ abstract class BaseWidgetServiceIntentClass<T : AppWidgetProvider> : JobIntentSe
|
||||
lateinit var appWidgetManager: AppWidgetManager
|
||||
lateinit var appWidgetIds: IntArray
|
||||
|
||||
fun initiallizeWidgetData(componentName: ComponentName) {
|
||||
fun initializeWidgetData(componentName: ComponentName) {
|
||||
appWidgetManager = AppWidgetManager.getInstance(baseContext)
|
||||
appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
|
||||
}
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
package com.appttude.h_mal.atlas_weather.widget
|
||||
|
||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.os.PowerManager
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.app.ActivityCompat.checkSelfPermission
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetError
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetState.HasData
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetState.HasError
|
||||
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
|
||||
import com.appttude.h_mal.atlas_weather.ui.MainActivity
|
||||
import com.appttude.h_mal.atlas_weather.utils.isInternetAvailable
|
||||
import com.appttude.h_mal.atlas_weather.utils.tryOrNullSuspended
|
||||
import com.appttude.h_mal.atlas_weather.widget.WidgetState.*
|
||||
import com.appttude.h_mal.atlas_weather.widget.WidgetState.Companion.getWidgetState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -46,59 +42,35 @@ class WidgetJobServiceIntent : BaseWidgetServiceIntentClass<NewAppWidget>() {
|
||||
executeWidgetUpdate()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun executeWidgetUpdate() {
|
||||
val componentName = ComponentName(this, NewAppWidget::class.java)
|
||||
initiallizeWidgetData(componentName)
|
||||
initializeWidgetData(componentName)
|
||||
|
||||
initiateWidgetUpdate(getCurrentWidgetState())
|
||||
}
|
||||
|
||||
private fun initiateWidgetUpdate(state: WidgetState) {
|
||||
when (state) {
|
||||
NO_LOCATION, SCREEN_ON_CONNECTION_UNAVAILABLE -> updateErrorWidget(state)
|
||||
else -> updateWidget(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWidget(fromStorage: Boolean) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val result = getWidgetWeather(fromStorage)
|
||||
appWidgetIds.forEach { id -> setupView(id, result) }
|
||||
setLoadingView()
|
||||
|
||||
val widgetState = helper.fetchWidgetData()
|
||||
appWidgetIds.forEach { id ->
|
||||
when (widgetState) {
|
||||
is HasData<*> -> {
|
||||
val data = widgetState.data as WidgetWeatherCollection
|
||||
setupView(id, data)
|
||||
}
|
||||
|
||||
is HasError<*> -> {
|
||||
if (widgetState.error is WidgetError) {
|
||||
val error = widgetState.error
|
||||
setupErrorView(id, error)
|
||||
}
|
||||
}
|
||||
|
||||
else -> return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateErrorWidget(state: WidgetState) {
|
||||
appWidgetIds.forEach { id -> setEmptyView(id, state) }
|
||||
}
|
||||
|
||||
private fun getCurrentWidgetState(): WidgetState {
|
||||
val pm = getSystemService(POWER_SERVICE) as PowerManager
|
||||
val isScreenOn = pm.isInteractive
|
||||
val locationGranted =
|
||||
checkSelfPermission(this, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED
|
||||
val internetAvailable = isInternetAvailable(this.applicationContext)
|
||||
|
||||
return getWidgetState(locationGranted, isScreenOn, internetAvailable)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
suspend fun getWidgetWeather(storageOnly: Boolean): WidgetWeatherCollection? {
|
||||
return tryOrNullSuspended {
|
||||
if (!storageOnly) helper.fetchData()
|
||||
helper.getWidgetWeatherCollection()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setEmptyView(appWidgetId: Int, state: WidgetState) {
|
||||
val error = when (state) {
|
||||
NO_LOCATION -> "No Location Permission"
|
||||
SCREEN_ON_CONNECTION_UNAVAILABLE -> "No network available"
|
||||
else -> "No data"
|
||||
}
|
||||
|
||||
val views = createRemoteView(R.layout.weather_app_widget)
|
||||
bindErrorView(appWidgetId, views, error)
|
||||
}
|
||||
|
||||
private fun setupView(
|
||||
appWidgetId: Int,
|
||||
@@ -115,6 +87,49 @@ class WidgetJobServiceIntent : BaseWidgetServiceIntentClass<NewAppWidget>() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupErrorView(
|
||||
appWidgetId: Int,
|
||||
error: WidgetError
|
||||
) {
|
||||
val views = createRemoteView(R.layout.weather_app_widget)
|
||||
// setLastUpdated(views, collection?.widgetData?.timeStamp)
|
||||
views.setInt(R.id.whole_widget_view, "setBackgroundColor", helper.getWidgetBackground())
|
||||
|
||||
bindEmptyView(appWidgetId, views, error.errorMessage)
|
||||
}
|
||||
|
||||
@SuppressLint("DiscouragedApi")
|
||||
private fun setLoadingView() {
|
||||
appWidgetIds.forEach { id ->
|
||||
val views = createRemoteView(R.layout.weather_app_widget)
|
||||
views.setInt(R.id.whole_widget_view, "setBackgroundColor", helper.getWidgetBackground())
|
||||
|
||||
val clickUpdate = createUpdatePendingIntent(NewAppWidget::class.java, id)
|
||||
|
||||
views.apply {
|
||||
setTextViewText(R.id.widget_current_location, "Loading...")
|
||||
setImageViewResource(R.id.location_icon, R.drawable.location_flag)
|
||||
setOnClickPendingIntent(R.id.widget_current_location, clickUpdate)
|
||||
|
||||
(0..4).forEach { i ->
|
||||
val dayId: Int =
|
||||
resources.getIdentifier("widget_item_day_$i", "id", packageName)
|
||||
val imageId: Int =
|
||||
resources.getIdentifier("widget_item_image_$i", "id", packageName)
|
||||
val tempId: Int =
|
||||
resources.getIdentifier("widget_item_temp_high_$i", "id", packageName)
|
||||
|
||||
views.setTextViewText(dayId, "loading")
|
||||
views.setTextViewText(tempId, "")
|
||||
setImageViewResource(imageId, R.drawable.baseline_refresh_24)
|
||||
}
|
||||
|
||||
// Instruct the widget manager to update the widget
|
||||
appWidgetManager.updateAppWidget(id, views)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun bindErrorView(
|
||||
widgetId: Int,
|
||||
views: RemoteViews,
|
||||
|
||||
5
app/src/main/res/drawable/baseline_refresh_24.xml
Normal file
5
app/src/main/res/drawable/baseline_refresh_24.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
||||
@@ -5,22 +5,17 @@ import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherRe
|
||||
import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST
|
||||
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.utils.BaseTest
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.doAnswer
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
@@ -124,7 +119,7 @@ class RepositoryImplTest : BaseTest() {
|
||||
@Test
|
||||
fun loadWeatherList_validResponse() {
|
||||
// Arrange
|
||||
val elements = listOf<EntityItem>(
|
||||
val elements = listOf<WeatherEntity>(
|
||||
mockk { every { id } returns any() },
|
||||
mockk { every { id } returns any() },
|
||||
mockk { every { id } returns any() },
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherRe
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
import com.appttude.h_mal.atlas_weather.utils.BaseTest
|
||||
import io.mockk.MockKAnnotations
|
||||
@@ -44,7 +44,7 @@ class ServicesHelperTest : BaseTest() {
|
||||
@Test
|
||||
fun testWidgetDataAsync_successfulResponse() = runBlocking {
|
||||
// Arrange
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = CURRENT_LOCATION
|
||||
})
|
||||
@@ -68,7 +68,7 @@ class ServicesHelperTest : BaseTest() {
|
||||
)
|
||||
}.returns(CURRENT_LOCATION)
|
||||
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(entityItem) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(weatherEntity) } returns Unit
|
||||
|
||||
// Assert
|
||||
val result = helper.fetchData()
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
|
||||
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.WeatherEntity
|
||||
import com.appttude.h_mal.atlas_weather.model.ViewState
|
||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
@@ -50,7 +50,7 @@ class WorldViewModelTest : BaseTest() {
|
||||
@Test
|
||||
fun fetchDataForSingleLocation_validLocation_validReturn() {
|
||||
// Arrange
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = CURRENT_LOCATION
|
||||
})
|
||||
@@ -75,7 +75,7 @@ class WorldViewModelTest : BaseTest() {
|
||||
)
|
||||
}.returns(CURRENT_LOCATION)
|
||||
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(entityItem) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(weatherEntity) } returns Unit
|
||||
|
||||
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
|
||||
|
||||
@@ -113,13 +113,13 @@ class WorldViewModelTest : BaseTest() {
|
||||
@Test
|
||||
fun searchAboveFallbackTime_validLocation_validReturn() {
|
||||
// Arrange
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
val weatherEntity = WeatherEntity(CURRENT_LOCATION, FullWeather(weatherResponse).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = CURRENT_LOCATION
|
||||
})
|
||||
|
||||
// Act
|
||||
coEvery { repository.getSingleWeather(CURRENT_LOCATION) }.returns(entityItem)
|
||||
coEvery { repository.getSingleWeather(CURRENT_LOCATION) }.returns(weatherEntity)
|
||||
every { repository.isSearchValid(CURRENT_LOCATION) }.returns(false)
|
||||
coEvery {
|
||||
repository.getWeatherFromApi(
|
||||
@@ -128,7 +128,7 @@ class WorldViewModelTest : BaseTest() {
|
||||
)
|
||||
}.returns(weatherResponse)
|
||||
every { repository.saveLastSavedAt(CURRENT_LOCATION) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(entityItem) } returns Unit
|
||||
coEvery { repository.saveCurrentWeatherToRoom(weatherEntity) } returns Unit
|
||||
|
||||
viewModel.fetchDataForSingleLocation(CURRENT_LOCATION)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user