- weathered

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

View File

@@ -378,7 +378,7 @@
<PersistentState>
<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>

Binary file not shown.

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

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

View File

@@ -36,5 +36,10 @@
<option name="name" value="C:\Users\h_mal\AppData\Local\Android\Sdk\extras\m2repository" />
<option name="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>

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ class NotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,54 +1,13 @@
package com.appttude.h_mal.atlas_weather.data.location
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
}

View File

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

View File

@@ -2,11 +2,11 @@ package com.appttude.h_mal.atlas_weather.data.network
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.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
baseUrl: String,
networkConnectionInterceptor: NetworkConnectionInterceptor,
queryParamsInterceptor: QueryParamsInterceptor
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
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.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)
}
}

View File

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

View File

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

View File

@@ -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)
}
} catch (e: Exception) { null }
val temp = dailyWeather.max?.toInt().toString()
val item = InnerWidgetData(day, bitmap, temp)
list.add(item)
}
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)
}
override fun onBitmapFailed(e: Exception?, d: Drawable?) {
cont.resume(null)
}
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {}
})
}
}
fun getWidgetBackground(): Int {
return if (settingsRepository.isBlackBackground()){
return if (settingsRepository.isBlackBackground()) {
Color.BLACK
}else{
} else {
Color.TRANSPARENT
}
}
fun setFirstTimer() = settingsRepository.setFirstTime()
}

View File

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

View File

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

View File

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

View File

@@ -6,15 +6,19 @@ data class FullWeather(
val current: Current? = null,
val 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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,11 +4,9 @@ package com.appttude.h_mal.atlas_weather.utils
import android.os.Build
import 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 {

View File

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

View File

@@ -1,16 +1,16 @@
package com.appttude.h_mal.atlas_weather.utils
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)
}

View File

@@ -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(){
): 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)
it?.let {
val weather = WeatherDisplay(it)
weatherLiveData.postValue(weather)
}
weatherLiveData.postValue(list)
}
}
}
@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.saveCurrentWeatherToRoom(entityItem)
}catch (e: Exception){
operationError.postValue(Event(e.message!!))
}finally {
operationState.postValue(Event(false))
operationRefresh.postValue(Event(false))
}
}
}

View File

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

View File

@@ -1,50 +1,74 @@
package com.appttude.h_mal.atlas_weather.viewmodel
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)
repository.saveCurrentWeatherToRoom(entityItem)
repository.saveLastSavedAt(retrievedLocation)
operationComplete.postValue(Event("$retrievedLocation saved"))
return@launch
}
} 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)
}
}

View File

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

View File

@@ -0,0 +1,30 @@
package com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels
import androidx.lifecycle.ViewModel
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
abstract class WeatherViewModel : ViewModel(){
fun createFullWeather(
weather: WeatherResponse,
location: String
): FullWeather {
return FullWeather(weather).apply {
temperatureUnit = "°C"
locationString = location
}
}
fun createWeatherEntity(
locationId: String,
weather: FullWeather
): EntityItem{
weather.apply {
locationString = locationId
}
return EntityItem(locationId, weather)
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

@@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
<vector android:height="128dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android: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>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM19,18H6c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4h0.71C7.37,7.69 9.48,6 12,6c3.04,0 5.5,2.46 5.5,5.5v0.5H19c1.66,0 3,1.34 3,3s-1.34,3 -3,3z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11,15h2v2h-2v-2zM11,7h2v6h-2L11,7zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

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