Refactor flavours (#17)

- Fastlane completed
 - Circleci config completed
 - Flavours build completed
This commit is contained in:
2023-08-07 20:17:08 +01:00
committed by GitHub
parent 4a37b724a6
commit baabebd40d
121 changed files with 1326 additions and 1586 deletions

View File

@@ -1,11 +1,16 @@
<?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_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" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application android:networkSecurityConfig="@xml/network_security_config" />
<uses-feature
android:name="android.hardware.location"

View File

@@ -1,27 +1,12 @@
package com.appttude.h_mal.atlas_weather.application
import androidx.room.RoomDatabase
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.Api
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
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.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.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
const val LOCATION_PERMISSION_REQUEST = 505

View File

@@ -25,7 +25,7 @@ import kotlin.coroutines.suspendCoroutine
class LocationProviderImpl(
val applicationContext: Context
private val applicationContext: Context
) : LocationProvider, LocationHelper(applicationContext) {
private var locationManager =
applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?

View File

@@ -1,6 +1,5 @@
package com.appttude.h_mal.atlas_weather.data.network.networkUtils
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor
import okhttp3.Interceptor
import okhttp3.OkHttpClient

View File

@@ -3,59 +3,61 @@ 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
data class Forecast(
val date: String?,
val day: String?,
val condition: String?,
val weatherIcon: String?,
val mainTemp: String?,
val minorTemp: String?,
val averageTemp: String?,
val windText: String?,
val precipitation: String?,
val humidity: String?,
val uvi: String?,
val sunrise: String?,
val sunset: String?,
val cloud: String?
): Parcelable {
val date: String?,
val day: String?,
val condition: String?,
val weatherIcon: String?,
val mainTemp: String?,
val minorTemp: String?,
val averageTemp: String?,
val windText: String?,
val precipitation: String?,
val humidity: String?,
val uvi: String?,
val sunrise: String?,
val sunset: String?,
val cloud: String?
): Parcelable{
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString())
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString()
) {
}
constructor(dailyWeather: DailyWeather) : this(
dailyWeather.dt?.toDayString(),
dailyWeather.dt?.toDayName(),
dailyWeather.description,
dailyWeather.icon,
dailyWeather.max?.toInt().toString(),
dailyWeather.min?.toInt().toString(),
dailyWeather.average?.toInt().toString(),
dailyWeather.windSpeed?.toInt().toString(),
(dailyWeather.pop?.times(100))?.toInt().toString(),
dailyWeather.humidity?.toString(),
dailyWeather.uvi?.toInt().toString(),
dailyWeather.sunrise?.toTime(),
dailyWeather.sunset?.toTime(),
dailyWeather.clouds?.toString()
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) {
@@ -72,13 +74,20 @@ data class Forecast(
parcel.writeString(uvi)
parcel.writeString(sunrise)
parcel.writeString(sunset)
parcel.writeString(cloud)
}
override fun describeContents(): Int {
return 0
}
companion object{
@JvmField val CREATOR = parcelableCreator(::Forecast)
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)
}
}
}

View File

@@ -1,43 +1,97 @@
package com.appttude.h_mal.atlas_weather.model.forecast
import android.os.Parcel
import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.Hour
data class WeatherDisplay(
val averageTemp: Double?,
var unit: String?,
var location: String?,
val iconURL: String?,
val description: String?,
val hourly: List<Hour>?,
val forecast: List<Forecast>?,
val windSpeed: String?,
val windDirection: String?,
val precipitation: String?,
val humidity: String?,
val clouds: String?,
val lat: Double = 0.00,
val lon: Double = 0.00,
var displayName: String?
){
val averageTemp: Double?,
var unit: String?,
var location: String?,
val iconURL: String?,
val description: String?,
val hourly: List<Hour>?,
val forecast: List<Forecast>?,
val windSpeed: String?,
val windDirection: String?,
val precipitation: String?,
val humidity: String?,
val clouds: String?,
val lat: Double = 0.00,
val lon: Double = 0.00,
var displayName: String?
): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readValue(Double::class.java.classLoader) as? Double,
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.createTypedArrayList(Hour),
parcel.createTypedArrayList(Forecast),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readDouble(),
parcel.readDouble(),
parcel.readString()
) {
}
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
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(hourly)
parcel.writeTypedList(forecast)
parcel.writeString(windSpeed)
parcel.writeString(windDirection)
parcel.writeString(precipitation)
parcel.writeString(humidity)
parcel.writeString(clouds)
parcel.writeDouble(lat)
parcel.writeDouble(lon)
parcel.writeString(displayName)
}
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

@@ -1,44 +1,45 @@
package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Current
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
data class Current(
val dt: Int? = null,
val sunrise: Int? = null,
val sunset: Int? = null,
val temp: Double? = null,
val visibility: Int? = null,
val uvi: Double? = null,
val pressure: Int? = null,
val clouds: Int? = null,
val feelsLike: Double? = null,
val windDeg: Int? = null,
val dewPoint: Double? = null,
val icon: String? = null,
val description: String? = null,
val main: String? = null,
val id: Int? = null,
val humidity: Int? = null,
val windSpeed: Double? = null
){
val dt: Int? = null,
val sunrise: Int? = null,
val sunset: Int? = null,
val temp: Double? = null,
val visibility: Int? = null,
val uvi: Double? = null,
val pressure: Int? = null,
val clouds: Int? = null,
val feelsLike: Double? = null,
val windDeg: Int? = null,
val dewPoint: Double? = null,
val icon: String? = null,
val description: String? = null,
val main: String? = null,
val id: Int? = null,
val humidity: Int? = null,
val windSpeed: Double? = null
) {
constructor(dailyItem: Current): this(
dailyItem.dt,
dailyItem.sunrise,
dailyItem.sunset,
dailyItem.temp,
dailyItem.visibility,
dailyItem.uvi,
dailyItem.pressure,
dailyItem.clouds,
dailyItem.feelsLike,
dailyItem.windDeg,
dailyItem.dewPoint,
dailyItem.weather?.get(0)?.icon?.let { "https://openweathermap.org/img/wn/${it}@4x.png" },
dailyItem.weather?.get(0)?.description,
dailyItem.weather?.get(0)?.main,
dailyItem.weather?.get(0)?.id,
dailyItem.humidity,
dailyItem.windSpeed
)
constructor(dailyItem: Current) : this(
dailyItem.dt,
dailyItem.sunrise,
dailyItem.sunset,
dailyItem.temp,
dailyItem.visibility,
dailyItem.uvi,
dailyItem.pressure,
dailyItem.clouds,
dailyItem.feelsLike,
dailyItem.windDeg,
dailyItem.dewPoint,
generateIconUrlString(dailyItem.weather?.getOrNull(0)?.icon),
dailyItem.weather?.get(0)?.description,
dailyItem.weather?.get(0)?.main,
dailyItem.weather?.get(0)?.id,
dailyItem.humidity,
dailyItem.windSpeed
)
}

View File

@@ -1,6 +1,7 @@
package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.DailyItem
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
data class DailyWeather(
@@ -39,7 +40,7 @@ data class DailyWeather(
dailyItem.dewPoint,
dailyItem.windSpeed,
dailyItem.windDeg,
dailyItem.weather?.get(0)?.icon?.let { "https://openweathermap.org/img/wn/${it}@4x.png" },
generateIconUrlString(dailyItem.weather?.getOrNull(0)?.icon),
dailyItem.weather?.get(0)?.description,
dailyItem.weather?.get(0)?.main,
dailyItem.weather?.get(0)?.id,

View File

@@ -1,19 +1,47 @@
package com.appttude.h_mal.atlas_weather.model.weather
import android.os.Parcel
import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Hour as ForecastHour
data class Hour(
val dt: Int? = null,
val temp: Double? = null,
val icon: String? = null
){
constructor(hour: ForecastHour) : this(
hour.dt,
hour.temp,
generateIconUrlString(hour.weather?.getOrNull(0)?.icon)
)
}
val dt: Int? = null,
val temp: Double? = null,
val icon: String? = null
): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readValue(Int::class.java.classLoader) as? Int,
parcel.readValue(Double::class.java.classLoader) as? Double,
parcel.readString()
) {
}
constructor(hour: ForecastHour) : this(
hour.dt,
hour.temp,
generateIconUrlString(hour.weather?.getOrNull(0)?.icon)
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeValue(dt)
parcel.writeValue(temp)
parcel.writeString(icon)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Hour> {
override fun createFromParcel(parcel: Parcel): Hour {
return Hour(parcel)
}
override fun newArray(size: Int): Array<Hour?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,154 @@
package com.appttude.h_mal.monoWeather.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.utils.displayToast
import com.appttude.h_mal.atlas_weather.utils.hide
import com.appttude.h_mal.atlas_weather.utils.show
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.KodeinAware
import org.kodein.di.android.x.kodein
import org.kodein.di.generic.instance
import kotlin.properties.Delegates
abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId),
KodeinAware {
override val kodein by kodein()
val factory by instance<ApplicationViewModelFactory>()
inline fun <reified VM : ViewModel> getFragmentViewModel(): Lazy<VM> = viewModels { factory }
private var shortAnimationDuration by Delegates.notNull<Int>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
}
// toggle visibility of progress spinner while async operations are taking place
fun progressBarStateObserver(progressBar: View) = Observer<Event<Boolean>> {
it.getContentIfNotHandled()?.let { i ->
if (i)
progressBar.fadeIn()
else
progressBar.fadeOut()
}
}
// display a toast when operation fails
fun errorObserver() = Observer<Event<String>> {
it.getContentIfNotHandled()?.let { message ->
displayToast(message)
}
}
fun refreshObserver(refresher: SwipeRefreshLayout) = Observer<Event<Boolean>> {
refresher.isRefreshing = false
}
/**
* Request a permission for
* @param permission with
* @param permissionCode
* Callback if is already permission granted
* @param permissionGranted
*/
fun getPermissionResult(
permission: String,
permissionCode: Int,
permissionGranted: () -> Unit
) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
permission
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissions(arrayOf(permission), permissionCode)
return
} else {
CoroutineScope(Dispatchers.Main).launch {
permissionGranted.invoke()
}
}
}
private fun View.fadeIn() {
apply {
// Set the content view to 0% opacity but visible, so that it is visible
// (but fully transparent) during the animation.
alpha = 0f
hide()
// Animate the content view to 100% opacity, and clear any animation
// listener set on the view.
animate()
.alpha(1f)
.setDuration(shortAnimationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
show()
}
})
}
}
private fun View.fadeOut() {
apply {
// Set the content view to 0% opacity but visible, so that it is visible
// (but fully transparent) during the animation.
alpha = 1f
show()
// Animate the content view to 100% opacity, and clear any animation
// listener set on the view.
animate()
.alpha(0f)
.setDuration(shortAnimationDuration.toLong())
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
hide()
}
})
}
}
@SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == LOCATION_PERMISSION_REQUEST) {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
permissionsGranted()
displayToast("Permission granted")
} else {
permissionsRefused()
displayToast("Permission denied")
}
}
}
open fun permissionsGranted() {}
open fun permissionsRefused() {}
}

View File

@@ -0,0 +1,52 @@
package com.appttude.h_mal.atlas_weather.ui
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.appttude.h_mal.atlas_weather.R
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.android.synthetic.main.activity_main_navigation.*
class MainActivity : AppCompatActivity() {
lateinit var navHost: NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_navigation)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
setSupportActionBar(toolbar)
navHost = supportFragmentManager
.findFragmentById(R.id.container) as NavHostFragment
val navController = navHost.navController
navController.setGraph(R.navigation.main_navigation)
setupBottomBar(navView, navController)
}
private fun setupBottomBar(navView: BottomNavigationView, navController: NavController) {
val appBarConfiguration = AppBarConfiguration(tabs)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
when (item.itemId) {
android.R.id.home -> onBackPressed()
}
return super.onOptionsItemSelected(item)
}
}

View File

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

View File

@@ -2,6 +2,7 @@ package com.appttude.h_mal.atlas_weather.utils
import android.app.Activity
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -10,7 +11,7 @@ import android.widget.ImageView
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.appttude.h_mal.atlas_weather.R
import com.bumptech.glide.Glide
import com.squareup.picasso.Picasso
fun View.show() {
this.visibility = View.VISIBLE
@@ -33,16 +34,10 @@ fun ViewGroup.generateView(layoutId: Int): View = LayoutInflater
.inflate(layoutId, this, false)
fun ImageView.loadImage(url: String?){
val c = Glide.with(this)
.load(url)
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)
Picasso.get().load(url)
.placeholder(R.drawable.ic_baseline_cloud_queue_24)
.error(R.drawable.ic_baseline_cloud_off_24)
.into(this)
}
fun Fragment.hideKeyboard() {

View File

@@ -5,9 +5,10 @@ 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 ApplicationViewModelFactory(
private val locationProvider: LocationProvider,
private val repository: RepositoryImpl
private val locationProvider: LocationProvider,
private val repository: RepositoryImpl
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")

View File

@@ -15,8 +15,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainViewModel(
private val locationProvider: LocationProvider,
private val repository: Repository
private val locationProvider: LocationProvider,
private val repository: Repository
): WeatherViewModel(){
val weatherLiveData = MutableLiveData<WeatherDisplay>()

View File

@@ -16,8 +16,8 @@ import java.io.IOException
const val ALL_LOADED = "all_loaded"
class WorldViewModel(
private val locationProvider: LocationProvider,
private val repository: Repository
private val locationProvider: LocationProvider,
private val repository: Repository
) : WeatherViewModel() {
val weatherLiveData = MutableLiveData<List<WeatherDisplay>>()

View File

@@ -0,0 +1,79 @@
package com.appttude.h_mal.atlas_weather.widget
import android.app.Activity
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.TaskStackBuilder
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Intent
import android.os.Build
import android.widget.RemoteViews
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.core.app.JobIntentService
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
abstract class BaseWidgetServiceIntentClass<T : AppWidgetProvider> : JobIntentService() {
lateinit var appWidgetManager: AppWidgetManager
lateinit var appWidgetIds: IntArray
fun initBaseWidget(componentName: ComponentName) {
appWidgetManager = AppWidgetManager.getInstance(baseContext)
appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
}
fun createRemoteView(@LayoutRes id: Int): RemoteViews {
return RemoteViews(packageName, id)
}
// Create pending intent commonly used for 'click to update' features
fun createUpdatePendingIntent(
appWidgetProvider: Class<T>,
appWidgetId: Int
): PendingIntent? {
val seconds = (System.currentTimeMillis() / 1000L).toInt()
val intentUpdate = Intent(applicationContext, appWidgetProvider)
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val idArray = intArrayOf(appWidgetId)
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(this, seconds, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getBroadcast(this, seconds, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
/**
* create a pending intent used to navigate to activity:
* @param activityClass
*/
fun <T : Activity> createClickingPendingIntent(activityClass: Class<T>): PendingIntent {
val clickIntentTemplate = Intent(this, activityClass)
return TaskStackBuilder.create(this)
.addNextIntentWithParentStack(clickIntentTemplate)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
}
fun setImageView(
path: String?,
views: RemoteViews,
@IdRes viewId: Int,
appWidgetId: Int
) {
CoroutineScope(Dispatchers.Main).launch {
Picasso.get().load(path).into(views, viewId, intArrayOf(appWidgetId))
}
}
open fun bindView(widgetId: Int, views: RemoteViews, data: Any?) {}
open fun bindEmptyView(widgetId: Int, views: RemoteViews, data: Any?) {}
open fun bindErrorView(widgetId: Int, views: RemoteViews, data: Any?) {}
}

View File

@@ -0,0 +1,32 @@
package com.appttude.h_mal.atlas_weather.widget
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import com.appttude.h_mal.atlas_weather.widget.WidgetJobServiceIntent.Companion.enqueueWork
/**
* Implementation of App Widget functionality.
*/
class NewAppWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
loadWidget(context)
}
override fun onEnabled(context: Context) {
super.onEnabled(context)
loadWidget(context)
}
override fun onDisabled(context: Context) { }
private fun loadWidget(context: Context){
val mIntent = Intent(context, WidgetJobServiceIntent::class.java)
enqueueWork(context, mIntent)
}
}

View File

@@ -0,0 +1,220 @@
package com.appttude.h_mal.atlas_weather.widget
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.icu.text.SimpleDateFormat
import android.os.PowerManager
import android.widget.RemoteViews
import android.os.Build
import androidx.core.app.ActivityCompat.checkSelfPermission
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.widget.WidgetState.*
import com.appttude.h_mal.atlas_weather.widget.WidgetState.Companion.getWidgetState
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.isInternetAvailable
import com.appttude.h_mal.atlas_weather.utils.tryOrNullSuspended
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.KodeinAware
import org.kodein.di.LateInitKodein
import org.kodein.di.generic.instance
import java.util.*
/**
* Implementation of a JobIntentService used for home screen widget
*/
const val HALF_DAY = 43200000L
class WidgetJobServiceIntent : BaseWidgetServiceIntentClass<NewAppWidget>() {
private val kodein = LateInitKodein()
private val helper: ServicesHelper by kodein.instance()
override fun onHandleWork(intent: Intent) {
// We have received work to do. The system or framework is already
// holding a wake lock for us at this point, so we can just go.
kodein.baseKodein = (applicationContext as KodeinAware).kodein
executeWidgetUpdate()
}
private fun executeWidgetUpdate() {
val componentName = ComponentName(this, NewAppWidget::class.java)
initBaseWidget(componentName)
initiateWidgetUpdate(getCurrentWidgetState())
}
private fun initiateWidgetUpdate(state: WidgetState) {
when (state) {
NO_LOCATION, SCREEN_ON_CONNECTION_UNAVAILABLE -> updateErrorWidget(state)
SCREEN_ON_CONNECTION_AVAILABLE -> updateWidget(false)
SCREEN_OFF_CONNECTION_AVAILABLE -> updateWidget(true)
SCREEN_OFF_CONNECTION_UNAVAILABLE -> return
}
}
private fun updateWidget(fromStorage: Boolean) {
CoroutineScope(Dispatchers.IO).launch {
val result = getWidgetWeather(fromStorage)
appWidgetIds.forEach { id -> setupView(id, result) }
}
}
private fun updateErrorWidget(state: WidgetState) {
appWidgetIds.forEach { id -> setEmptyView(id, state) }
}
private fun getCurrentWidgetState(): WidgetState {
val pm = getSystemService(POWER_SERVICE) as PowerManager
val isScreenOn = pm.isInteractive
val locationGranted =
checkSelfPermission(this, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED
val internetAvailable = isInternetAvailable(this.applicationContext)
return getWidgetState(locationGranted, isScreenOn, internetAvailable)
}
@SuppressLint("MissingPermission")
suspend fun getWidgetWeather(storageOnly: Boolean): WidgetWeatherCollection? {
return tryOrNullSuspended {
if (!storageOnly) helper.fetchData()
helper.getWidgetWeatherCollection()
}
}
private fun setEmptyView(appWidgetId: Int, state: WidgetState) {
val error = when (state) {
NO_LOCATION -> "No Location Permission"
SCREEN_ON_CONNECTION_UNAVAILABLE -> "No network available"
else -> "No data"
}
val views = createRemoteView(R.layout.weather_app_widget)
bindErrorView(appWidgetId, views, error)
}
private fun setupView(
appWidgetId: Int,
collection: WidgetWeatherCollection?
) {
val views = createRemoteView(R.layout.weather_app_widget)
setLastUpdated(views, collection?.widgetData?.timeStamp)
views.setInt(R.id.whole_widget_view, "setBackgroundColor", helper.getWidgetBackground())
if (collection != null) {
bindView(appWidgetId, views, collection)
} else {
bindEmptyView(appWidgetId, views, "No weather available")
}
}
override fun bindErrorView(
widgetId: Int,
views: RemoteViews,
data: Any?
) {
bindEmptyView(widgetId, views, data)
}
override fun bindEmptyView(
widgetId: Int,
views: RemoteViews,
data: Any?
) {
val clickUpdate = createUpdatePendingIntent(NewAppWidget::class.java, widgetId)
views.apply {
setTextViewText(R.id.widget_current_location, data as String)
setImageViewResource(R.id.widget_current_icon, R.drawable.ic_baseline_cloud_off_24)
setImageViewResource(R.id.location_icon, 0)
setTextViewText(R.id.widget_main_temp, "")
setTextViewText(R.id.widget_feel_temp, "")
setOnClickPendingIntent(R.id.widget_current_icon, clickUpdate)
setOnClickPendingIntent(R.id.widget_current_location, clickUpdate)
appWidgetManager.updateAppWidget(widgetId, this)
}
}
override fun bindView(widgetId: Int, views: RemoteViews, data: Any?) {
val clickUpdate = createUpdatePendingIntent(NewAppWidget::class.java, widgetId)
val clickToMain = createClickingPendingIntent(MainActivity::class.java)
val collection = data as WidgetWeatherCollection
val weather = collection.widgetData
views.apply {
setTextViewText(R.id.widget_main_temp, weather.currentTemp)
setTextViewText(R.id.widget_feel_temp, "°C")
setTextViewText(R.id.widget_current_location, weather.location)
setImageViewResource(R.id.location_icon, R.drawable.location_flag)
setImageView(weather.icon, this, R.id.widget_current_icon, widgetId)
setOnClickPendingIntent(R.id.widget_current_icon, clickUpdate)
setOnClickPendingIntent(R.id.widget_current_location, clickUpdate)
loadCells(widgetId, this, collection.forecast, clickToMain)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(widgetId, views)
}
}
private fun loadCells(
appWidgetId: Int,
remoteViews: RemoteViews,
weather: List<InnerWidgetCellData>,
clickIntent: PendingIntent
) {
(0..4).forEach { i ->
val containerId: Int = resources.getIdentifier("widget_item_$i", "id", packageName)
val dayId: Int = resources.getIdentifier("widget_item_day_$i", "id", packageName)
val imageId: Int = resources.getIdentifier("widget_item_image_$i", "id", packageName)
val tempId: Int = resources.getIdentifier("widget_item_temp_high_$i", "id", packageName)
val it = weather[i]
remoteViews.setTextViewText(dayId, it.date)
remoteViews.setTextViewText(tempId, it.highTemp)
setImageView(it.icon, remoteViews, imageId, appWidgetId)
remoteViews.setOnClickPendingIntent(containerId, clickIntent)
}
}
private fun setLastUpdated(views: RemoteViews, timeStamp: Long?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && timeStamp != null) {
val difference = System.currentTimeMillis().minus(timeStamp)
val status = if (difference > HALF_DAY) {
"12hrs ago"
} else {
val date = Date(timeStamp)
val sdf = SimpleDateFormat("HH:mm", Locale.getDefault())
sdf.format(date)
}
views.setTextViewText(R.id.widget_current_status, "last updated: $status")
}
}
companion object {
/**
* Unique job ID for this service.
*/
private const val JOB_ID = 1000
/**
* Convenience method for enqueuing work in to this service.
*/
fun enqueueWork(context: Context, work: Intent) {
enqueueWork(context, WidgetJobServiceIntent::class.java, JOB_ID, work)
}
}
}

View File

@@ -0,0 +1,28 @@
package com.appttude.h_mal.atlas_weather.widget
enum class WidgetState {
NO_LOCATION,
SCREEN_ON_CONNECTION_AVAILABLE,
SCREEN_ON_CONNECTION_UNAVAILABLE,
SCREEN_OFF_CONNECTION_AVAILABLE,
SCREEN_OFF_CONNECTION_UNAVAILABLE;
companion object {
fun getWidgetState(
locationAvailable: Boolean,
screenOn: Boolean,
connectionAvailable: Boolean
): WidgetState {
return if (!locationAvailable)
NO_LOCATION
else if (screenOn && connectionAvailable)
SCREEN_ON_CONNECTION_AVAILABLE
else if (screenOn && !connectionAvailable)
SCREEN_ON_CONNECTION_UNAVAILABLE
else if (!screenOn && connectionAvailable)
SCREEN_OFF_CONNECTION_AVAILABLE
else
SCREEN_OFF_CONNECTION_UNAVAILABLE
}
}
}

View File

@@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.appttude.h_mal.atlas_weather.monoWeather.ui.world.AddLocationFragment">
tools:context="com.appttude.h_mal.atlas_weather.ui.world.AddLocationFragment">
<LinearLayout

View File

@@ -29,7 +29,7 @@
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/tabs_menu" />
<fragment
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"

View File

@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.appttude.h_mal.atlas_weather.monoWeather.ui.world.WorldFragment">
tools:context="com.appttude.h_mal.atlas_weather.ui.world.WorldFragment">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"

View File

@@ -5,7 +5,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.appttude.h_mal.atlas_weather.monoWeather.ui.world.AddLocationFragment">
tools:context="com.appttude.h_mal.atlas_weather.ui.world.AddLocationFragment">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"

View File

@@ -26,7 +26,7 @@
android:layout_height="match_parent"
android:background="@android:color/black"
android:visibility="gone"
tools:visibility="visible">
tools:visibility="gone">
<ProgressBar
android:layout_gravity="center"
style="?android:attr/progressBarStyle"
@@ -34,6 +34,4 @@
android:layout_height="wrap_content"/>
</FrameLayout>
</RelativeLayout>

View File

@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context=".legacy.ui.home.MainActivity">
<item
android:id="@+id/action_settings"
android:id="@+id/settings_fragment"
android:orderInCategory="100"
android:title="@string/action_settings"
android:icon="@drawable/ic_round_settings_24"

View File

@@ -27,4 +27,12 @@
<item name="android:textColor">#ffffff</item>
<item name="android:textSize">12sp</item>
</style>
<style name="icon_style__further_deatils">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
<item name="android:adjustViewBounds">true</item>
<item name="android:layout_gravity">center</item>
<item name="android:tint">@color/colorAccent</item>
</style>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">openweathermap.org</domain>
</domain-config>
</network-security-config>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.appttude.h_mal.atlas_weather.monoWeather.ui.widget.WidgetLocationPermissionActivity"
android:initialKeyguardLayout="@layout/weather_app_widget"
android:initialLayout="@layout/weather_app_widget"
android:minHeight="110.0dp"
android:minWidth="320.0dp"
android:minResizeWidth="320.0dp"
android:minResizeHeight="110.0dp"
android:previewImage="@drawable/widget_screenshot"
android:updatePeriodMillis="1800000"
android:resizeMode="vertical"
android:widgetCategory="home_screen">
</appwidget-provider>