- change location retrieval accuracy

- change location retrieval caching from location provider
This commit is contained in:
2023-12-17 23:12:20 +00:00
parent 65d0c17b00
commit 226280452d
22 changed files with 281 additions and 137 deletions

1
.gitignore vendored
View File

@@ -89,6 +89,7 @@ gen-external-apklibs
.idea/assetWizardSettings.xml
.idea/gradle.xml
.idea/jarRepositorie
.idea/assetWizardSettings.xml
# Gem/fastlane
Gemfile.lock

View File

@@ -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)

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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()
}

View File

@@ -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!!)
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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,

View 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>

View File

@@ -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() },

View File

@@ -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()

View File

@@ -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)