- weathered

This commit is contained in:
2021-04-20 20:09:32 +01:00
parent 9d053bfb60
commit b41ba10abb
212 changed files with 2453 additions and 1413 deletions

View File

@@ -378,7 +378,7 @@
<PersistentState> <PersistentState>
<option name="values"> <option name="values">
<map> <map>
<entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material/icons/materialicons/cloud_off/baseline_cloud_off_24.xml" /> <entry key="url" value="jar:file:/C:/Program%20Files/Android/Android%20Studio/plugins/android/lib/android.jar!/images/material/icons/materialicons/cloud_queue/baseline_cloud_queue_24.xml" />
</map> </map>
</option> </option>
</PersistentState> </PersistentState>
@@ -389,7 +389,7 @@
<option name="values"> <option name="values">
<map> <map>
<entry key="color" value="ffffff" /> <entry key="color" value="ffffff" />
<entry key="outputName" value="ic_baseline_cloud_off_24" /> <entry key="outputName" value="ic_baseline_cloud_queue_24" />
<entry key="sourceFile" value="D:\Android Studio Projects\Private work\Altas_-_Weather" /> <entry key="sourceFile" value="D:\Android Studio Projects\Private work\Altas_-_Weather" />
</map> </map>
</option> </option>

Binary file not shown.

7
.idea/dictionaries/h_mal.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="h_mal">
<words>
<w>upsert</w>
</words>
</dictionary>
</component>

View File

@@ -36,5 +36,10 @@
<option name="name" value="C:\Users\h_mal\AppData\Local\Android\Sdk\extras\m2repository" /> <option name="name" value="C:\Users\h_mal\AppData\Local\Android\Sdk\extras\m2repository" />
<option name="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/extras/m2repository/" /> <option name="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/extras/m2repository/" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://maven.tomtom.com:8443/nexus/content/repositories/releases/" />
</remote-repository>
</component> </component>
</project> </project>

View File

@@ -12,12 +12,30 @@ android {
applicationId "com.appttude.h_mal.atlas_weather" applicationId "com.appttude.h_mal.atlas_weather"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 30 targetSdkVersion 30
versionCode 2 versionCode 5
versionName "2.0" versionName "3.0"
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
buildConfigField "String", "ParamOne", "${paramOneEndPoint}" buildConfigField "String", "ParamOne", "${paramOneEndPoint}"
buildConfigField "String", "ParamTwo", "${paramTwoEndPoint}"
}
android {
sourceSets{
test {
resources.srcDirs += ['src/test/resources']
}
androidTest {
resources.srcDirs += ['src/androidTest/resources']
}
}
}
testOptions {
unitTests {
includeAndroidResources = true
returnDefaultValues = true
}
} }
buildTypes { buildTypes {
release { release {
@@ -58,6 +76,8 @@ android {
} }
} }
} }
dependencies { dependencies {
@@ -67,15 +87,19 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
implementation 'androidx.vectordrawable:vectordrawable:1.0.0' implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
implementation "com.google.android.gms:play-services-location:16.0.0" implementation "com.google.android.gms:play-services-location:18.0.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1' implementation 'androidx.navigation:navigation-ui-ktx:2.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
// Unit testing // Unit testing
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
@@ -93,7 +117,7 @@ dependencies {
androidTestImplementation 'androidx.test:rules:1.3.0-rc01' androidTestImplementation 'androidx.test:rules:1.3.0-rc01'
// Mockk // Mockk
def mockk_ver = "1.10.2" def mockk_ver = "1.10.5"
testImplementation "io.mockk:mockk:$mockk_ver" testImplementation "io.mockk:mockk:$mockk_ver"
androidTestImplementation "io.mockk:mockk-android:$mockk_ver" androidTestImplementation "io.mockk:mockk-android:$mockk_ver"
@@ -101,6 +125,7 @@ dependencies {
def retrofit_ver = "2.8.1" def retrofit_ver = "2.8.1"
implementation "com.squareup.retrofit2:retrofit:$retrofit_ver" implementation "com.squareup.retrofit2:retrofit:$retrofit_ver"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver" implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"
// Shared prefs // Shared prefs
def prefs_ver = "1.1.1" def prefs_ver = "1.1.1"
@@ -119,4 +144,21 @@ dependencies {
// Picasso // Picasso
implementation 'com.squareup.picasso:picasso:2.71828' implementation 'com.squareup.picasso:picasso:2.71828'
// coroutine
def coroutine_version = "1.3.9"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
// tomtom search
def tomtom_version = "2.4771"
implementation "com.tomtom.online:sdk-search:$tomtom_version"
implementation "com.tomtom.online:sdk-maps:2.4807"
/* coroutines support for firebase operations */
def coroutines_google_ver = "1.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutines_google_ver"
/ * Glide */
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
} }

View File

@@ -0,0 +1,85 @@
package com.appttude.h_mal.atlas_weather.data.location
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.appttude.h_mal.atlas_weather.model.types.LocationType
import kotlinx.coroutines.runBlocking
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.*
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
@SmallTest
class LocationProviderImplTest {
private val lat = 51.558
private val long = -0.091
private val town = "Highbury"
private val city = "London"
lateinit var locationProvider: LocationProviderImpl
@Before
fun setUp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext
locationProvider = LocationProviderImpl(appContext)
}
@Test
fun getLatLongFromLocationName_correctLatLongReturned() {
// Act
val retrievedLocation = locationProvider.getLatLongFromLocationName(town)
val retrievedLat = retrievedLocation.first
val retrievedLong = retrievedLocation.second
// Assert
assertRangeOfDouble(retrievedLat, lat, 0.1)
assertRangeOfDouble(retrievedLong, long, 0.1)
}
@Test
fun getLatLongFromLocationName_throwReturned() {
// Arrange
val randomString = "gHKJkhgkj"
try {
// Act
locationProvider.getLatLongFromLocationName(randomString)
}catch (e: IOException){
// Assert
assertEquals(e.message, "No location found")
}
}
@Test
fun getLocationNameFromLatLong_locationTypeIsDefault_correctLocationReturned() = runBlocking {
// Act
val retrievedLocation = locationProvider.getLocationNameFromLatLong(lat, long)
// Assert
assertEquals(retrievedLocation, town)
}
@Test
fun getLocationNameFromLatLong_locationTypeIsCity_correctLocationReturned() = runBlocking {
// Act
val retrievedLocation = locationProvider.getLocationNameFromLatLong(lat, long, LocationType.City)
// Assert
assertEquals(retrievedLocation, city)
}
private fun assertRangeOfDouble(input: Double, expected: Double, range: Double) {
assertThat(expected, allOf(
greaterThanOrEqualTo(input - range),
lessThanOrEqualTo(input + range))
)
}
}

View File

@@ -36,7 +36,7 @@ class NotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
context.displayToast("Please enable location permissions") context.displayToast("Please enable location permissions")
return return
} }

View File

@@ -57,13 +57,13 @@ class HomeFragment : BaseFragment(), KodeinAware {
adapter = recyclerAdapter adapter = recyclerAdapter
} }
getPermissionResult(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION_PERMISSION_REQUEST){ getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){
viewModel.fetchData() viewModel.fetchData()
} }
swipe_refresh.apply { swipe_refresh.apply {
setOnRefreshListener { setOnRefreshListener {
getPermissionResult(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION_PERMISSION_REQUEST){ getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){
viewModel.fetchData() viewModel.fetchData()
} }
isRefreshing = true isRefreshing = true

View File

@@ -35,7 +35,7 @@ class NewAppWidget : BaseWidgetClass() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
// There may be multiple widgets active, so update all of them // There may be multiple widgets active, so update all of them
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return return
} }

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">

View File

@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.appttude.h_mal.atlas_weather"> package="com.appttude.h_mal.atlas_weather">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SET_ALARM" /> <uses-permission android:name="android.permission.SET_ALARM" />

View File

@@ -2,16 +2,17 @@ package com.appttude.h_mal.atlas_weather.application
import android.app.Application import android.app.Application
import androidx.test.espresso.idling.CountingIdlingResource import androidx.test.espresso.idling.CountingIdlingResource
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.google.gson.Gson import com.google.gson.Gson
import org.kodein.di.Kodein import org.kodein.di.Kodein
import org.kodein.di.KodeinAware import org.kodein.di.KodeinAware
@@ -22,9 +23,10 @@ import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton import org.kodein.di.generic.singleton
const val LOCATION_PERMISSION_REQUEST = 505 const val LOCATION_PERMISSION_REQUEST = 505
class AppClass : Application(), KodeinAware { class AppClass : Application(), KodeinAware {
companion object{ companion object {
// idling resource to be used for espresso testing // idling resource to be used for espresso testing
// when we need to wait for async operations to complete // when we need to wait for async operations to complete
val idlingResources = CountingIdlingResource("Data_loader") val idlingResources = CountingIdlingResource("Data_loader")
@@ -35,15 +37,15 @@ class AppClass : Application(), KodeinAware {
import(androidXModule(this@AppClass)) import(androidXModule(this@AppClass))
bind() from singleton { Gson() } bind() from singleton { Gson() }
bind() from singleton { NetworkConnectionInterceptor(instance()) } bind() from singleton { NetworkConnectionInterceptor(instance()) }
bind() from singleton { QueryParamsInterceptor() } bind() from singleton { QueryParamsInterceptor() }
bind() from singleton { WeatherApi(instance(), instance())} bind() from singleton { loggingInterceptor }
bind() from singleton { WeatherApi("https://api.openweathermap.org/data/2.5/", instance(), instance(), instance()) }
bind() from singleton { AppDatabase(instance()) } bind() from singleton { AppDatabase(instance()) }
bind() from singleton { PreferenceProvider(instance()) } bind() from singleton { PreferenceProvider(instance()) }
bind() from singleton { RepositoryImpl(instance(), instance(), instance()) } bind() from singleton { RepositoryImpl(instance(), instance(), instance()) }
bind() from singleton { SettingsRepositoryImpl(instance()) } bind() from singleton { SettingsRepositoryImpl(instance()) }
bind() from singleton { LocationProvider(instance()) } bind() from singleton { LocationProviderImpl(instance()) }
bind() from singleton { ServicesHelper(instance(), instance(), instance()) } bind() from singleton { ServicesHelper(instance(), instance(), instance()) }
bind() from provider { ApplicationViewModelFactory(instance(), instance()) } bind() from provider { ApplicationViewModelFactory(instance(), instance()) }
} }

View File

@@ -0,0 +1,34 @@
package com.appttude.h_mal.atlas_weather.data.location
import android.content.Context
import android.location.Location
import com.appttude.h_mal.atlas_weather.BuildConfig
import com.appttude.h_mal.atlas_weather.utils.createSuspend
import com.tomtom.online.sdk.search.OnlineSearchApi
import com.tomtom.online.sdk.search.data.common.Address
import com.tomtom.online.sdk.search.data.reversegeocoder.ReverseGeocoderSearchQueryBuilder
abstract class LocationHelper(
context: Context
) {
private val key = BuildConfig.ParamTwo
private val searchApi = OnlineSearchApi.create(context, key)
suspend fun getAddressFromLatLong(
lat: Double, long: Double
): Address? {
return createSuspend {
val revGeoQuery =
ReverseGeocoderSearchQueryBuilder(lat, long).build()
val resultSingle =
searchApi.reverseGeocoding(revGeoQuery)
resultSingle.blockingGet()?.addresses?.get(0)?.address
}
}
protected fun Location.getLatLonPair(): Pair<Double, Double> = Pair(latitude, longitude)
}

View File

@@ -1,54 +1,13 @@
package com.appttude.h_mal.atlas_weather.data.location package com.appttude.h_mal.atlas_weather.data.location
import android.Manifest.permission.ACCESS_FINE_LOCATION import com.appttude.h_mal.atlas_weather.model.types.LocationType
import android.content.Context
import android.content.Context.LOCATION_SERVICE
import android.location.Geocoder
import android.location.LocationManager
import androidx.annotation.RequiresPermission
import java.io.IOException
import java.util.*
class LocationProvider(
val applicationContext: Context
) {
private var locationManager =
applicationContext.getSystemService(LOCATION_SERVICE) as LocationManager?
private val geoCoder: Geocoder by lazy { Geocoder(applicationContext, Locale.getDefault()) }
@RequiresPermission(value = ACCESS_FINE_LOCATION)
fun getLatLong(): Pair<Double, Double>{
val location = locationManager
?.getLastKnownLocation(LocationManager.GPS_PROVIDER)
?: locationManager?.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
location ?: throw IOException("Unable to get location")
val lat = location.latitude
val long = location.longitude
return Pair(lat, long)
}
fun getLocationName(lat: Double, long: Double): String{
val result = geoCoder.getFromLocation(lat, long, 1)?.get(0)
return result?.let { location ->
location.locality?.let { return it }
location.subAdminArea?.let { return it }
} ?: "$lat, $long"
}
fun getLatLongFromLocationString(location: String): Pair<Double, Double>{
val locations = geoCoder.getFromLocationName(location, 1)
locations?.takeIf { it.isNotEmpty() }?.get(0)?.let {
return Pair(it.latitude, it.longitude)
}
throw IOException("No location found")
}
interface LocationProvider {
suspend fun getCurrentLatLong(): Pair<Double, Double>
fun getLatLongFromLocationName(location: String): Pair<Double, Double>
suspend fun getLocationNameFromLatLong(
lat: Double,
long: Double,
type: LocationType = LocationType.Town
): String
} }

View File

@@ -0,0 +1,116 @@
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.Geocoder
import android.location.Location
import android.location.LocationManager
import android.os.HandlerThread
import androidx.annotation.RequiresPermission
import com.appttude.h_mal.atlas_weather.model.types.LocationType
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationRequest.PRIORITY_HIGH_ACCURACY
import com.google.android.gms.location.LocationRequest.PRIORITY_LOW_POWER
import com.google.android.gms.location.LocationResult
import com.google.android.gms.tasks.CancellationToken
import com.google.android.gms.tasks.OnTokenCanceledListener
import kotlinx.coroutines.tasks.await
import java.io.IOException
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LocationProviderImpl(
val applicationContext: Context
) : LocationProvider, LocationHelper(applicationContext) {
private var locationManager =
applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
private val client = FusedLocationProviderClient(applicationContext)
private val geoCoder: Geocoder by lazy { Geocoder(applicationContext, Locale.getDefault()) }
@RequiresPermission(value = ACCESS_COARSE_LOCATION)
override suspend fun getCurrentLatLong(): Pair<Double, Double> {
val location = client.lastLocation.await() ?: getAFreshLocation()
return location?.getLatLonPair() ?: throw IOException("Unable to get location")
}
override fun getLatLongFromLocationName(location: String): Pair<Double, Double> {
val locations = geoCoder.getFromLocationName(location, 1)
locations?.takeIf { it.isNotEmpty() }?.get(0)?.let {
return Pair(it.latitude, it.longitude)
}
throw IOException("No location found")
}
override suspend fun getLocationNameFromLatLong(
lat: Double, long: Double, type: LocationType
): String {
val address = getAddressFromLatLong(lat, long) ?: return "$lat $long"
return when (type) {
LocationType.Town -> {
val location = address
.municipalitySubdivision
?.takeIf { it.isNotBlank() }
?: address.municipality
location ?: throw IOException("No location municipalitySubdivision or municipality")
}
LocationType.City -> {
address.municipality ?: throw IOException("No location municipality")
}
}
}
@SuppressLint("MissingPermission")
private suspend fun getAFreshLocation(): Location? {
return client.getCurrentLocation(PRIORITY_HIGH_ACCURACY, object : CancellationToken() {
override fun isCancellationRequested(): Boolean = false
override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken = this
}).await()
}
@SuppressLint("MissingPermission")
private suspend fun requestFreshLocation(): Location? {
val handlerThread = HandlerThread("MyHandlerThread")
handlerThread.start()
// Now get the Looper from the HandlerThread
// NOTE: This call will block until the HandlerThread gets control and initializes its Looper
// Now get the Looper from the HandlerThread
// NOTE: This call will block until the HandlerThread gets control and initializes its Looper
val looper = handlerThread.looper
return suspendCoroutine { cont ->
val callback = object : LocationCallback() {
override fun onLocationResult(p0: LocationResult?) {
client.removeLocationUpdates(this)
cont.resume(p0?.lastLocation)
}
}
with(locationManager!!) {
when {
isProviderEnabled(LocationManager.GPS_PROVIDER) -> {
client.requestLocationUpdates(createLocationRequest(PRIORITY_HIGH_ACCURACY), callback, looper)
}
isProviderEnabled(LocationManager.NETWORK_PROVIDER) -> {
client.requestLocationUpdates(createLocationRequest(PRIORITY_LOW_POWER), callback, looper)
}
else -> {
cont.resume(null)
}
}
}
}
}
private fun createLocationRequest(priority: Int) = LocationRequest.create()
.setPriority(priority)
.setNumUpdates(1)
.setExpirationDuration(1000)
}

View File

@@ -2,11 +2,11 @@ package com.appttude.h_mal.atlas_weather.data.network
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.buildOkHttpClient
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.createRetrofit
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
@@ -17,34 +17,30 @@ interface WeatherApi {
suspend fun getFromApi( suspend fun getFromApi(
@Query("lat") query: String, @Query("lat") query: String,
@Query("lon") lon: String, @Query("lon") lon: String,
@Query("exclude") exclude: String = "hourly,minutely", @Query("exclude") exclude: String = "minutely",
@Query("units") units: String = "metric" @Query("units") units: String = "metric"
): Response<WeatherResponse> ): Response<WeatherResponse>
// invoke method creating an invocation of the api call // invoke method creating an invocation of the api call
companion object{ companion object{
operator fun invoke( operator fun invoke(
// injected @params baseUrl: String,
networkConnectionInterceptor: NetworkConnectionInterceptor, networkConnectionInterceptor: NetworkConnectionInterceptor,
queryParamsInterceptor: QueryParamsInterceptor queryParamsInterceptor: QueryParamsInterceptor,
loggingInterceptor: HttpLoggingInterceptor
) : WeatherApi { ) : WeatherApi {
val okHttpClient = buildOkHttpClient(
networkConnectionInterceptor,
queryParamsInterceptor,
loggingInterceptor
)
val baseUrl = "https://api.openweathermap.org/data/2.5/" return createRetrofit(
baseUrl,
// okHttpClient with interceptors okHttpClient,
val okkHttpclient = OkHttpClient.Builder() WeatherApi::class.java
.addNetworkInterceptor(networkConnectionInterceptor) )
.addInterceptor(queryParamsInterceptor)
.build()
// creation of retrofit class
return Retrofit.Builder()
.client(okkHttpclient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(WeatherApi::class.java)
} }
} }
} }

View File

@@ -1,8 +1,7 @@
package com.appttude.h_mal.atlas_weather.data.network.interceptors package com.appttude.h_mal.atlas_weather.data.network.interceptors
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import com.appttude.h_mal.atlas_weather.utils.isInternetAvailable
import android.net.NetworkCapabilities
import okhttp3.Interceptor import okhttp3.Interceptor
import java.io.IOException import java.io.IOException
@@ -13,26 +12,10 @@ class NetworkConnectionInterceptor(
private val applicationContext = context.applicationContext private val applicationContext = context.applicationContext
override fun intercept(chain: Interceptor.Chain): okhttp3.Response { override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
if (!isInternetAvailable()){ if (!isInternetAvailable(applicationContext)){
throw IOException("Make sure you have an active data connection") throw IOException("Make sure you have an active data connection")
} }
return chain.proceed(chain.request()) return chain.proceed(chain.request())
} }
private fun isInternetAvailable(): Boolean {
var result = false
val connectivityManager =
applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
result = when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
else -> false
}
}
}
return result
}
} }

View File

@@ -14,16 +14,14 @@ class QueryParamsInterceptor : Interceptor{
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request() val original = chain.request()
val originalHttpUrl = original.url() val originalHttpUrl = original.url
val url = originalHttpUrl.newBuilder() val url = originalHttpUrl.newBuilder()
.addQueryParameter("appid", id) .addQueryParameter("appid", id)
.build() .build()
// Request customization: add request headers // Request customization: add request headers
// Request customization: add request headers val requestBuilder= original.newBuilder().url(url)
val requestBuilder: Request.Builder = original.newBuilder()
.url(url)
val request: Request = requestBuilder.build() val request: Request = requestBuilder.build()
return chain.proceed(request) return chain.proceed(request)

View File

@@ -0,0 +1,46 @@
package com.appttude.h_mal.atlas_weather.data.network.networkUtils
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
fun buildOkHttpClient(
networkConnectionInterceptor: NetworkConnectionInterceptor,
vararg interceptor: Interceptor,
timeoutSeconds: Long = 30L
): OkHttpClient {
val builder = OkHttpClient.Builder()
interceptor.forEach {
builder.addInterceptor(it)
}
builder.addNetworkInterceptor(networkConnectionInterceptor)
.connectTimeout(timeoutSeconds, TimeUnit.SECONDS)
.writeTimeout(timeoutSeconds, TimeUnit.SECONDS)
.readTimeout(timeoutSeconds, TimeUnit.SECONDS)
return builder.build()
}
fun <T> createRetrofit(
baseUrl: String,
okHttpClient: OkHttpClient,
service: Class<T>
): T {
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(service)
}

View File

@@ -13,6 +13,9 @@ data class WeatherResponse(
@field:SerializedName("timezone_offset") @field:SerializedName("timezone_offset")
val timezoneOffset: Int? = null, val timezoneOffset: Int? = null,
@field:SerializedName("hourly")
val hourly: List<Hour>? = null,
@field:SerializedName("daily") @field:SerializedName("daily")
val daily: List<DailyItem>? = null, val daily: List<DailyItem>? = null,

View File

@@ -7,13 +7,15 @@ import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
interface Repository { interface Repository {
suspend fun getWeatherFromApi(lat: String, long: String): WeatherResponse suspend fun getWeatherFromApi(lat: String, long: String): WeatherResponse
suspend fun saveCurrentWeatherToRoom(locationId: String, weatherResponse: WeatherResponse) suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem)
suspend fun saveWeatherListToRoom(list: List<EntityItem>) suspend fun saveWeatherListToRoom(list: List<EntityItem>)
fun loadAllWeatherExceptCurrentFromRoom(): LiveData<List<EntityItem>> fun loadRoomWeatherLiveData(): LiveData<List<EntityItem>>
suspend fun loadWeatherList() : List<String>
fun loadCurrentWeatherFromRoom(id: String): LiveData<EntityItem> fun loadCurrentWeatherFromRoom(id: String): LiveData<EntityItem>
suspend fun loadSingleCurrentWeatherFromRoom(id: String): EntityItem suspend fun loadSingleCurrentWeatherFromRoom(id: String): EntityItem
fun isSearchValid(locationName: String): Boolean fun isSearchValid(locationName: String): Boolean
fun saveLastSavedAt(locationName: String) fun saveLastSavedAt(locationName: String)
suspend fun deleteSavedWeatherEntry(locationName: String): Boolean suspend fun deleteSavedWeatherEntry(locationName: String): Boolean
fun getSavedLocations(): List<String> fun getSavedLocations(): List<String>
suspend fun getSingleWeather(locationName: String): EntityItem
} }

View File

@@ -7,7 +7,7 @@ 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.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase 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.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
private const val FIVE_MINS = 300000L private const val FIVE_MINS = 300000L
class RepositoryImpl( class RepositoryImpl(
@@ -23,15 +23,8 @@ class RepositoryImpl(
return responseUnwrap { api.getFromApi(lat, long) } return responseUnwrap { api.getFromApi(lat, long) }
} }
override suspend fun saveCurrentWeatherToRoom( override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem){
locationId: String, db.getSimpleDao().upsertFullWeather(entityItem)
weatherResponse: WeatherResponse
){
val entity = EntityItem(
locationId,
FullWeather(weatherResponse)
)
db.getSimpleDao().upsertFullWeather(entity)
} }
override suspend fun saveWeatherListToRoom( override suspend fun saveWeatherListToRoom(
@@ -40,15 +33,26 @@ class RepositoryImpl(
db.getSimpleDao().upsertListOfFullWeather(list) db.getSimpleDao().upsertListOfFullWeather(list)
} }
override fun loadAllWeatherExceptCurrentFromRoom() = db.getSimpleDao().getAllFullWeatherWithoutCurrent() override fun loadRoomWeatherLiveData() = db.getSimpleDao().getAllFullWeatherWithoutCurrent()
override fun loadCurrentWeatherFromRoom(id: String) = db.getSimpleDao().getCurrentFullWeather(id) override suspend fun loadWeatherList() : List<String>{
return db.getSimpleDao()
.getWeatherListWithoutCurrent()
.map { it.id }
}
override suspend fun loadSingleCurrentWeatherFromRoom(id: String) = db.getSimpleDao().getCurrentFullWeatherSingle(id) override fun loadCurrentWeatherFromRoom(id: String)
= db.getSimpleDao().getCurrentFullWeather(id)
override suspend fun loadSingleCurrentWeatherFromRoom(id: String)
= db.getSimpleDao().getCurrentFullWeatherSingle(id)
override fun isSearchValid(locationName: String): Boolean { override fun isSearchValid(locationName: String): Boolean {
val lastSaved = prefs.getLastSavedAt(locationName) ?: return true val lastSaved = prefs
.getLastSavedAt("$LOCATION_CONST$locationName")
?: return true
val difference = System.currentTimeMillis() - lastSaved val difference = System.currentTimeMillis() - lastSaved
return difference > FIVE_MINS return difference > FIVE_MINS
} }
@@ -65,4 +69,8 @@ class RepositoryImpl(
return prefs.getAllKeys().toList() return prefs.getAllKeys().toList()
} }
override suspend fun getSingleWeather(locationName: String): EntityItem {
return db.getSimpleDao().getCurrentFullWeatherSingle(locationName)
}
} }

View File

@@ -3,7 +3,7 @@ package com.appttude.h_mal.atlas_weather.data.repository
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
class SettingsRepositoryImpl( class SettingsRepositoryImpl(
val prefs: PreferenceProvider private val prefs: PreferenceProvider
) : SettingsRepository{ ) : SettingsRepository{
override fun isNotificationsEnabled(): Boolean = prefs.isNotificationsEnabled() override fun isNotificationsEnabled(): Boolean = prefs.isNotificationsEnabled()

View File

@@ -1,6 +1,5 @@
package com.appttude.h_mal.atlas_weather.data.room package com.appttude.h_mal.atlas_weather.data.room
import android.text.BoringLayout
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
@@ -27,6 +26,9 @@ interface WeatherDao {
@Query("SELECT * FROM EntityItem WHERE id != :id") @Query("SELECT * FROM EntityItem WHERE id != :id")
fun getAllFullWeatherWithoutCurrent(id: String = CURRENT_LOCATION) : LiveData<List<EntityItem>> fun getAllFullWeatherWithoutCurrent(id: String = CURRENT_LOCATION) : LiveData<List<EntityItem>>
@Query("SELECT * FROM EntityItem WHERE id != :id")
suspend fun getWeatherListWithoutCurrent(id: String = CURRENT_LOCATION) : List<EntityItem>
@Query("DELETE FROM EntityItem WHERE id = :userId") @Query("DELETE FROM EntityItem WHERE id = :userId")
suspend fun deleteEntry(userId: String): Int suspend fun deleteEntry(userId: String): Int

View File

@@ -2,61 +2,55 @@ package com.appttude.h_mal.atlas_weather.helper
import android.Manifest import android.Manifest
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider 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.Repository
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository 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.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData 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.WidgetData
import com.appttude.h_mal.atlas_weather.utils.toSmallDayName import com.appttude.h_mal.atlas_weather.utils.toSmallDayName
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
import java.net.URL import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ServicesHelper( class ServicesHelper(
private val repository: Repository, private val repository: Repository,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val locationProvider: LocationProvider private val locationProvider: LocationProvider
) { ){
@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) @RequiresPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
suspend fun getData(): FullWeather? {
return try {
val latLon = locationProvider.getLatLong()
val result =
repository.getWeatherFromApi(
latLon.first.toString(),
latLon.second.toString()
)
FullWeather(result)
} catch (e: Exception) {
null
}
}
@RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION)
suspend fun fetchData(): Boolean { suspend fun fetchData(): Boolean {
if (!repository.isSearchValid(CURRENT_LOCATION)) return true if (!repository.isSearchValid(CURRENT_LOCATION)) return false
return try { return try {
// Get location // Get location
val latLong = locationProvider.getLatLong() val latLong = locationProvider.getCurrentLatLong()
// Get weather from api // Get weather from api
val weather = repository val weather = repository
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString()) .getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
val currentLocation = locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
// Save data if not null val fullWeather = FullWeather(weather).apply {
weather.let { temperatureUnit = "°C"
repository.saveLastSavedAt(CURRENT_LOCATION) locationString = currentLocation
repository.saveCurrentWeatherToRoom(CURRENT_LOCATION, it)
} }
false val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
// Save data if not null
repository.saveLastSavedAt(CURRENT_LOCATION)
repository.saveCurrentWeatherToRoom(entityItem)
true
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace()
false false
} }
} }
@@ -66,50 +60,59 @@ class ServicesHelper(
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION) val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
result.weather.let { result.weather.let {
WidgetData( val bitmap = it.current?.icon
locationProvider.getLocationName(it.lat, it.lon), val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon)
getBitmapFromUrl(it.daily?.get(0)?.icon), val temp = it.current?.temp?.toInt().toString()
it.current?.temp?.toInt().toString()
) WidgetData(location, bitmap, temp)
} }
} catch (e: Exception) { null } } catch (e: Exception) {
null
}
} }
suspend fun getWidgetInnerWeather(): List<InnerWidgetData>? { suspend fun getWidgetInnerWeather(): List<InnerWidgetData>? {
return try { return try {
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION) val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
val list = mutableListOf<InnerWidgetData>()
result.weather.daily?.drop(1)?.dropLast(2)?.map{ result.weather.daily?.drop(1)?.dropLast(2)?.forEach { dailyWeather ->
InnerWidgetData( val day = dailyWeather.dt?.toSmallDayName()
it.dt?.toSmallDayName(), val bitmap = withContext(Dispatchers.Main) {
getBitmapFromUrl(it.icon), getBitmapFromUrl(dailyWeather.icon)
it.max?.toInt().toString() }
) val temp = dailyWeather.max?.toInt().toString()
val item = InnerWidgetData(day, bitmap, temp)
list.add(item)
} }
} catch (e: Exception) { null } list.toList()
} } catch (e: Exception) {
private fun getBitmapFromUrl(imageAddress: String?): Bitmap? {
return try {
val url = URL(imageAddress)
BitmapFactory.decodeStream(url.openConnection().getInputStream())
} catch (e: IOException) {
e.printStackTrace()
null null
} }
} }
fun isEnabled() = settingsRepository.isNotificationsEnabled() private suspend fun getBitmapFromUrl(imageAddress: String?): Bitmap? {
return suspendCoroutine { cont ->
Picasso.get().load(imageAddress).into(object : Target {
override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
cont.resume(bitmap)
}
fun getWidgetBackground(): Int { override fun onBitmapFailed(e: Exception?, d: Drawable?) {
return if (settingsRepository.isBlackBackground()){ cont.resume(null)
Color.BLACK }
}else{
Color.TRANSPARENT override fun onPrepareLoad(placeHolderDrawable: Drawable?) {}
})
} }
} }
fun getWidgetBackground(): Int {
fun setFirstTimer() = settingsRepository.setFirstTime() return if (settingsRepository.isBlackBackground()) {
Color.BLACK
} else {
Color.TRANSPARENT
}
}
} }

View File

@@ -3,6 +3,7 @@ package com.appttude.h_mal.atlas_weather.model.forecast
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.model.weather.DailyWeather import com.appttude.h_mal.atlas_weather.model.weather.DailyWeather
import com.appttude.h_mal.atlas_weather.utils.parcelableCreator
import com.appttude.h_mal.atlas_weather.utils.toDayName import com.appttude.h_mal.atlas_weather.utils.toDayName
import com.appttude.h_mal.atlas_weather.utils.toDayString import com.appttude.h_mal.atlas_weather.utils.toDayString
import com.appttude.h_mal.atlas_weather.utils.toTime import com.appttude.h_mal.atlas_weather.utils.toTime
@@ -20,27 +21,10 @@ data class Forecast(
val humidity: String?, val humidity: String?,
val uvi: String?, val uvi: String?,
val sunrise: String?, val sunrise: String?,
val sunset: String? val sunset: String?,
val cloud: String?
): Parcelable { ): Parcelable {
constructor(dailyWeather: DailyWeather) : this(
dailyWeather.dt?.toDayString(),
dailyWeather.dt?.toDayName(),
dailyWeather.description,
dailyWeather.icon,
dailyWeather.max?.toInt().toString(),
dailyWeather.min?.toInt().toString(),
dailyWeather.average?.toInt().toString(),
dailyWeather.windSpeed?.toInt().toString(),
(dailyWeather.pop?.times(100)).toString(),
dailyWeather.humidity?.toString(),
dailyWeather.uvi?.toInt().toString(),
dailyWeather.sunrise?.toTime(),
dailyWeather.sunset?.toTime()
)
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readString(), parcel.readString(),
parcel.readString(), parcel.readString(),
@@ -54,8 +38,25 @@ data class Forecast(
parcel.readString(), parcel.readString(),
parcel.readString(), parcel.readString(),
parcel.readString(), parcel.readString(),
parcel.readString()) { parcel.readString(),
} parcel.readString())
constructor(dailyWeather: DailyWeather) : this(
dailyWeather.dt?.toDayString(),
dailyWeather.dt?.toDayName(),
dailyWeather.description,
dailyWeather.icon,
dailyWeather.max?.toInt().toString(),
dailyWeather.min?.toInt().toString(),
dailyWeather.average?.toInt().toString(),
dailyWeather.windSpeed?.toInt().toString(),
(dailyWeather.pop?.times(100))?.toInt().toString(),
dailyWeather.humidity?.toString(),
dailyWeather.uvi?.toInt().toString(),
dailyWeather.sunrise?.toTime(),
dailyWeather.sunset?.toTime(),
dailyWeather.clouds?.toString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(date) parcel.writeString(date)
@@ -77,13 +78,7 @@ data class Forecast(
return 0 return 0
} }
companion object CREATOR : Parcelable.Creator<Forecast> { companion object{
override fun createFromParcel(parcel: Parcel): Forecast { @JvmField val CREATOR = parcelableCreator(::Forecast)
return Forecast(parcel)
}
override fun newArray(size: Int): Array<Forecast?> {
return arrayOfNulls(size)
}
} }
} }

View File

@@ -1,8 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.forecast package com.appttude.h_mal.atlas_weather.model.forecast
import android.os.Parcel import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import android.os.Parcelable import com.appttude.h_mal.atlas_weather.model.weather.Hour
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
data class WeatherDisplay( data class WeatherDisplay(
@@ -11,6 +10,7 @@ data class WeatherDisplay(
var location: String?, var location: String?,
val iconURL: String?, val iconURL: String?,
val description: String?, val description: String?,
val hourly: List<Hour>?,
val forecast: List<Forecast>?, val forecast: List<Forecast>?,
val windSpeed: String?, val windSpeed: String?,
val windDirection: String?, val windDirection: String?,
@@ -18,65 +18,26 @@ data class WeatherDisplay(
val humidity: String?, val humidity: String?,
val clouds: String?, val clouds: String?,
val lat: Double = 0.00, val lat: Double = 0.00,
val lon: Double = 0.00 val lon: Double = 0.00,
): Parcelable{ var displayName: String?
){
constructor(parcel: Parcel) : this( constructor(entity: EntityItem) : this(
parcel.readValue(Double::class.java.classLoader) as? Double, entity.weather.current?.temp,
parcel.readString(), entity.weather.temperatureUnit,
parcel.readString(), entity.id,
parcel.readString(), entity.weather.current?.icon,
parcel.readString(), entity.weather.current?.description,
parcel.createTypedArrayList(Forecast), entity.weather.hourly,
parcel.readString(), entity.weather.daily?.drop(1)?.map { Forecast(it) },
parcel.readString(), entity.weather.current?.windSpeed?.toString(),
parcel.readString(), entity.weather.current?.windDeg?.toString(),
parcel.readString(), entity.weather.daily?.get(0)?.pop?.times(100)?.toInt()?.toString(),
parcel.readString()) { entity.weather.current?.humidity?.toString(),
} entity.weather.current?.clouds?.toString(),
entity.weather.lat,
constructor(weather: FullWeather) : this( entity.weather.lon,
weather.current?.temp, entity.weather.locationString
null,
null,
weather.current?.icon,
weather.current?.description,
weather.daily?.drop(1)?.map { Forecast(it) },
weather.current?.windSpeed?.toString(),
weather.current?.windDeg?.toString(),
weather.daily?.get(0)?.pop?.times(100)?.toString(),
weather.current?.humidity?.toString(),
weather.current?.clouds?.toString(),
weather.lat,
weather.lon
) )
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeValue(averageTemp)
parcel.writeString(unit)
parcel.writeString(location)
parcel.writeString(iconURL)
parcel.writeString(description)
parcel.writeTypedList(forecast)
parcel.writeString(windSpeed)
parcel.writeString(windDirection)
parcel.writeString(precipitation)
parcel.writeString(humidity)
parcel.writeString(clouds)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<WeatherDisplay> {
override fun createFromParcel(parcel: Parcel): WeatherDisplay {
return WeatherDisplay(parcel)
}
override fun newArray(size: Int): Array<WeatherDisplay?> {
return arrayOfNulls(size)
}
}
} }

View File

@@ -0,0 +1,6 @@
package com.appttude.h_mal.atlas_weather.model.types
enum class LocationType{
City,
Town
}

View File

@@ -6,15 +6,19 @@ data class FullWeather(
val current: Current? = null, val current: Current? = null,
val timezone: String? = null, val timezone: String? = null,
val timezoneOffset: Int? = null, val timezoneOffset: Int? = null,
val hourly: List<Hour>? = null,
val daily: List<DailyWeather>? = null, val daily: List<DailyWeather>? = null,
val lon: Double = 0.00, val lon: Double = 0.00,
val lat: Double = 0.00 val lat: Double = 0.00,
var locationString: String? = null,
var temperatureUnit: String? = null
) { ) {
constructor(weatherResponse: WeatherResponse): this( constructor(weatherResponse: WeatherResponse): this(
weatherResponse.current?.let { Current(it) }, weatherResponse.current?.let { Current(it) },
weatherResponse.timezone, weatherResponse.timezone,
weatherResponse.timezoneOffset, weatherResponse.timezoneOffset,
weatherResponse.hourly?.subList(0,23)?.map { Hour(it) },
weatherResponse.daily?.map { DailyWeather(it) }, weatherResponse.daily?.map { DailyWeather(it) },
weatherResponse.lon, weatherResponse.lon,
weatherResponse.lat weatherResponse.lat

View File

@@ -1,78 +1,15 @@
package com.appttude.h_mal.atlas_weather.model.widget package com.appttude.h_mal.atlas_weather.model.widget
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Parcel
import android.os.Parcelable
data class WidgetData( data class WidgetData(
val location: String?, val location: String?,
val icon: Bitmap?, val icon: String?,
val currentTemp: String?, val currentTemp: String?
val list: List<InnerWidgetData>? = listOf() )
):Parcelable{
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readParcelable(Bitmap::class.java.classLoader),
parcel.readString(),
parcel.createTypedArrayList(InnerWidgetData)
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(location)
parcel.writeParcelable(icon, flags)
parcel.writeString(currentTemp)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<WidgetData> {
override fun createFromParcel(parcel: Parcel): WidgetData {
return WidgetData(parcel)
}
override fun newArray(size: Int): Array<WidgetData?> {
return arrayOfNulls(size)
}
}
}
data class InnerWidgetData( data class InnerWidgetData(
val date: String?, val date: String?,
val icon: Bitmap?, val icon: Bitmap?,
val highTemp: String? val highTemp: String?
):Parcelable{ )
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readParcelable(Bitmap::class.java.classLoader),
parcel.readString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(date)
parcel.writeParcelable(icon, flags)
parcel.writeString(highTemp)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<InnerWidgetData> {
override fun createFromParcel(parcel: Parcel): InnerWidgetData {
return InnerWidgetData(parcel)
}
override fun newArray(size: Int): Array<InnerWidgetData?> {
return arrayOfNulls(size)
}
}
}
interface WidgetDataImplementation{
fun getWidgetData(): WidgetData
}

View File

@@ -0,0 +1,58 @@
package com.appttude.h_mal.atlas_weather.utils
import android.os.Parcel
import android.os.Parcelable
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
inline fun <reified T> parcelableCreator(
crossinline create: (Parcel) -> T) =
object : Parcelable.Creator<T> {
override fun createFromParcel(source: Parcel) = create(source)
override fun newArray(size: Int) = arrayOfNulls<T>(size)
}
suspend fun <T : Any?> tryOrNullSuspended(
call: suspend () -> T?
): T? {
return try {
call.invoke()
}catch (e: Exception){
e.printStackTrace()
null
}
}
fun <T : Any?> tryOrNull(
call: () -> T?
): T? {
return try {
call.invoke()
}catch (e: Exception){
e.printStackTrace()
null
}
}
/**
* Creates a suspend function
* @param call - the body of the lambda
*
* @sample
* fun getNumber() = 2
* suspend fun getSuspendNumber{ getNumber() }
*
* Both equal 2.
*/
suspend fun <T: Any> createSuspend(
call: () -> T?
): T?{
return suspendCoroutine { cont ->
cont.resume(call())
}
}

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.atlas_weather.utils
fun printToLog(msg: String) {
println("widget monitoring: $msg")
}

View File

@@ -0,0 +1,23 @@
package com.appttude.h_mal.atlas_weather.utils
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
fun isInternetAvailable(
context: Context
): Boolean {
var result = false
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
connectivityManager?.let {
it.getNetworkCapabilities(connectivityManager.activeNetwork)?.apply {
result = when {
hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
else -> false
}
}
}
return result
}

View File

@@ -4,11 +4,9 @@ package com.appttude.h_mal.atlas_weather.utils
import android.os.Build import android.os.Build
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Instant import java.time.Instant
import java.time.LocalDate
import java.time.OffsetTime import java.time.OffsetTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.* import java.util.*
fun Int.toDayString(): String { fun Int.toDayString(): String {

View File

@@ -0,0 +1,12 @@
package com.appttude.h_mal.atlas_weather.utils
fun generateIconUrlString(icon: String?): String?{
return icon?.let {
StringBuilder()
.append("https://openweathermap.org/img/wn/")
.append(it)
.append("@4x.png")
.toString()
}
}

View File

@@ -1,16 +1,16 @@
package com.appttude.h_mal.atlas_weather.utils package com.appttude.h_mal.atlas_weather.utils
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Resources
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.appttude.h_mal.atlas_weather.R import com.appttude.h_mal.atlas_weather.R
import com.squareup.picasso.Picasso import com.bumptech.glide.Glide
fun View.show() { fun View.show() {
this.visibility = View.VISIBLE this.visibility = View.VISIBLE
@@ -28,38 +28,24 @@ fun Fragment.displayToast(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
} }
fun ImageView.loadImage(url: String?){
Picasso.get()
.load(url)
.error(R.drawable.ic_baseline_cloud_off_24)
.into(this)
}
fun ViewGroup.generateView(layoutId: Int): View = LayoutInflater fun ViewGroup.generateView(layoutId: Int): View = LayoutInflater
.from(context) .from(context)
.inflate(layoutId, this, false) .inflate(layoutId, this, false)
fun ImageView.loadImage(url: String?, height: Int, width: Int){ fun ImageView.loadImage(url: String?){
Picasso.get() val c = Glide.with(this)
.load(url) .load(url)
.resize(width.dpToPx(), height.dpToPx()) viewTreeObserver.addOnPreDrawListener {
.centerCrop() c.override(width, height)
true
}
c.placeholder(R.drawable.ic_baseline_cloud_queue_24)
.error(R.drawable.ic_baseline_cloud_off_24) .error(R.drawable.ic_baseline_cloud_off_24)
.fitCenter()
.into(this) .into(this)
} }
fun Int.dpToPx(): Int = (this * Resources.getSystem().displayMetrics.density).toInt() fun Fragment.hideKeyboard() {
val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?
fun SearchView.onSubmitListener(searchSubmit: (String) -> Unit) { imm?.hideSoftInputFromWindow(view?.windowToken, 0)
this.setOnQueryTextListener(object :
SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(s: String): Boolean {
searchSubmit.invoke(s)
return true
}
override fun onQueryTextChange(s: String): Boolean {
return true
}
})
} }

View File

@@ -3,64 +3,63 @@ package com.appttude.h_mal.atlas_weather.viewmodel
import android.Manifest import android.Manifest
import androidx.annotation.RequiresPermission import androidx.annotation.RequiresPermission
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider 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.Repository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION 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.model.forecast.WeatherDisplay import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.model.weather.Current
import com.appttude.h_mal.atlas_weather.utils.Event import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.WeatherViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException
class MainViewModel( class MainViewModel(
private val locationProvider: LocationProvider, private val locationProvider: LocationProvider,
private val repository: Repository private val repository: Repository
): ViewModel(){ ): WeatherViewModel(){
val weatherLiveData = MutableLiveData<WeatherDisplay>() val weatherLiveData = MutableLiveData<WeatherDisplay>()
val operationState = MutableLiveData<Event<Boolean>>() val operationState = MutableLiveData<Event<Boolean>>()
val operationError = MutableLiveData<Event<String>>() val operationError = MutableLiveData<Event<String>>()
val operationRefresh = MutableLiveData<Event<Boolean>>()
init { init {
repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever { repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever {
it?.weather?.let { data -> it?.let {
val list= WeatherDisplay(data).apply { val weather = WeatherDisplay(it)
unit = "°C" weatherLiveData.postValue(weather)
location = locationProvider.getLocationName(data.lat, data.lon)
}
weatherLiveData.postValue(list)
} }
} }
} }
@RequiresPermission(value = Manifest.permission.ACCESS_FINE_LOCATION) @RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
fun fetchData(){ fun fetchData(){
if (!repository.isSearchValid(CURRENT_LOCATION)) return if (!repository.isSearchValid(CURRENT_LOCATION)){
operationRefresh.postValue(Event(false))
return
}
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
operationState.postValue(Event(true)) operationState.postValue(Event(true))
try { try {
// Get location // Get location
val latLong = locationProvider.getLatLong() val latLong = locationProvider.getCurrentLatLong()
// Get weather from api // Get weather from api
val weather = repository val weather = repository
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString()) .getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
val currentLocation = locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
val fullWeather = createFullWeather(weather, currentLocation)
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
// Save data if not null // Save data if not null
weather.let { repository.saveLastSavedAt(CURRENT_LOCATION)
repository.saveLastSavedAt(CURRENT_LOCATION) repository.saveCurrentWeatherToRoom(entityItem)
repository.saveCurrentWeatherToRoom(CURRENT_LOCATION, it) }catch (e: Exception){
return@launch
}
}catch (e: IOException){
operationError.postValue(Event(e.message!!)) operationError.postValue(Event(e.message!!))
}finally { }finally {
operationState.postValue(Event(false)) operationState.postValue(Event(false))
operationRefresh.postValue(Event(false))
} }
} }
} }

View File

@@ -1,20 +0,0 @@
package com.appttude.h_mal.atlas_weather.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
class MainViewModelFactory(
private val locationProvider: LocationProvider,
private val repository: RepositoryImpl
) : ViewModelProvider.Factory{
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
return (MainViewModel(locationProvider, repository)) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View File

@@ -1,50 +1,74 @@
package com.appttude.h_mal.atlas_weather.viewmodel package com.appttude.h_mal.atlas_weather.viewmodel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider 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.repository.Repository
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather import com.appttude.h_mal.atlas_weather.model.types.LocationType
import com.appttude.h_mal.atlas_weather.utils.Event import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.WeatherViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
const val ALL_LOADED = "all_loaded"
class WorldViewModel( class WorldViewModel(
private val locationProvider: LocationProvider, private val locationProvider: LocationProvider,
private val repository: Repository private val repository: Repository
) : ViewModel() { ) : WeatherViewModel() {
val weatherLiveData = MutableLiveData<List<WeatherDisplay>>() val weatherLiveData = MutableLiveData<List<WeatherDisplay>>()
val singleWeatherLiveData = MutableLiveData<WeatherDisplay>()
val operationState = MutableLiveData<Event<Boolean>>() val operationState = MutableLiveData<Event<Boolean>>()
val operationError = MutableLiveData<Event<String>>() val operationError = MutableLiveData<Event<String>>()
val operationRefresh = MutableLiveData<Event<Boolean>>()
val operationComplete = MutableLiveData<Event<String>>() val operationComplete = MutableLiveData<Event<String>>()
private val weatherListLiveData = repository.loadAllWeatherExceptCurrentFromRoom() private val weatherListLiveData = repository.loadRoomWeatherLiveData()
init { init {
weatherListLiveData.observeForever { weatherListLiveData.observeForever {
it.forEach { item->
println(item)
}
val list = it.map { data -> val list = it.map { data ->
WeatherDisplay(data.weather).apply { WeatherDisplay(data)
unit = "°C"
location = locationProvider.getLocationName(data.weather.lat, data.weather.lon)
}
} }
weatherLiveData.postValue(list) weatherLiveData.postValue(list)
} }
} }
fun getSingleLocation(locationName: String){
CoroutineScope(Dispatchers.IO).launch {
val entity = repository.getSingleWeather(locationName)
val item = WeatherDisplay(entity)
singleWeatherLiveData.postValue(item)
}
}
fun fetchDataForSingleLocation(locationName: String) { fun fetchDataForSingleLocation(locationName: String) {
if (!repository.isSearchValid(locationName)){
operationRefresh.postValue(Event(false))
return
}
CoroutineScope(Dispatchers.IO).launch {
operationState.postValue(Event(true))
try {
val weatherEntity = createWeatherEntity(locationName)
repository.saveCurrentWeatherToRoom(weatherEntity)
repository.saveLastSavedAt(locationName)
} catch (e: IOException) {
operationError.postValue(Event(e.message!!))
} finally {
operationState.postValue(Event(false))
operationRefresh.postValue(Event(false))
}
}
}
fun fetchDataForSingleLocationSearch(locationName: String) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
operationState.postValue(Event(true)) operationState.postValue(Event(true))
// Check if location exists // Check if location exists
@@ -54,29 +78,20 @@ class WorldViewModel(
} }
try { try {
// Get location
val latLong =
locationProvider.getLatLongFromLocationString(locationName)
val lat = latLong.first
val lon = latLong.second
// Get weather from api // Get weather from api
val weather = repository val entityItem = createWeatherEntity(locationName)
.getWeatherFromApi(lat.toString(), lon.toString())
// retrieved location name // retrieved location name
val retrievedLocation = locationProvider.getLocationName(weather.lat, weather.lon) val retrievedLocation = locationProvider.getLocationNameFromLatLong(entityItem.weather.lat, entityItem.weather.lon, LocationType.City)
if (repository.getSavedLocations().contains(retrievedLocation)){ if (repository.getSavedLocations().contains(retrievedLocation)){
operationError.postValue(Event("$retrievedLocation already exists")) operationError.postValue(Event("$retrievedLocation already exists"))
return@launch return@launch
} }
// Save data if not null // Save data if not null
weather.let { repository.saveCurrentWeatherToRoom(entityItem)
repository.saveCurrentWeatherToRoom(retrievedLocation, it) repository.saveLastSavedAt(retrievedLocation)
operationComplete.postValue(Event("$retrievedLocation saved")) operationComplete.postValue(Event("$retrievedLocation saved"))
return@launch
}
} catch (e: IOException) { } catch (e: IOException) {
operationError.postValue(Event(e.message!!)) operationError.postValue(Event(e.message!!))
} finally { } finally {
@@ -86,27 +101,26 @@ class WorldViewModel(
} }
fun fetchAllLocations() { fun fetchAllLocations() {
if (!repository.isSearchValid(ALL_LOADED)){
operationRefresh.postValue(Event(false))
return
}
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
operationState.postValue(Event(true)) operationState.postValue(Event(true))
try { try {
val list = mutableListOf<EntityItem>() val list = mutableListOf<EntityItem>()
weatherLiveData.value?.forEach { weatherItem -> repository.loadWeatherList().forEach { locationName ->
// If search not valid move onto next in loop // If search not valid move onto next in loop
weatherItem.location?.let { if (!repository.isSearchValid(locationName)) return@forEach
if (!repository.isSearchValid(it)) return@forEach
}
try { try {
val weather = repository val entity = createWeatherEntity(locationName)
.getWeatherFromApi(weatherItem.lat.toString(), weatherItem.lon.toString()) list.add(entity)
repository.saveLastSavedAt(locationName)
weatherItem.location?.let { loc ->
repository.saveLastSavedAt(loc)
list.add(EntityItem(loc, FullWeather(weather)))
}
} catch (e: IOException) { } } catch (e: IOException) { }
} }
repository.saveWeatherListToRoom(list) repository.saveWeatherListToRoom(list)
repository.saveLastSavedAt(ALL_LOADED)
} catch (e: IOException) { } catch (e: IOException) {
operationError.postValue(Event(e.message!!)) operationError.postValue(Event(e.message!!))
} finally { } finally {
@@ -130,4 +144,22 @@ class WorldViewModel(
} }
} }
} }
private suspend fun getWeather(locationName: String): WeatherResponse {
// Get location
val latLong =
locationProvider.getLatLongFromLocationName(locationName)
val lat = latLong.first
val lon = latLong.second
// Get weather from api
return repository.getWeatherFromApi(lat.toString(), lon.toString())
}
private suspend fun createWeatherEntity(locationName: String): EntityItem {
val weather = getWeather(locationName)
val location = locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon, LocationType.City)
val fullWeather = createFullWeather(weather, location)
return createWeatherEntity(location,fullWeather)
}
} }

View File

@@ -1,20 +0,0 @@
package com.appttude.h_mal.atlas_weather.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
class WorldViewModelFactory(
private val locationProvider: LocationProvider,
private val repository: RepositoryImpl
) : ViewModelProvider.Factory{
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WorldViewModel::class.java)) {
return (WorldViewModel(locationProvider, repository)) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View File

@@ -0,0 +1,30 @@
package com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels
import androidx.lifecycle.ViewModel
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.model.weather.FullWeather
abstract class WeatherViewModel : ViewModel(){
fun createFullWeather(
weather: WeatherResponse,
location: String
): FullWeather {
return FullWeather(weather).apply {
temperatureUnit = "°C"
locationString = location
}
}
fun createWeatherEntity(
locationId: String,
weather: FullWeather
): EntityItem{
weather.apply {
locationString = locationId
}
return EntityItem(locationId, weather)
}
}

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="@color/colour_two"
android:centerColor="@color/colour_three"
android:endColor="@color/colour_four"
android:type="linear"
android:angle="45"/>
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="@color/colour_two"
android:centerColor="@color/colour_three"
android:endColor="@color/colour_four"
android:type="linear"
android:angle="45"/>
</shape>

View File

@@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF" <vector android:height="128dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="128dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4c-1.48,0 -2.85,0.43 -4.01,1.17l1.46,1.46C10.21,6.23 11.08,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3 0,1.13 -0.64,2.11 -1.56,2.62l1.45,1.45C23.16,18.16 24,16.68 24,15c0,-2.64 -2.05,-4.78 -4.65,-4.96zM3,5.27l2.75,2.74C2.56,8.15 0,10.77 0,14c0,3.31 2.69,6 6,6h11.73l2,2L21,20.73 4.27,4 3,5.27zM7.73,10l8,8H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h1.73z"/> <path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4c-1.48,0 -2.85,0.43 -4.01,1.17l1.46,1.46C10.21,6.23 11.08,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3 0,1.13 -0.64,2.11 -1.56,2.62l1.45,1.45C23.16,18.16 24,16.68 24,15c0,-2.64 -2.05,-4.78 -4.65,-4.96zM3,5.27l2.75,2.74C2.56,8.15 0,10.77 0,14c0,3.31 2.69,6 6,6h11.73l2,2L21,20.73 4.27,4 3,5.27zM7.73,10l8,8H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h1.73z"/>
</vector> </vector>

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="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM19,18H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h0.71C7.37,7.69 9.48,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3s-1.34,3 -3,3z"/>
</vector>

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="M11,15h2v2h-2v-2zM11,7h2v6h-2L11,7zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

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="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Some files were not shown because too many files have changed in this diff Show More