- weathered
4
.idea/assetWizardSettings.xml
generated
@@ -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>
|
||||||
|
|||||||
BIN
.idea/caches/build_file_checksums.ser
generated
7
.idea/dictionaries/h_mal.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="h_mal">
|
||||||
|
<words>
|
||||||
|
<w>upsert</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
5
.idea/jarRepositories.xml
generated
@@ -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>
|
||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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()) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) { null }
|
val temp = dailyWeather.max?.toInt().toString()
|
||||||
|
|
||||||
|
val item = InnerWidgetData(day, bitmap, temp)
|
||||||
|
list.add(item)
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBitmapFailed(e: Exception?, d: Drawable?) {
|
||||||
|
cont.resume(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getWidgetBackground(): Int {
|
fun getWidgetBackground(): Int {
|
||||||
return if (settingsRepository.isBlackBackground()){
|
return if (settingsRepository.isBlackBackground()) {
|
||||||
Color.BLACK
|
Color.BLACK
|
||||||
}else{
|
} else {
|
||||||
Color.TRANSPARENT
|
Color.TRANSPARENT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun setFirstTimer() = settingsRepository.setFirstTime()
|
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.model.types
|
||||||
|
|
||||||
|
enum class LocationType{
|
||||||
|
City,
|
||||||
|
Town
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.utils
|
||||||
|
|
||||||
|
fun printToLog(msg: String) {
|
||||||
|
println("widget monitoring: $msg")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
@@ -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(CURRENT_LOCATION, it)
|
repository.saveCurrentWeatherToRoom(entityItem)
|
||||||
return@launch
|
}catch (e: Exception){
|
||||||
}
|
|
||||||
}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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
5
app/src/main/res/drawable/ic_baseline_cloud_queue_24.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="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>
|
||||||
@@ -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>
|
||||||
5
app/src/main/res/drawable/ic_round_settings_24.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="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>
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |