- weathered
4
.idea/assetWizardSettings.xml
generated
@@ -378,7 +378,7 @@
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<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>
|
||||
</option>
|
||||
</PersistentState>
|
||||
@@ -389,7 +389,7 @@
|
||||
<option name="values">
|
||||
<map>
|
||||
<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" />
|
||||
</map>
|
||||
</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="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/extras/m2repository/" />
|
||||
</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>
|
||||
</project>
|
||||
@@ -12,12 +12,30 @@ android {
|
||||
applicationId "com.appttude.h_mal.atlas_weather"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 2
|
||||
versionName "2.0"
|
||||
versionCode 5
|
||||
versionName "3.0"
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
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 {
|
||||
release {
|
||||
@@ -58,6 +76,8 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -67,15 +87,19 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
implementation 'androidx.vectordrawable:vectordrawable:1.0.0'
|
||||
implementation "com.google.android.gms:play-services-location:16.0.0"
|
||||
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
|
||||
implementation "com.google.android.gms:play-services-location:18.0.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.2'
|
||||
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
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
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"
|
||||
implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
@@ -93,7 +117,7 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test:rules:1.3.0-rc01'
|
||||
|
||||
// Mockk
|
||||
def mockk_ver = "1.10.2"
|
||||
def mockk_ver = "1.10.5"
|
||||
testImplementation "io.mockk:mockk:$mockk_ver"
|
||||
androidTestImplementation "io.mockk:mockk-android:$mockk_ver"
|
||||
|
||||
@@ -101,6 +125,7 @@ dependencies {
|
||||
def retrofit_ver = "2.8.1"
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_ver"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"
|
||||
|
||||
// Shared prefs
|
||||
def prefs_ver = "1.1.1"
|
||||
@@ -119,4 +144,21 @@ dependencies {
|
||||
|
||||
// Picasso
|
||||
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) {
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,13 +57,13 @@ class HomeFragment : BaseFragment(), KodeinAware {
|
||||
adapter = recyclerAdapter
|
||||
}
|
||||
|
||||
getPermissionResult(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION_PERMISSION_REQUEST){
|
||||
getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){
|
||||
viewModel.fetchData()
|
||||
}
|
||||
|
||||
swipe_refresh.apply {
|
||||
setOnRefreshListener {
|
||||
getPermissionResult(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION_PERMISSION_REQUEST){
|
||||
getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){
|
||||
viewModel.fetchData()
|
||||
}
|
||||
isRefreshing = true
|
||||
|
||||
@@ -35,7 +35,7 @@ class NewAppWidget : BaseWidgetClass() {
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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.INTERNET" />
|
||||
<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 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.interceptors.NetworkConnectionInterceptor
|
||||
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.repository.RepositoryImpl
|
||||
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.viewmodel.ApplicationViewModelFactory
|
||||
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
|
||||
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
||||
import com.google.gson.Gson
|
||||
import org.kodein.di.Kodein
|
||||
import org.kodein.di.KodeinAware
|
||||
@@ -22,9 +23,10 @@ import org.kodein.di.generic.provider
|
||||
import org.kodein.di.generic.singleton
|
||||
|
||||
const val LOCATION_PERMISSION_REQUEST = 505
|
||||
|
||||
class AppClass : Application(), KodeinAware {
|
||||
|
||||
companion object{
|
||||
companion object {
|
||||
// idling resource to be used for espresso testing
|
||||
// when we need to wait for async operations to complete
|
||||
val idlingResources = CountingIdlingResource("Data_loader")
|
||||
@@ -35,15 +37,15 @@ class AppClass : Application(), KodeinAware {
|
||||
import(androidXModule(this@AppClass))
|
||||
|
||||
bind() from singleton { Gson() }
|
||||
|
||||
bind() from singleton { NetworkConnectionInterceptor(instance()) }
|
||||
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 { PreferenceProvider(instance()) }
|
||||
bind() from singleton { RepositoryImpl(instance(), instance(), 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 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
|
||||
|
||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
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")
|
||||
}
|
||||
import com.appttude.h_mal.atlas_weather.model.types.LocationType
|
||||
|
||||
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.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 okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
@@ -17,34 +17,30 @@ interface WeatherApi {
|
||||
suspend fun getFromApi(
|
||||
@Query("lat") query: String,
|
||||
@Query("lon") lon: String,
|
||||
@Query("exclude") exclude: String = "hourly,minutely",
|
||||
@Query("exclude") exclude: String = "minutely",
|
||||
@Query("units") units: String = "metric"
|
||||
): Response<WeatherResponse>
|
||||
|
||||
// invoke method creating an invocation of the api call
|
||||
companion object{
|
||||
operator fun invoke(
|
||||
// injected @params
|
||||
networkConnectionInterceptor: NetworkConnectionInterceptor,
|
||||
queryParamsInterceptor: QueryParamsInterceptor
|
||||
baseUrl: String,
|
||||
networkConnectionInterceptor: NetworkConnectionInterceptor,
|
||||
queryParamsInterceptor: QueryParamsInterceptor,
|
||||
loggingInterceptor: HttpLoggingInterceptor
|
||||
) : WeatherApi {
|
||||
|
||||
val okHttpClient = buildOkHttpClient(
|
||||
networkConnectionInterceptor,
|
||||
queryParamsInterceptor,
|
||||
loggingInterceptor
|
||||
)
|
||||
|
||||
val baseUrl = "https://api.openweathermap.org/data/2.5/"
|
||||
|
||||
// okHttpClient with interceptors
|
||||
val okkHttpclient = OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(networkConnectionInterceptor)
|
||||
.addInterceptor(queryParamsInterceptor)
|
||||
.build()
|
||||
|
||||
// creation of retrofit class
|
||||
return Retrofit.Builder()
|
||||
.client(okkHttpclient)
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(WeatherApi::class.java)
|
||||
return createRetrofit(
|
||||
baseUrl,
|
||||
okHttpClient,
|
||||
WeatherApi::class.java
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.appttude.h_mal.atlas_weather.data.network.interceptors
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import com.appttude.h_mal.atlas_weather.utils.isInternetAvailable
|
||||
import okhttp3.Interceptor
|
||||
import java.io.IOException
|
||||
|
||||
@@ -13,26 +12,10 @@ class NetworkConnectionInterceptor(
|
||||
private val applicationContext = context.applicationContext
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
|
||||
if (!isInternetAvailable()){
|
||||
if (!isInternetAvailable(applicationContext)){
|
||||
throw IOException("Make sure you have an active data connection")
|
||||
}
|
||||
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 {
|
||||
val original = chain.request()
|
||||
val originalHttpUrl = original.url()
|
||||
val originalHttpUrl = original.url
|
||||
|
||||
val url = originalHttpUrl.newBuilder()
|
||||
.addQueryParameter("appid", id)
|
||||
.build()
|
||||
|
||||
// Request customization: add request headers
|
||||
// Request customization: add request headers
|
||||
val requestBuilder: Request.Builder = original.newBuilder()
|
||||
.url(url)
|
||||
val requestBuilder= original.newBuilder().url(url)
|
||||
|
||||
val request: Request = requestBuilder.build()
|
||||
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")
|
||||
val timezoneOffset: Int? = null,
|
||||
|
||||
@field:SerializedName("hourly")
|
||||
val hourly: List<Hour>? = null,
|
||||
|
||||
@field:SerializedName("daily")
|
||||
val daily: List<DailyItem>? = null,
|
||||
|
||||
|
||||
@@ -7,13 +7,15 @@ import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
interface Repository {
|
||||
|
||||
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>)
|
||||
fun loadAllWeatherExceptCurrentFromRoom(): LiveData<List<EntityItem>>
|
||||
fun loadRoomWeatherLiveData(): LiveData<List<EntityItem>>
|
||||
suspend fun loadWeatherList() : List<String>
|
||||
fun loadCurrentWeatherFromRoom(id: String): LiveData<EntityItem>
|
||||
suspend fun loadSingleCurrentWeatherFromRoom(id: String): EntityItem
|
||||
fun isSearchValid(locationName: String): Boolean
|
||||
fun saveLastSavedAt(locationName: String)
|
||||
suspend fun deleteSavedWeatherEntry(locationName: String): Boolean
|
||||
fun getSavedLocations(): List<String>
|
||||
suspend fun getSingleWeather(locationName: String): EntityItem
|
||||
}
|
||||
@@ -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.room.AppDatabase
|
||||
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
|
||||
class RepositoryImpl(
|
||||
@@ -23,15 +23,8 @@ class RepositoryImpl(
|
||||
return responseUnwrap { api.getFromApi(lat, long) }
|
||||
}
|
||||
|
||||
override suspend fun saveCurrentWeatherToRoom(
|
||||
locationId: String,
|
||||
weatherResponse: WeatherResponse
|
||||
){
|
||||
val entity = EntityItem(
|
||||
locationId,
|
||||
FullWeather(weatherResponse)
|
||||
)
|
||||
db.getSimpleDao().upsertFullWeather(entity)
|
||||
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem){
|
||||
db.getSimpleDao().upsertFullWeather(entityItem)
|
||||
}
|
||||
|
||||
override suspend fun saveWeatherListToRoom(
|
||||
@@ -40,15 +33,26 @@ class RepositoryImpl(
|
||||
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 {
|
||||
val lastSaved = prefs.getLastSavedAt(locationName) ?: return true
|
||||
val lastSaved = prefs
|
||||
.getLastSavedAt("$LOCATION_CONST$locationName")
|
||||
?: return true
|
||||
val difference = System.currentTimeMillis() - lastSaved
|
||||
|
||||
return difference > FIVE_MINS
|
||||
}
|
||||
|
||||
@@ -65,4 +69,8 @@ class RepositoryImpl(
|
||||
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
|
||||
|
||||
class SettingsRepositoryImpl(
|
||||
val prefs: PreferenceProvider
|
||||
private val prefs: PreferenceProvider
|
||||
) : SettingsRepository{
|
||||
|
||||
override fun isNotificationsEnabled(): Boolean = prefs.isNotificationsEnabled()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.appttude.h_mal.atlas_weather.data.room
|
||||
|
||||
import android.text.BoringLayout
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
@@ -27,6 +26,9 @@ interface WeatherDao {
|
||||
@Query("SELECT * FROM EntityItem WHERE id != :id")
|
||||
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")
|
||||
suspend fun deleteEntry(userId: String): Int
|
||||
|
||||
|
||||
@@ -2,61 +2,55 @@ package com.appttude.h_mal.atlas_weather.helper
|
||||
|
||||
import android.Manifest
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
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.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.net.URL
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
class ServicesHelper(
|
||||
private val repository: Repository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val locationProvider: LocationProvider
|
||||
) {
|
||||
){
|
||||
|
||||
@RequiresPermission(Manifest.permission.ACCESS_FINE_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)
|
||||
@RequiresPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
suspend fun fetchData(): Boolean {
|
||||
if (!repository.isSearchValid(CURRENT_LOCATION)) return true
|
||||
if (!repository.isSearchValid(CURRENT_LOCATION)) return false
|
||||
|
||||
return try {
|
||||
// Get location
|
||||
val latLong = locationProvider.getLatLong()
|
||||
val latLong = locationProvider.getCurrentLatLong()
|
||||
// Get weather from api
|
||||
val weather = repository
|
||||
.getWeatherFromApi(latLong.first.toString(), latLong.second.toString())
|
||||
|
||||
// Save data if not null
|
||||
weather.let {
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(CURRENT_LOCATION, it)
|
||||
val currentLocation = locationProvider.getLocationNameFromLatLong(weather.lat, weather.lon)
|
||||
val fullWeather = FullWeather(weather).apply {
|
||||
temperatureUnit = "°C"
|
||||
locationString = currentLocation
|
||||
}
|
||||
false
|
||||
val entityItem = EntityItem(CURRENT_LOCATION, fullWeather)
|
||||
// Save data if not null
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -66,50 +60,59 @@ class ServicesHelper(
|
||||
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
|
||||
|
||||
result.weather.let {
|
||||
WidgetData(
|
||||
locationProvider.getLocationName(it.lat, it.lon),
|
||||
getBitmapFromUrl(it.daily?.get(0)?.icon),
|
||||
it.current?.temp?.toInt().toString()
|
||||
)
|
||||
val bitmap = it.current?.icon
|
||||
val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon)
|
||||
val temp = it.current?.temp?.toInt().toString()
|
||||
|
||||
WidgetData(location, bitmap, temp)
|
||||
}
|
||||
} catch (e: Exception) { null }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getWidgetInnerWeather(): List<InnerWidgetData>? {
|
||||
return try {
|
||||
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
|
||||
val list = mutableListOf<InnerWidgetData>()
|
||||
|
||||
result.weather.daily?.drop(1)?.dropLast(2)?.map{
|
||||
InnerWidgetData(
|
||||
it.dt?.toSmallDayName(),
|
||||
getBitmapFromUrl(it.icon),
|
||||
it.max?.toInt().toString()
|
||||
)
|
||||
result.weather.daily?.drop(1)?.dropLast(2)?.forEach { dailyWeather ->
|
||||
val day = dailyWeather.dt?.toSmallDayName()
|
||||
val bitmap = withContext(Dispatchers.Main) {
|
||||
getBitmapFromUrl(dailyWeather.icon)
|
||||
}
|
||||
val temp = dailyWeather.max?.toInt().toString()
|
||||
|
||||
val item = InnerWidgetData(day, bitmap, temp)
|
||||
list.add(item)
|
||||
}
|
||||
} catch (e: Exception) { null }
|
||||
}
|
||||
|
||||
|
||||
private fun getBitmapFromUrl(imageAddress: String?): Bitmap? {
|
||||
return try {
|
||||
val url = URL(imageAddress)
|
||||
BitmapFactory.decodeStream(url.openConnection().getInputStream())
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
list.toList()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun isEnabled() = settingsRepository.isNotificationsEnabled()
|
||||
private suspend fun getBitmapFromUrl(imageAddress: String?): Bitmap? {
|
||||
return suspendCoroutine { cont ->
|
||||
Picasso.get().load(imageAddress).into(object : Target {
|
||||
override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
|
||||
cont.resume(bitmap)
|
||||
}
|
||||
|
||||
fun getWidgetBackground(): Int {
|
||||
return if (settingsRepository.isBlackBackground()){
|
||||
Color.BLACK
|
||||
}else{
|
||||
Color.TRANSPARENT
|
||||
override fun onBitmapFailed(e: Exception?, d: Drawable?) {
|
||||
cont.resume(null)
|
||||
}
|
||||
|
||||
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun setFirstTimer() = settingsRepository.setFirstTime()
|
||||
fun getWidgetBackground(): Int {
|
||||
return if (settingsRepository.isBlackBackground()) {
|
||||
Color.BLACK
|
||||
} else {
|
||||
Color.TRANSPARENT
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.appttude.h_mal.atlas_weather.model.forecast
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.appttude.h_mal.atlas_weather.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.toDayString
|
||||
import com.appttude.h_mal.atlas_weather.utils.toTime
|
||||
@@ -20,27 +21,10 @@ data class Forecast(
|
||||
val humidity: String?,
|
||||
val uvi: String?,
|
||||
val sunrise: String?,
|
||||
val sunset: String?
|
||||
val sunset: String?,
|
||||
val cloud: String?
|
||||
): 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(
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
@@ -54,8 +38,25 @@ data class Forecast(
|
||||
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) {
|
||||
parcel.writeString(date)
|
||||
@@ -77,13 +78,7 @@ data class Forecast(
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Forecast> {
|
||||
override fun createFromParcel(parcel: Parcel): Forecast {
|
||||
return Forecast(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Forecast?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
companion object{
|
||||
@JvmField val CREATOR = parcelableCreator(::Forecast)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package com.appttude.h_mal.atlas_weather.model.forecast
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.model.weather.Hour
|
||||
|
||||
|
||||
data class WeatherDisplay(
|
||||
@@ -11,6 +10,7 @@ data class WeatherDisplay(
|
||||
var location: String?,
|
||||
val iconURL: String?,
|
||||
val description: String?,
|
||||
val hourly: List<Hour>?,
|
||||
val forecast: List<Forecast>?,
|
||||
val windSpeed: String?,
|
||||
val windDirection: String?,
|
||||
@@ -18,65 +18,26 @@ data class WeatherDisplay(
|
||||
val humidity: String?,
|
||||
val clouds: String?,
|
||||
val lat: Double = 0.00,
|
||||
val lon: Double = 0.00
|
||||
): Parcelable{
|
||||
val lon: Double = 0.00,
|
||||
var displayName: String?
|
||||
){
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readValue(Double::class.java.classLoader) as? Double,
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.createTypedArrayList(Forecast),
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.readString(),
|
||||
parcel.readString()) {
|
||||
}
|
||||
|
||||
constructor(weather: FullWeather) : this(
|
||||
weather.current?.temp,
|
||||
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
|
||||
constructor(entity: EntityItem) : this(
|
||||
entity.weather.current?.temp,
|
||||
entity.weather.temperatureUnit,
|
||||
entity.id,
|
||||
entity.weather.current?.icon,
|
||||
entity.weather.current?.description,
|
||||
entity.weather.hourly,
|
||||
entity.weather.daily?.drop(1)?.map { Forecast(it) },
|
||||
entity.weather.current?.windSpeed?.toString(),
|
||||
entity.weather.current?.windDeg?.toString(),
|
||||
entity.weather.daily?.get(0)?.pop?.times(100)?.toInt()?.toString(),
|
||||
entity.weather.current?.humidity?.toString(),
|
||||
entity.weather.current?.clouds?.toString(),
|
||||
entity.weather.lat,
|
||||
entity.weather.lon,
|
||||
entity.weather.locationString
|
||||
)
|
||||
|
||||
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 timezone: String? = null,
|
||||
val timezoneOffset: Int? = null,
|
||||
val hourly: List<Hour>? = null,
|
||||
val daily: List<DailyWeather>? = null,
|
||||
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(
|
||||
weatherResponse.current?.let { Current(it) },
|
||||
weatherResponse.timezone,
|
||||
weatherResponse.timezoneOffset,
|
||||
weatherResponse.hourly?.subList(0,23)?.map { Hour(it) },
|
||||
weatherResponse.daily?.map { DailyWeather(it) },
|
||||
weatherResponse.lon,
|
||||
weatherResponse.lat
|
||||
|
||||
@@ -1,78 +1,15 @@
|
||||
package com.appttude.h_mal.atlas_weather.model.widget
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
data class WidgetData(
|
||||
val location: String?,
|
||||
val icon: Bitmap?,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
val icon: String?,
|
||||
val currentTemp: String?
|
||||
)
|
||||
|
||||
data class InnerWidgetData(
|
||||
val date: String?,
|
||||
val icon: Bitmap?,
|
||||
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 java.text.SimpleDateFormat
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.OffsetTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
|
||||
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
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.bumptech.glide.Glide
|
||||
|
||||
fun View.show() {
|
||||
this.visibility = View.VISIBLE
|
||||
@@ -28,38 +28,24 @@ fun Fragment.displayToast(message: String) {
|
||||
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
|
||||
.from(context)
|
||||
.inflate(layoutId, this, false)
|
||||
|
||||
fun ImageView.loadImage(url: String?, height: Int, width: Int){
|
||||
Picasso.get()
|
||||
fun ImageView.loadImage(url: String?){
|
||||
val c = Glide.with(this)
|
||||
.load(url)
|
||||
.resize(width.dpToPx(), height.dpToPx())
|
||||
.centerCrop()
|
||||
viewTreeObserver.addOnPreDrawListener {
|
||||
c.override(width, height)
|
||||
true
|
||||
}
|
||||
c.placeholder(R.drawable.ic_baseline_cloud_queue_24)
|
||||
.error(R.drawable.ic_baseline_cloud_off_24)
|
||||
.fitCenter()
|
||||
.into(this)
|
||||
}
|
||||
|
||||
fun Int.dpToPx(): Int = (this * Resources.getSystem().displayMetrics.density).toInt()
|
||||
|
||||
fun SearchView.onSubmitListener(searchSubmit: (String) -> Unit) {
|
||||
this.setOnQueryTextListener(object :
|
||||
SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(s: String): Boolean {
|
||||
searchSubmit.invoke(s)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(s: String): Boolean {
|
||||
return true
|
||||
}
|
||||
})
|
||||
fun Fragment.hideKeyboard() {
|
||||
val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||
imm?.hideSoftInputFromWindow(view?.windowToken, 0)
|
||||
}
|
||||
@@ -3,64 +3,63 @@ package com.appttude.h_mal.atlas_weather.viewmodel
|
||||
import android.Manifest
|
||||
import androidx.annotation.RequiresPermission
|
||||
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.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.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.viewmodel.baseViewModels.WeatherViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
|
||||
class MainViewModel(
|
||||
private val locationProvider: LocationProvider,
|
||||
private val repository: Repository
|
||||
): ViewModel(){
|
||||
private val locationProvider: LocationProvider,
|
||||
private val repository: Repository
|
||||
): WeatherViewModel(){
|
||||
|
||||
val weatherLiveData = MutableLiveData<WeatherDisplay>()
|
||||
|
||||
val operationState = MutableLiveData<Event<Boolean>>()
|
||||
val operationError = MutableLiveData<Event<String>>()
|
||||
val operationRefresh = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
init {
|
||||
repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever {
|
||||
it?.weather?.let { data ->
|
||||
val list= WeatherDisplay(data).apply {
|
||||
unit = "°C"
|
||||
location = locationProvider.getLocationName(data.lat, data.lon)
|
||||
}
|
||||
|
||||
weatherLiveData.postValue(list)
|
||||
it?.let {
|
||||
val weather = WeatherDisplay(it)
|
||||
weatherLiveData.postValue(weather)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(value = Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
fun fetchData(){
|
||||
if (!repository.isSearchValid(CURRENT_LOCATION)) return
|
||||
if (!repository.isSearchValid(CURRENT_LOCATION)){
|
||||
operationRefresh.postValue(Event(false))
|
||||
return
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
operationState.postValue(Event(true))
|
||||
try {
|
||||
// Get location
|
||||
val latLong = locationProvider.getLatLong()
|
||||
val latLong = locationProvider.getCurrentLatLong()
|
||||
// Get weather from api
|
||||
val weather = repository
|
||||
.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
|
||||
weather.let {
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(CURRENT_LOCATION, it)
|
||||
return@launch
|
||||
}
|
||||
}catch (e: IOException){
|
||||
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
}catch (e: Exception){
|
||||
operationError.postValue(Event(e.message!!))
|
||||
}finally {
|
||||
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
|
||||
|
||||
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.network.response.forecast.WeatherResponse
|
||||
import com.appttude.h_mal.atlas_weather.data.repository.Repository
|
||||
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
|
||||
import com.appttude.h_mal.atlas_weather.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.viewmodel.baseViewModels.WeatherViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
const val ALL_LOADED = "all_loaded"
|
||||
class WorldViewModel(
|
||||
private val locationProvider: LocationProvider,
|
||||
private val repository: Repository
|
||||
) : ViewModel() {
|
||||
) : WeatherViewModel() {
|
||||
|
||||
val weatherLiveData = MutableLiveData<List<WeatherDisplay>>()
|
||||
val singleWeatherLiveData = MutableLiveData<WeatherDisplay>()
|
||||
|
||||
val operationState = MutableLiveData<Event<Boolean>>()
|
||||
val operationError = MutableLiveData<Event<String>>()
|
||||
val operationRefresh = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val operationComplete = MutableLiveData<Event<String>>()
|
||||
|
||||
private val weatherListLiveData = repository.loadAllWeatherExceptCurrentFromRoom()
|
||||
private val weatherListLiveData = repository.loadRoomWeatherLiveData()
|
||||
|
||||
init {
|
||||
weatherListLiveData.observeForever {
|
||||
it.forEach { item->
|
||||
println(item)
|
||||
}
|
||||
|
||||
val list = it.map { data ->
|
||||
WeatherDisplay(data.weather).apply {
|
||||
unit = "°C"
|
||||
location = locationProvider.getLocationName(data.weather.lat, data.weather.lon)
|
||||
}
|
||||
WeatherDisplay(data)
|
||||
}
|
||||
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) {
|
||||
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 {
|
||||
operationState.postValue(Event(true))
|
||||
// Check if location exists
|
||||
@@ -54,29 +78,20 @@ class WorldViewModel(
|
||||
}
|
||||
|
||||
try {
|
||||
// Get location
|
||||
val latLong =
|
||||
locationProvider.getLatLongFromLocationString(locationName)
|
||||
val lat = latLong.first
|
||||
val lon = latLong.second
|
||||
|
||||
// Get weather from api
|
||||
val weather = repository
|
||||
.getWeatherFromApi(lat.toString(), lon.toString())
|
||||
val entityItem = createWeatherEntity(locationName)
|
||||
|
||||
// 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)){
|
||||
operationError.postValue(Event("$retrievedLocation already exists"))
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Save data if not null
|
||||
weather.let {
|
||||
repository.saveCurrentWeatherToRoom(retrievedLocation, it)
|
||||
operationComplete.postValue(Event("$retrievedLocation saved"))
|
||||
return@launch
|
||||
}
|
||||
repository.saveCurrentWeatherToRoom(entityItem)
|
||||
repository.saveLastSavedAt(retrievedLocation)
|
||||
operationComplete.postValue(Event("$retrievedLocation saved"))
|
||||
|
||||
} catch (e: IOException) {
|
||||
operationError.postValue(Event(e.message!!))
|
||||
} finally {
|
||||
@@ -86,27 +101,26 @@ class WorldViewModel(
|
||||
}
|
||||
|
||||
fun fetchAllLocations() {
|
||||
if (!repository.isSearchValid(ALL_LOADED)){
|
||||
operationRefresh.postValue(Event(false))
|
||||
return
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
operationState.postValue(Event(true))
|
||||
try {
|
||||
val list = mutableListOf<EntityItem>()
|
||||
weatherLiveData.value?.forEach { weatherItem ->
|
||||
repository.loadWeatherList().forEach { locationName ->
|
||||
// If search not valid move onto next in loop
|
||||
weatherItem.location?.let {
|
||||
if (!repository.isSearchValid(it)) return@forEach
|
||||
}
|
||||
if (!repository.isSearchValid(locationName)) return@forEach
|
||||
|
||||
try {
|
||||
val weather = repository
|
||||
.getWeatherFromApi(weatherItem.lat.toString(), weatherItem.lon.toString())
|
||||
|
||||
weatherItem.location?.let { loc ->
|
||||
repository.saveLastSavedAt(loc)
|
||||
list.add(EntityItem(loc, FullWeather(weather)))
|
||||
}
|
||||
val entity = createWeatherEntity(locationName)
|
||||
list.add(entity)
|
||||
repository.saveLastSavedAt(locationName)
|
||||
} catch (e: IOException) { }
|
||||
}
|
||||
repository.saveWeatherListToRoom(list)
|
||||
repository.saveLastSavedAt(ALL_LOADED)
|
||||
} catch (e: IOException) {
|
||||
operationError.postValue(Event(e.message!!))
|
||||
} 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: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"/>
|
||||
</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 |