mirror of
https://github.com/hmalik144/Weather-apps.git
synced 2026-03-17 23:16:01 +00:00
- mid commit (broken)
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.notification
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
|
||||||
|
data class NotificationData(
|
||||||
|
val temp: String,
|
||||||
|
val icon: Bitmap
|
||||||
|
)
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.notification
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.TaskStackBuilder
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.MainActivity
|
||||||
|
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.displayToast
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by h_mal on 29/04/2018.
|
||||||
|
* Updated by h_mal on 27/11/2020
|
||||||
|
*/
|
||||||
|
const val NOTIFICATION_CHANNEL_ID = "my_notification_channel_1"
|
||||||
|
|
||||||
|
class NotificationReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
private val kodein = LateInitKodein()
|
||||||
|
private val helper: ServicesHelper by kodein.instance()
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
|
||||||
|
|
||||||
|
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
context.displayToast("Please enable location permissions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// notification validation
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pushNotif(context: Context?, weather: FullWeather) {
|
||||||
|
val notificationIntent = Intent(context, MainActivity::class.java)
|
||||||
|
|
||||||
|
val stackBuilder = TaskStackBuilder.create(context).apply {
|
||||||
|
addParentStack(MainActivity::class.java)
|
||||||
|
addNextIntent(notificationIntent)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
|
||||||
|
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
Notification.Builder(context, NOTIFICATION_CHANNEL_ID)
|
||||||
|
} else {
|
||||||
|
Notification.Builder(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notification = builder.setContentTitle("Weather App")
|
||||||
|
.setContentText(weather.current?.main + "°C")
|
||||||
|
.setSmallIcon(R.mipmap.ic_notif) //change icon
|
||||||
|
// .setLargeIcon(Icon.createWithResource(context, getImageResource(forecastItem.getCurrentForecast().getIconURL(), context)))
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pendingIntent).build()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
builder.setChannelId(NOTIFICATION_CHANNEL_ID)
|
||||||
|
}
|
||||||
|
val notificationManager = context!!.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.notify(0, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
|
abstract class BaseActivity : AppCompatActivity(){
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
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
|
||||||
|
|
||||||
|
abstract class BaseFragment: Fragment(){
|
||||||
|
|
||||||
|
// toggle visibility of progress spinner while async operations are taking place
|
||||||
|
fun progressBarStateObserver(progressBar: View) = Observer<Event<Boolean>> {
|
||||||
|
when(it.getContentIfNotHandled()){
|
||||||
|
true -> {
|
||||||
|
progressBar.show()
|
||||||
|
}
|
||||||
|
false -> {
|
||||||
|
progressBar.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// display a toast when operation fails
|
||||||
|
fun errorObserver() = Observer<Event<String>> {
|
||||||
|
it.getContentIfNotHandled()?.let { message ->
|
||||||
|
displayToast(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter.WeatherRecyclerAdapter
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.navigateTo
|
||||||
|
import kotlinx.android.synthetic.main.fragment_home.*
|
||||||
|
|
||||||
|
|
||||||
|
class WorldItemFragment : Fragment() {
|
||||||
|
private var param1: WeatherDisplay? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
param1 = WorldItemFragmentArgs.fromBundle(requireArguments()).weatherDisplay
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
// Inflate the layout for this fragment
|
||||||
|
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val recyclerAdapter = WeatherRecyclerAdapter {
|
||||||
|
val directions =
|
||||||
|
WorldItemFragmentDirections.actionWorldItemFragmentToFurtherDetailsFragment(it)
|
||||||
|
navigateTo(directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
param1?.let { recyclerAdapter.addCurrent(it) }
|
||||||
|
|
||||||
|
forecast_listview.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = recyclerAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.details
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
|
||||||
|
import kotlinx.android.synthetic.main.activity_further_info.*
|
||||||
|
|
||||||
|
|
||||||
|
private const val WEATHER = "param1"
|
||||||
|
/**
|
||||||
|
* A simple [Fragment] subclass.
|
||||||
|
* Use the [FurtherInfoFragment.newInstance] factory method to
|
||||||
|
* create an instance of this fragment.
|
||||||
|
*/
|
||||||
|
class FurtherInfoFragment : Fragment() {
|
||||||
|
private var param1: Forecast? = null
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
param1 = FurtherInfoFragmentArgs.fromBundle(requireArguments()).forecast
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.activity_further_info, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
maxtemp.text = param1?.mainTemp
|
||||||
|
averagetemp.text = param1?.averageTemp
|
||||||
|
minimumtemp.text = param1?.minorTemp
|
||||||
|
windtext.text = param1?.windText
|
||||||
|
preciptext.text = param1?.precipitation
|
||||||
|
humiditytext.text = param1?.humidity
|
||||||
|
uvtext.text = param1?.uvi
|
||||||
|
sunrisetext.text = param1?.sunrise
|
||||||
|
sunsettext.text = param1?.sunset
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.observe
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.BaseFragment
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter.WeatherRecyclerAdapter
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.displayToast
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.navigateTo
|
||||||
|
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
||||||
|
import com.appttude.h_mal.atlas_weather.viewmodel.MainViewModel
|
||||||
|
import kotlinx.android.synthetic.main.fragment_home.*
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.android.x.kodein
|
||||||
|
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple [Fragment] subclass.
|
||||||
|
* create an instance of this fragment.
|
||||||
|
*/
|
||||||
|
class HomeFragment : BaseFragment(), KodeinAware {
|
||||||
|
override val kodein by kodein()
|
||||||
|
private val factory by instance<ApplicationViewModelFactory>()
|
||||||
|
|
||||||
|
private val viewModel by activityViewModels<MainViewModel> { factory }
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
// Inflate the layout for this fragment
|
||||||
|
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val recyclerAdapter = WeatherRecyclerAdapter {
|
||||||
|
val directions =
|
||||||
|
HomeFragmentDirections.actionHomeFragmentToFurtherDetailsFragment(it)
|
||||||
|
navigateTo(directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
forecast_listview.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = recyclerAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
getPermissionResult(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION_PERMISSION_REQUEST){
|
||||||
|
viewModel.fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
swipe_refresh.apply {
|
||||||
|
setOnRefreshListener {
|
||||||
|
getPermissionResult(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION_PERMISSION_REQUEST){
|
||||||
|
viewModel.fetchData()
|
||||||
|
}
|
||||||
|
isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.weatherLiveData.observe(viewLifecycleOwner) {
|
||||||
|
recyclerAdapter.addCurrent(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
|
||||||
|
viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
|
||||||
|
|
||||||
|
viewModel.operationState.observe(viewLifecycleOwner){
|
||||||
|
swipe_refresh.isRefreshing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@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) {
|
||||||
|
viewModel.fetchData()
|
||||||
|
displayToast("Permission granted")
|
||||||
|
} else {
|
||||||
|
displayToast("Permission denied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
class EmptyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.loadImage
|
||||||
|
|
||||||
|
class ViewHolderCurrent(listItemView: View) : RecyclerView.ViewHolder(listItemView) {
|
||||||
|
|
||||||
|
var locationTV: TextView = listItemView.findViewById(R.id.location_main_4)
|
||||||
|
var conditionTV: TextView = listItemView.findViewById(R.id.condition_main_4)
|
||||||
|
var weatherIV: ImageView = listItemView.findViewById(R.id.icon_main_4)
|
||||||
|
var avgTempTV: TextView = listItemView.findViewById(R.id.temp_main_4)
|
||||||
|
var tempUnit: TextView = listItemView.findViewById(R.id.temp_unit_4)
|
||||||
|
|
||||||
|
fun bindData(weather: WeatherDisplay?){
|
||||||
|
locationTV.text = weather?.location
|
||||||
|
conditionTV.text = weather?.description
|
||||||
|
weatherIV.loadImage(weather?.iconURL)
|
||||||
|
avgTempTV.text = weather?.averageTemp?.toInt().toString()
|
||||||
|
tempUnit.text = weather?.unit
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.loadImage
|
||||||
|
|
||||||
|
class ViewHolderForecast(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var dateTV: TextView = itemView.findViewById(R.id.list_date)
|
||||||
|
var dayTV: TextView = itemView.findViewById(R.id.list_day)
|
||||||
|
var conditionTV: TextView = itemView.findViewById(R.id.list_condition)
|
||||||
|
var weatherIV: ImageView = itemView.findViewById(R.id.list_icon)
|
||||||
|
var mainTempTV: TextView = itemView.findViewById(R.id.list_main_temp)
|
||||||
|
var minorTempTV: TextView = itemView.findViewById(R.id.list_minor_temp)
|
||||||
|
|
||||||
|
fun bindView(forecast: Forecast?) {
|
||||||
|
dateTV.text = forecast?.date
|
||||||
|
dayTV.text = forecast?.day
|
||||||
|
conditionTV.text = forecast?.condition
|
||||||
|
weatherIV.loadImage(forecast?.weatherIcon)
|
||||||
|
mainTempTV.text = forecast?.mainTemp
|
||||||
|
minorTempTV.text = forecast?.minorTemp
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||||
|
|
||||||
|
internal class ViewHolderFurtherDetails(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var windSpeed: TextView = itemView.findViewById(R.id.windspeed)
|
||||||
|
var windDirection: TextView = itemView.findViewById(R.id.winddirection)
|
||||||
|
var precipitation: TextView = itemView.findViewById(R.id.precip_)
|
||||||
|
var humidity: TextView = itemView.findViewById(R.id.humidity_)
|
||||||
|
var clouds: TextView = itemView.findViewById(R.id.clouds_)
|
||||||
|
|
||||||
|
fun bindData(weather: WeatherDisplay?){
|
||||||
|
windSpeed.text = weather?.windSpeed
|
||||||
|
windDirection.text = weather?.windDirection
|
||||||
|
precipitation.text = weather?.precipitation
|
||||||
|
humidity.text = weather?.humidity
|
||||||
|
clouds.text = weather?.clouds
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.home.adapter
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.forecast.Forecast
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.generateView
|
||||||
|
|
||||||
|
class WeatherRecyclerAdapter(
|
||||||
|
val itemClick: (Forecast) -> Unit
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
var weather: WeatherDisplay? = null
|
||||||
|
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
fun addCurrent(current: WeatherDisplay){
|
||||||
|
weather = current
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
return when (getDataType(viewType)){
|
||||||
|
is ViewType.Empty -> {
|
||||||
|
val emptyViewHolder = View(parent.context)
|
||||||
|
EmptyViewHolder(emptyViewHolder)
|
||||||
|
}
|
||||||
|
is ViewType.Current -> {
|
||||||
|
val viewCurrent = parent.generateView(R.layout.list_item_current)
|
||||||
|
ViewHolderCurrent(viewCurrent)
|
||||||
|
}
|
||||||
|
is ViewType.Forecast -> {
|
||||||
|
val viewForecast = parent.generateView(R.layout.list_item_forecast)
|
||||||
|
ViewHolderForecast(viewForecast)
|
||||||
|
}
|
||||||
|
is ViewType.Further -> {
|
||||||
|
val viewFurther = parent.generateView(R.layout.list_item_further)
|
||||||
|
ViewHolderFurtherDetails(viewFurther)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ViewType{
|
||||||
|
object Empty : ViewType()
|
||||||
|
object Current : ViewType()
|
||||||
|
object Forecast : ViewType()
|
||||||
|
object Further : ViewType()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDataType(type: Int): ViewType {
|
||||||
|
return when (type){
|
||||||
|
0 -> ViewType.Empty
|
||||||
|
1 -> ViewType.Current
|
||||||
|
2 -> ViewType.Forecast
|
||||||
|
3 -> ViewType.Further
|
||||||
|
else -> ViewType.Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
if (weather == null) return 0
|
||||||
|
|
||||||
|
return when(position){
|
||||||
|
0 -> 1
|
||||||
|
in 1 until itemCount -2 -> 2
|
||||||
|
itemCount - 1 -> 3
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
when (getDataType(getItemViewType(position))){
|
||||||
|
is ViewType.Empty -> {
|
||||||
|
holder as EmptyViewHolder
|
||||||
|
}
|
||||||
|
is ViewType.Current -> {
|
||||||
|
val viewHolderCurrent = holder as ViewHolderCurrent
|
||||||
|
viewHolderCurrent.bindData(weather)
|
||||||
|
}
|
||||||
|
is ViewType.Forecast -> {
|
||||||
|
val viewHolderForecast = holder as ViewHolderForecast
|
||||||
|
|
||||||
|
weather?.forecast?.get(position - 1)?.let { i ->
|
||||||
|
viewHolderForecast.bindView(i)
|
||||||
|
viewHolderForecast.itemView.setOnClickListener {
|
||||||
|
itemClick(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ViewType.Further -> {
|
||||||
|
val viewHolderCurrent = holder as ViewHolderFurtherDetails
|
||||||
|
viewHolderCurrent.bindData(weather)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
if (weather == null) return 0
|
||||||
|
return 2 + (weather?.forecast?.size?: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.settings
|
||||||
|
|
||||||
|
import android.app.AlarmManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceActivity
|
||||||
|
import android.preference.PreferenceFragment
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.notification.NotificationReceiver
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.widget.NewAppWidget
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
|
class UnitSettingsActivity : PreferenceActivity() {
|
||||||
|
private var prefListener: OnSharedPreferenceChangeListener? = null
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
PreferenceManager.setDefaultValues(this, R.xml.prefs, false)
|
||||||
|
fragmentManager.beginTransaction().replace(android.R.id.content, MyPreferenceFragment()).commit()
|
||||||
|
|
||||||
|
//listener on changed sort order preference:
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
prefListener = OnSharedPreferenceChangeListener { _, key ->
|
||||||
|
if (key == "temp_units") {
|
||||||
|
val intent = Intent(baseContext, NewAppWidget::class.java)
|
||||||
|
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||||
|
val ids = AppWidgetManager.getInstance(application).getAppWidgetIds(ComponentName(application, NewAppWidget::class.java))
|
||||||
|
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
if (key == "notif_boolean") {
|
||||||
|
setupNotificationBroadcaster(baseContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == "widget_black_background"){
|
||||||
|
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
|
||||||
|
val widgetManager = AppWidgetManager.getInstance(this)
|
||||||
|
val ids = widgetManager.getAppWidgetIds(ComponentName(this, NewAppWidget::class.java))
|
||||||
|
AppWidgetManager.getInstance(this).notifyAppWidgetViewDataChanged(ids, R.id.whole_widget_view)
|
||||||
|
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefs.registerOnSharedPreferenceChangeListener(prefListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setupNotificationBroadcaster(context: Context) {
|
||||||
|
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
|
val notificationIntent = Intent(context, NotificationReceiver::class.java)
|
||||||
|
val broadcast = PendingIntent.getBroadcast(context, 100, notificationIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
val cal: Calendar = Calendar.getInstance()
|
||||||
|
cal.set(Calendar.HOUR_OF_DAY, 6)
|
||||||
|
cal.set(Calendar.MINUTE, 8)
|
||||||
|
cal.set(Calendar.SECOND, 5)
|
||||||
|
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, cal.timeInMillis, AlarmManager.INTERVAL_DAY, broadcast)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Override
|
||||||
|
// public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
|
||||||
|
// Log.i(TAG, "onSharedPreferenceChanged: " + s);
|
||||||
|
// if (s == "temp_units"){
|
||||||
|
// Intent intent = new Intent(getBaseContext(), NewAppWidget.class);
|
||||||
|
// intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
|
||||||
|
//
|
||||||
|
// int[] ids = AppWidgetManager.getInstance(getApplication()).getAppWidgetIds(new ComponentName(getApplication(), NewAppWidget.class));
|
||||||
|
// intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
|
||||||
|
// sendBroadcast(intent);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
class MyPreferenceFragment : PreferenceFragment() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
addPreferencesFromResource(R.xml.prefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.world
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.observe
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.BaseFragment
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.displayToast
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.goBack
|
||||||
|
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
||||||
|
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
|
||||||
|
import kotlinx.android.synthetic.main.activity_add_forecast.*
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.android.x.kodein
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
|
||||||
|
|
||||||
|
class AddLocationFragment : BaseFragment(), KodeinAware {
|
||||||
|
override val kodein by kodein()
|
||||||
|
private val factory by instance<ApplicationViewModelFactory>()
|
||||||
|
|
||||||
|
private val viewModel by viewModels<WorldViewModel> { factory }
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
// Inflate the layout for this fragment
|
||||||
|
return inflater.inflate(R.layout.activity_add_forecast, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
submit.setOnClickListener {
|
||||||
|
val locationName = location_name_tv.text?.trim()?.toString()
|
||||||
|
if (locationName.isNullOrBlank()){
|
||||||
|
submit.error = "Location cannot be blank"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
viewModel.fetchDataForSingleLocation(locationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
|
||||||
|
viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
|
||||||
|
|
||||||
|
viewModel.operationComplete.observe(viewLifecycleOwner) {
|
||||||
|
it?.getContentIfNotHandled()?.let {message ->
|
||||||
|
displayToast(message)
|
||||||
|
}
|
||||||
|
goBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.world
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.observe
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.world.WorldRecyclerAdapter
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.BaseFragment
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.world.WorldFragmentDirections
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.navigateTo
|
||||||
|
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
|
||||||
|
import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
|
||||||
|
import kotlinx.android.synthetic.main.fragment_add_location.*
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.android.x.kodein
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple [Fragment] subclass.
|
||||||
|
* create an instance of this fragment.
|
||||||
|
*/
|
||||||
|
class WorldFragment : BaseFragment(), KodeinAware {
|
||||||
|
override val kodein by kodein()
|
||||||
|
private val factory by instance<ApplicationViewModelFactory>()
|
||||||
|
|
||||||
|
val viewModel by viewModels<WorldViewModel> { factory }
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?): View? {
|
||||||
|
// Inflate the layout for this fragment
|
||||||
|
return inflater.inflate(R.layout.fragment_add_location, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val recyclerAdapter = WorldRecyclerAdapter{
|
||||||
|
val direction =
|
||||||
|
WorldFragmentDirections.actionWorldFragmentToWorldItemFragment(it)
|
||||||
|
navigateTo(direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
world_recycler.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = recyclerAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.weatherLiveData.observe(viewLifecycleOwner) {
|
||||||
|
recyclerAdapter.addCurrent(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
floatingActionButton.setOnClickListener{
|
||||||
|
navigateTo(R.id.action_worldFragment_to_addLocationFragment)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar2))
|
||||||
|
viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
viewModel.fetchAllLocations()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.ui.world
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.generateView
|
||||||
|
import com.appttude.h_mal.atlas_weather.utils.loadImage
|
||||||
|
|
||||||
|
class WorldRecyclerAdapter(
|
||||||
|
val itemClick: (WeatherDisplay) -> Unit
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
var weather: MutableList<WeatherDisplay> = mutableListOf()
|
||||||
|
|
||||||
|
fun addCurrent(current: List<WeatherDisplay>){
|
||||||
|
weather.clear()
|
||||||
|
weather.addAll(current)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
return when (getDataType(viewType)){
|
||||||
|
is ViewType.Empty -> {
|
||||||
|
val emptyViewHolder = View(parent.context)
|
||||||
|
EmptyViewHolder(emptyViewHolder)
|
||||||
|
}
|
||||||
|
is ViewType.Current -> {
|
||||||
|
val viewCurrent = parent.generateView(R.layout.db_list_item)
|
||||||
|
WorldHolderCurrent(viewCurrent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ViewType{
|
||||||
|
object Empty : ViewType()
|
||||||
|
object Current : ViewType()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDataType(type: Int): ViewType{
|
||||||
|
return when (type){
|
||||||
|
0 -> ViewType.Empty
|
||||||
|
1 -> ViewType.Current
|
||||||
|
else -> ViewType.Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return if (weather.isEmpty()) 0 else 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
when (getDataType(getItemViewType(position))){
|
||||||
|
is ViewType.Empty -> {
|
||||||
|
holder as EmptyViewHolder
|
||||||
|
|
||||||
|
}
|
||||||
|
is ViewType.Current -> {
|
||||||
|
val viewHolderCurrent = holder as WorldHolderCurrent
|
||||||
|
viewHolderCurrent.bindData(weather[position])
|
||||||
|
viewHolderCurrent.itemView.setOnClickListener {
|
||||||
|
itemClick.invoke(weather[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return if (weather.size == 0) 1 else weather.size
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class WorldHolderCurrent(listItemView: View) : RecyclerView.ViewHolder(listItemView) {
|
||||||
|
|
||||||
|
var locationTV: TextView = listItemView.findViewById(R.id.db_location)
|
||||||
|
var conditionTV: TextView = listItemView.findViewById(R.id.db_condition)
|
||||||
|
var weatherIV: ImageView = listItemView.findViewById(R.id.db_icon)
|
||||||
|
var avgTempTV: TextView = listItemView.findViewById(R.id.db_main_temp)
|
||||||
|
// var tempUnit: TextView = listItemView.findViewById(R.id.db_minor_temp)
|
||||||
|
|
||||||
|
fun bindData(weather: WeatherDisplay?){
|
||||||
|
locationTV.text = weather?.location
|
||||||
|
conditionTV.text = weather?.description
|
||||||
|
weatherIV.loadImage(weather?.iconURL)
|
||||||
|
avgTempTV.text = weather?.forecast?.get(0)?.mainTemp
|
||||||
|
// tempUnit.text = weather?.unit
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
internal class EmptyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.TaskStackBuilder
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
|
||||||
|
abstract class BaseWidgetClass : AppWidgetProvider(){
|
||||||
|
|
||||||
|
fun createRemoteView(context: Context, @LayoutRes id: Int): RemoteViews {
|
||||||
|
return RemoteViews(context.packageName, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AppWidgetProvider.createUpdatePendingIntent(context: Context, appWidgetId: Int): PendingIntent? {
|
||||||
|
val seconds = (System.currentTimeMillis() / 1000L).toInt()
|
||||||
|
val intentUpdate = Intent(context, this::class.java)
|
||||||
|
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||||
|
val idArray = intArrayOf(appWidgetId)
|
||||||
|
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
|
||||||
|
|
||||||
|
return PendingIntent.getBroadcast(
|
||||||
|
context, seconds, intentUpdate,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T: Activity> createClickingPendingIntent(context: Context, activityClass: Class<T>): PendingIntent {
|
||||||
|
val clickIntentTemplate = Intent(context, activityClass)
|
||||||
|
|
||||||
|
return TaskStackBuilder.create(context)
|
||||||
|
.addNextIntentWithParentStack(clickIntentTemplate)
|
||||||
|
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import android.widget.RemoteViewsService.RemoteViewsFactory
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.LateInitKodein
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
|
||||||
|
|
||||||
|
class MyWidgetRemoteViewsFactory(
|
||||||
|
private val context: Context,
|
||||||
|
val intent: Intent
|
||||||
|
) : RemoteViewsFactory{
|
||||||
|
private val TAG = "MyWidgetRemoteViewsFactory"
|
||||||
|
|
||||||
|
private val kodein = LateInitKodein()
|
||||||
|
private val helper : ServicesHelper by kodein.instance()
|
||||||
|
|
||||||
|
private var appWidgetId: Int? = 0
|
||||||
|
private var list: List<InnerWidgetData>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||||
|
AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||||
|
kodein.baseKodein = (context.applicationContext as KodeinAware).kodein
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {}
|
||||||
|
override fun onDataSetChanged() {
|
||||||
|
runBlocking {
|
||||||
|
list = helper.getWidgetInnerWeather()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onDestroy() {}
|
||||||
|
|
||||||
|
override fun getCount(): Int = list?.size ?: 5
|
||||||
|
|
||||||
|
override fun getViewAt(i: Int): RemoteViews {
|
||||||
|
val rv = RemoteViews(context.packageName, R.layout.widget_item)
|
||||||
|
|
||||||
|
if (list.isNullOrEmpty()) return rv
|
||||||
|
|
||||||
|
|
||||||
|
list?.get(i)?.let {
|
||||||
|
rv.setTextViewText(R.id.widget_item_day, it.date)
|
||||||
|
rv.setImageViewBitmap(R.id.widget_item_image, it.icon)
|
||||||
|
rv.setTextViewText(R.id.widget_item_temp_high, it.highTemp)
|
||||||
|
rv.setOnClickFillInIntent(R.id.widget_item_layout, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLoadingView(): RemoteViews {
|
||||||
|
return RemoteViews(context.packageName, R.layout.widget_item_loading)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getViewTypeCount(): Int = 1
|
||||||
|
|
||||||
|
|
||||||
|
override fun getItemId(i: Int): Long = i.toLong()
|
||||||
|
|
||||||
|
|
||||||
|
override fun hasStableIds(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import com.appttude.h_mal.atlas_weather.R
|
||||||
|
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.MainActivity
|
||||||
|
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
|
||||||
|
import com.appttude.h_mal.atlas_weather.model.widget.WidgetData
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.LateInitKodein
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of App Widget functionality.
|
||||||
|
*/
|
||||||
|
private val TAG = NewAppWidget::class.java.simpleName
|
||||||
|
class NewAppWidget : BaseWidgetClass() {
|
||||||
|
|
||||||
|
private val kodein = LateInitKodein()
|
||||||
|
private val helper : ServicesHelper by kodein.instance()
|
||||||
|
|
||||||
|
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_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
val results = helper.fetchData()
|
||||||
|
if (results) return@launch
|
||||||
|
val weatherWidgetCurrent = helper.getWidgetWeather()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main){
|
||||||
|
for (appWidgetId in appWidgetIds) {
|
||||||
|
val updatePendingIntent = createUpdatePendingIntent(context, appWidgetId)
|
||||||
|
val views = createRemoteView(context, R.layout.new_app_widget)
|
||||||
|
bindView(context, appWidgetId, views, updatePendingIntent, weatherWidgetCurrent)
|
||||||
|
}
|
||||||
|
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnabled(context: Context) {
|
||||||
|
try {
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val thisAppWidget = ComponentName(context.packageName, NewAppWidget::class.java.name)
|
||||||
|
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
|
||||||
|
onUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
|
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_listview)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "onEnabled: ", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisabled(context: Context) {
|
||||||
|
// Enter relevant functionality for when the last widget is disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action ==
|
||||||
|
AppWidgetManager.ACTION_APPWIDGET_UPDATE) {
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val thisAppWidget = ComponentName(context.packageName, NewAppWidget::class.java.name)
|
||||||
|
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget)
|
||||||
|
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_listview)
|
||||||
|
}
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createForecastListIntent(
|
||||||
|
context: Context,
|
||||||
|
appWidgetId: Int
|
||||||
|
): Intent {
|
||||||
|
return Intent(context, WidgetRemoteViewsService::class.java).apply {
|
||||||
|
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||||
|
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindView(
|
||||||
|
context: Context,
|
||||||
|
appWidgetId: Int,
|
||||||
|
views: RemoteViews,
|
||||||
|
updatePendingIntent: PendingIntent?,
|
||||||
|
weather: WidgetData?){
|
||||||
|
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
|
||||||
|
views.setInt(R.id.whole_widget_view, "setBackgroundColor", helper.getWidgetBackground())
|
||||||
|
|
||||||
|
weather?.let {
|
||||||
|
|
||||||
|
val intent = createForecastListIntent(
|
||||||
|
context,
|
||||||
|
appWidgetId
|
||||||
|
)
|
||||||
|
|
||||||
|
views.setRemoteAdapter(R.id.widget_listview, intent)
|
||||||
|
views.setTextViewText(R.id.widget_main_temp, it.currentTemp)
|
||||||
|
views.setTextViewText(R.id.widget_feel_temp, "°C")
|
||||||
|
views.setTextViewText(R.id.widget_current_location, it.location)
|
||||||
|
views.setImageViewResource(R.id.location_icon, R.drawable.location_flag)
|
||||||
|
// views.setImageViewBitmap(R.id.widget_current_icon, it.icon)
|
||||||
|
|
||||||
|
val clickPendingIntentTemplate = createClickingPendingIntent(context, MainActivity::class.java)
|
||||||
|
views.setPendingIntentTemplate(R.id.widget_listview, clickPendingIntentTemplate)
|
||||||
|
|
||||||
|
views.setOnClickPendingIntent(R.id.widget_current_icon, updatePendingIntent)
|
||||||
|
views.setOnClickPendingIntent(R.id.widget_current_location, updatePendingIntent)
|
||||||
|
|
||||||
|
// Instruct the widget manager to update the widget
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_listview)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "onPostExecute: weather is empty")
|
||||||
|
views.setTextViewText(R.id.widget_current_location, "Refresh")
|
||||||
|
views.setImageViewResource(R.id.widget_current_icon, R.drawable.widget_error_icon)
|
||||||
|
views.setImageViewResource(R.id.location_icon, R.drawable.refreshing)
|
||||||
|
views.setOnClickPendingIntent(R.id.widget_current_icon, updatePendingIntent)
|
||||||
|
views.setOnClickPendingIntent(R.id.widget_current_location, updatePendingIntent)
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.RemoteViewsService
|
||||||
|
|
||||||
|
class WidgetRemoteViewsService : RemoteViewsService() {
|
||||||
|
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
|
||||||
|
return MyWidgetRemoteViewsFactory(applicationContext, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.appttude.h_mal.atlas_weather.atlasWeather.widget
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.TaskStackBuilder
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
|
||||||
|
fun <T: Activity> createClickingPendingIntent(context: Context, activityClass: Class<T>): PendingIntent {
|
||||||
|
val clickIntentTemplate = Intent(context, activityClass)
|
||||||
|
|
||||||
|
return TaskStackBuilder.create(context)
|
||||||
|
.addNextIntentWithParentStack(clickIntentTemplate)
|
||||||
|
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
}
|
||||||
5
app/src/atlasWeather/java/com/appttude/h_mal/ui/Tabs.kt
Normal file
5
app/src/atlasWeather/java/com/appttude/h_mal/ui/Tabs.kt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package com.appttude.h_mal.ui
|
||||||
|
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
|
||||||
|
val tabs = setOf(R.id.nav_home, R.id.nav_world)
|
||||||
27
app/src/main/java/com/appttude/h_mal/application/AppClass.kt
Normal file
27
app/src/main/java/com/appttude/h_mal/application/AppClass.kt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.appttude.h_mal.application
|
||||||
|
|
||||||
|
import com.appttude.h_mal.data.location.LocationProviderImpl
|
||||||
|
import com.appttude.h_mal.data.network.NetworkModule
|
||||||
|
import com.appttude.h_mal.data.network.WeatherApi
|
||||||
|
import com.appttude.h_mal.data.network.interceptors.NetworkConnectionInterceptor
|
||||||
|
import com.appttude.h_mal.data.network.interceptors.QueryParamsInterceptor
|
||||||
|
import com.appttude.h_mal.data.network.networkUtils.loggingInterceptor
|
||||||
|
import com.appttude.h_mal.data.room.AppDatabase
|
||||||
|
|
||||||
|
const val LOCATION_PERMISSION_REQUEST = 505
|
||||||
|
|
||||||
|
class AppClass : BaseAppClass() {
|
||||||
|
|
||||||
|
override fun createNetworkModule(): WeatherApi {
|
||||||
|
return NetworkModule().invoke<WeatherApi>(
|
||||||
|
NetworkConnectionInterceptor(this),
|
||||||
|
QueryParamsInterceptor(),
|
||||||
|
loggingInterceptor
|
||||||
|
) as WeatherApi
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createLocationModule() = LocationProviderImpl(this)
|
||||||
|
|
||||||
|
override fun createRoomDatabase(): AppDatabase = AppDatabase(this)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.appttude.h_mal.application
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import com.appttude.h_mal.data.location.LocationProvider
|
||||||
|
import com.appttude.h_mal.data.network.WeatherApi
|
||||||
|
import com.appttude.h_mal.data.prefs.PreferenceProvider
|
||||||
|
import com.appttude.h_mal.data.repository.RepositoryImpl
|
||||||
|
import com.appttude.h_mal.data.repository.SettingsRepositoryImpl
|
||||||
|
import com.appttude.h_mal.data.room.AppDatabase
|
||||||
|
import com.appttude.h_mal.helper.ServicesHelper
|
||||||
|
import com.appttude.h_mal.viewmodel.ApplicationViewModelFactory
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import org.kodein.di.Kodein
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
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
|
||||||
|
|
||||||
|
abstract class BaseAppClass : Application(), KodeinAware {
|
||||||
|
|
||||||
|
// Kodein creation of modules to be retrieve within the app
|
||||||
|
override val kodein = Kodein.lazy {
|
||||||
|
import(androidXModule(this@BaseAppClass))
|
||||||
|
|
||||||
|
bind() from singleton { createNetworkModule() }
|
||||||
|
bind() from singleton { createLocationModule() }
|
||||||
|
|
||||||
|
bind() from singleton { Gson() }
|
||||||
|
bind() from singleton { createRoomDatabase() }
|
||||||
|
bind() from singleton { PreferenceProvider(instance()) }
|
||||||
|
bind() from singleton { RepositoryImpl(instance(), instance(), instance()) }
|
||||||
|
bind() from singleton { SettingsRepositoryImpl(instance()) }
|
||||||
|
bind() from singleton { ServicesHelper(instance(), instance(), instance()) }
|
||||||
|
bind() from provider { ApplicationViewModelFactory(instance(), instance()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun createNetworkModule(): WeatherApi
|
||||||
|
abstract fun createLocationModule(): LocationProvider
|
||||||
|
abstract fun createRoomDatabase(): AppDatabase
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.appttude.h_mal.data.location
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.location.Location
|
||||||
|
import com.appttude.h_mal.BuildConfig
|
||||||
|
import com.appttude.h_mal.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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.appttude.h_mal.data.location
|
||||||
|
|
||||||
|
import com.appttude.h_mal.model.types.LocationType
|
||||||
|
|
||||||
|
interface LocationProvider {
|
||||||
|
suspend fun getCurrentLatLong(): Pair<Double, Double>
|
||||||
|
fun getLatLongFromLocationName(location: String): Pair<Double, Double>
|
||||||
|
suspend fun getLocationNameFromLatLong(
|
||||||
|
lat: Double,
|
||||||
|
long: Double,
|
||||||
|
type: LocationType = LocationType.Town
|
||||||
|
): String
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package com.appttude.h_mal.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.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(
|
||||||
|
private 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_LOW_POWER, 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
|
||||||
|
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)
|
||||||
|
}
|
||||||
3
app/src/main/java/com/appttude/h_mal/data/network/Api.kt
Normal file
3
app/src/main/java/com/appttude/h_mal/data/network/Api.kt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package com.appttude.h_mal.data.network
|
||||||
|
|
||||||
|
interface Api
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.appttude.h_mal.data.network
|
||||||
|
|
||||||
|
import com.appttude.h_mal.data.network.networkUtils.buildOkHttpClient
|
||||||
|
import com.appttude.h_mal.data.network.networkUtils.createRetrofit
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
|
||||||
|
open class BaseNetworkModule {
|
||||||
|
// Declare the method we want/can change (no annotations)
|
||||||
|
open fun baseUrl() = "/"
|
||||||
|
|
||||||
|
inline fun <reified T: Api> invoke(
|
||||||
|
vararg interceptors: Interceptor
|
||||||
|
): Api {
|
||||||
|
|
||||||
|
val okHttpClient = buildOkHttpClient(*interceptors)
|
||||||
|
|
||||||
|
return createRetrofit(
|
||||||
|
baseUrl(),
|
||||||
|
okHttpClient,
|
||||||
|
T::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.appttude.h_mal.data.network
|
||||||
|
|
||||||
|
class NetworkModule : BaseNetworkModule() {
|
||||||
|
override fun baseUrl(): String = "https://api.openweathermap.org/data/2.5/"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.appttude.h_mal.data.network
|
||||||
|
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
abstract class ResponseUnwrap {
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
suspend fun <T : Any> responseUnwrap(
|
||||||
|
call: suspend () -> Response<T>
|
||||||
|
): T {
|
||||||
|
|
||||||
|
val response = call.invoke()
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
return response.body()!!
|
||||||
|
} else {
|
||||||
|
val error = response.errorBody()?.string()
|
||||||
|
|
||||||
|
val errorMessage = error?.let {
|
||||||
|
try {
|
||||||
|
JSONObject(it).getString("message")
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} ?: "Error Code: ${response.code()}"
|
||||||
|
|
||||||
|
throw IOException(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.appttude.h_mal.data.network
|
||||||
|
|
||||||
|
import com.appttude.h_mal.data.network.response.forecast.WeatherResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
|
|
||||||
|
interface WeatherApi: Api {
|
||||||
|
|
||||||
|
@GET("onecall?")
|
||||||
|
suspend fun getFromApi(
|
||||||
|
@Query("lat") query: String,
|
||||||
|
@Query("lon") lon: String,
|
||||||
|
@Query("exclude") exclude: String = "minutely",
|
||||||
|
@Query("units") units: String = "metric"
|
||||||
|
): Response<WeatherResponse>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.appttude.h_mal.data.network.interceptors
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.appttude.h_mal.utils.isInternetAvailable
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class NetworkConnectionInterceptor(
|
||||||
|
context: Context
|
||||||
|
) : NetworkInterceptor {
|
||||||
|
|
||||||
|
private val applicationContext = context.applicationContext
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
|
||||||
|
if (!isInternetAvailable(applicationContext)){
|
||||||
|
throw IOException("Make sure you have an active data connection")
|
||||||
|
}
|
||||||
|
return chain.proceed(chain.request())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.appttude.h_mal.data.network.interceptors
|
||||||
|
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
|
||||||
|
interface NetworkInterceptor : Interceptor
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.appttude.h_mal.data.network.interceptors
|
||||||
|
|
||||||
|
import com.appttude.h_mal.BuildConfig
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interceptor used to add default query parameters to api calls
|
||||||
|
*/
|
||||||
|
class QueryParamsInterceptor : Interceptor{
|
||||||
|
|
||||||
|
val id = BuildConfig.ParamOne
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val original = chain.request()
|
||||||
|
|
||||||
|
val url = original.url.newBuilder()
|
||||||
|
.addQueryParameter("appid", id)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Request customization: add request headers
|
||||||
|
val requestBuilder = original.newBuilder().url(url)
|
||||||
|
|
||||||
|
val request: Request = requestBuilder.build()
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.appttude.h_mal.data.network.networkUtils
|
||||||
|
|
||||||
|
import com.appttude.h_mal.data.network.interceptors.NetworkConnectionInterceptor
|
||||||
|
import com.appttude.h_mal.data.network.interceptors.NetworkInterceptor
|
||||||
|
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(
|
||||||
|
vararg interceptor: Interceptor,
|
||||||
|
timeoutSeconds: Long = 30L
|
||||||
|
): OkHttpClient {
|
||||||
|
|
||||||
|
val builder = OkHttpClient.Builder()
|
||||||
|
|
||||||
|
interceptor.forEach {
|
||||||
|
if (it is NetworkInterceptor) {
|
||||||
|
builder.addNetworkInterceptor(it)
|
||||||
|
} else {
|
||||||
|
builder.addInterceptor(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.appttude.h_mal.data.network.response.forecast
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class Current(
|
||||||
|
|
||||||
|
@field:SerializedName("sunrise")
|
||||||
|
val sunrise: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("temp")
|
||||||
|
val temp: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("visibility")
|
||||||
|
val visibility: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("uvi")
|
||||||
|
val uvi: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("pressure")
|
||||||
|
val pressure: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("clouds")
|
||||||
|
val clouds: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("feels_like")
|
||||||
|
val feelsLike: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("dt")
|
||||||
|
val dt: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("wind_deg")
|
||||||
|
val windDeg: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("dew_point")
|
||||||
|
val dewPoint: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("sunset")
|
||||||
|
val sunset: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("weather")
|
||||||
|
val weather: List<WeatherItem?>? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("humidity")
|
||||||
|
val humidity: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("wind_speed")
|
||||||
|
val windSpeed: Double? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.appttude.h_mal.data.network.response.forecast
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class DailyItem(
|
||||||
|
|
||||||
|
@field:SerializedName("sunrise")
|
||||||
|
val sunrise: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("temp")
|
||||||
|
val temp: Temp? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("uvi")
|
||||||
|
val uvi: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("pressure")
|
||||||
|
val pressure: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("clouds")
|
||||||
|
val clouds: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("feels_like")
|
||||||
|
val feelsLike: FeelsLike? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("dt")
|
||||||
|
val dt: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("pop")
|
||||||
|
val pop: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("wind_deg")
|
||||||
|
val windDeg: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("dew_point")
|
||||||
|
val dewPoint: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("sunset")
|
||||||
|
val sunset: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("weather")
|
||||||
|
val weather: List<WeatherItem?>? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("humidity")
|
||||||
|
val humidity: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("wind_speed")
|
||||||
|
val windSpeed: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("rain")
|
||||||
|
val rain: Double? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.appttude.h_mal.data.network.response.forecast
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class FeelsLike(
|
||||||
|
|
||||||
|
@field:SerializedName("eve")
|
||||||
|
val eve: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("night")
|
||||||
|
val night: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("day")
|
||||||
|
val day: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("morn")
|
||||||
|
val morn: Double? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.appttude.h_mal.data.network.response.forecast
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class Hour(
|
||||||
|
|
||||||
|
@field:SerializedName("sunrise")
|
||||||
|
val sunrise: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("temp")
|
||||||
|
val temp: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("visibility")
|
||||||
|
val visibility: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("uvi")
|
||||||
|
val uvi: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("pressure")
|
||||||
|
val pressure: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("clouds")
|
||||||
|
val clouds: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("feels_like")
|
||||||
|
val feelsLike: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("dt")
|
||||||
|
val dt: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("wind_deg")
|
||||||
|
val windDeg: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("dew_point")
|
||||||
|
val dewPoint: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("sunset")
|
||||||
|
val sunset: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("weather")
|
||||||
|
val weather: List<WeatherItem?>? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("humidity")
|
||||||
|
val humidity: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("wind_speed")
|
||||||
|
val windSpeed: Double? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.appttude.h_mal.data.network.response.forecast
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class Response(
|
||||||
|
|
||||||
|
@field:SerializedName("current")
|
||||||
|
val current: Current? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("timezone")
|
||||||
|
val timezone: String? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("timezone_offset")
|
||||||
|
val timezoneOffset: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("daily")
|
||||||
|
val daily: List<DailyItem?>? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("lon")
|
||||||
|
val lon: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("lat")
|
||||||
|
val lat: Double? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.appttude.h_mal.data.network.response.forecast
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class Temp(
|
||||||
|
|
||||||
|
@field:SerializedName("min")
|
||||||
|
val min: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("max")
|
||||||
|
val max: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("eve")
|
||||||
|
val eve: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("night")
|
||||||
|
val night: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("day")
|
||||||
|
val day: Double? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("morn")
|
||||||
|
val morn: Double? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.appttude.h_mal.data.network.response.forecast
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class WeatherItem(
|
||||||
|
|
||||||
|
@field:SerializedName("icon")
|
||||||
|
val icon: String? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("description")
|
||||||
|
val description: String? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("main")
|
||||||
|
val main: String? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("id")
|
||||||
|
val id: Int? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.appttude.h_mal.data.network.response.forecast
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class WeatherResponse(
|
||||||
|
|
||||||
|
@field:SerializedName("current")
|
||||||
|
val current: Current? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("timezone")
|
||||||
|
val timezone: String? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("timezone_offset")
|
||||||
|
val timezoneOffset: Int? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("hourly")
|
||||||
|
val hourly: List<Hour>? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("daily")
|
||||||
|
val daily: List<DailyItem>? = null,
|
||||||
|
|
||||||
|
@field:SerializedName("lon")
|
||||||
|
val lon: Double = 0.00,
|
||||||
|
|
||||||
|
@field:SerializedName("lat")
|
||||||
|
val lat: Double = 0.00
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.appttude.h_mal.data.prefs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.appttude.h_mal.data.room.entity.CURRENT_LOCATION
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared preferences to save & load last timestamp
|
||||||
|
*/
|
||||||
|
const val LOCATION_CONST = "location_"
|
||||||
|
class PreferenceProvider(
|
||||||
|
context: Context
|
||||||
|
){
|
||||||
|
|
||||||
|
private val appContext = context.applicationContext
|
||||||
|
|
||||||
|
private val preference: SharedPreferences
|
||||||
|
get() = PreferenceManager.getDefaultSharedPreferences(appContext)
|
||||||
|
|
||||||
|
fun saveLastSavedAt(locationName: String) {
|
||||||
|
preference.edit().putLong(
|
||||||
|
locationName,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastSavedAt(locationName: String): Long? {
|
||||||
|
return preference.getLong(locationName, 0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllKeys() = preference.all.keys.apply {
|
||||||
|
remove(CURRENT_LOCATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteLocation(locationName: String){
|
||||||
|
preference.edit().remove(locationName).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNotificationsEnabled(): Boolean = preference.getBoolean("notif_boolean", true)
|
||||||
|
|
||||||
|
fun setFirstTimeRun(){
|
||||||
|
preference.edit().putBoolean("FIRST_TIME_RUN", false).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isWidgetBlackground(): Boolean {
|
||||||
|
return preference.getBoolean("widget_black_background", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.appttude.h_mal.data.repository
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import com.appttude.h_mal.data.network.response.forecast.WeatherResponse
|
||||||
|
import com.appttude.h_mal.data.room.entity.EntityItem
|
||||||
|
|
||||||
|
interface Repository {
|
||||||
|
|
||||||
|
suspend fun getWeatherFromApi(lat: String, long: String): WeatherResponse
|
||||||
|
suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem)
|
||||||
|
suspend fun saveWeatherListToRoom(list: 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.appttude.h_mal.data.repository
|
||||||
|
|
||||||
|
import com.appttude.h_mal.data.network.ResponseUnwrap
|
||||||
|
import com.appttude.h_mal.data.network.WeatherApi
|
||||||
|
import com.appttude.h_mal.data.network.response.forecast.WeatherResponse
|
||||||
|
import com.appttude.h_mal.data.prefs.LOCATION_CONST
|
||||||
|
import com.appttude.h_mal.data.prefs.PreferenceProvider
|
||||||
|
import com.appttude.h_mal.data.room.AppDatabase
|
||||||
|
import com.appttude.h_mal.data.room.entity.EntityItem
|
||||||
|
import com.appttude.h_mal.utils.FALLBACK_TIME
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryImpl(
|
||||||
|
private val api: WeatherApi,
|
||||||
|
private val db: AppDatabase,
|
||||||
|
private val prefs: PreferenceProvider
|
||||||
|
) : Repository, ResponseUnwrap() {
|
||||||
|
|
||||||
|
override suspend fun getWeatherFromApi(
|
||||||
|
lat: String,
|
||||||
|
long: String
|
||||||
|
): WeatherResponse {
|
||||||
|
return responseUnwrap { api.getFromApi(lat, long) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveCurrentWeatherToRoom(entityItem: EntityItem){
|
||||||
|
db.getSimpleDao().upsertFullWeather(entityItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun saveWeatherListToRoom(
|
||||||
|
list: List<EntityItem>
|
||||||
|
){
|
||||||
|
db.getSimpleDao().upsertListOfFullWeather(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadRoomWeatherLiveData() = db.getSimpleDao().getAllFullWeatherWithoutCurrent()
|
||||||
|
|
||||||
|
override suspend fun loadWeatherList() : List<String>{
|
||||||
|
return db.getSimpleDao()
|
||||||
|
.getWeatherListWithoutCurrent()
|
||||||
|
.map { it.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("$LOCATION_CONST$locationName")
|
||||||
|
?: return true
|
||||||
|
val difference = System.currentTimeMillis() - lastSaved
|
||||||
|
|
||||||
|
return difference > FALLBACK_TIME
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveLastSavedAt(locationName: String) {
|
||||||
|
prefs.saveLastSavedAt("$LOCATION_CONST$locationName")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteSavedWeatherEntry(locationName: String): Boolean {
|
||||||
|
prefs.deleteLocation(locationName)
|
||||||
|
return db.getSimpleDao().deleteEntry(locationName) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSavedLocations(): List<String> {
|
||||||
|
return prefs.getAllKeys().toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getSingleWeather(locationName: String): EntityItem {
|
||||||
|
return db.getSimpleDao().getCurrentFullWeatherSingle(locationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.appttude.h_mal.data.repository
|
||||||
|
|
||||||
|
interface SettingsRepository {
|
||||||
|
fun isNotificationsEnabled(): Boolean
|
||||||
|
fun setFirstTime()
|
||||||
|
fun isBlackBackground(): Boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.appttude.h_mal.data.repository
|
||||||
|
|
||||||
|
import com.appttude.h_mal.data.prefs.PreferenceProvider
|
||||||
|
|
||||||
|
class SettingsRepositoryImpl(
|
||||||
|
private val prefs: PreferenceProvider
|
||||||
|
) : SettingsRepository{
|
||||||
|
|
||||||
|
override fun isNotificationsEnabled(): Boolean = prefs.isNotificationsEnabled()
|
||||||
|
|
||||||
|
override fun setFirstTime() = prefs.setFirstTimeRun()
|
||||||
|
|
||||||
|
override fun isBlackBackground() = prefs.isWidgetBlackground()
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.appttude.h_mal.data.room
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import com.appttude.h_mal.data.room.entity.EntityItem
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [EntityItem::class],
|
||||||
|
version = 1,
|
||||||
|
exportSchema = false
|
||||||
|
)
|
||||||
|
@TypeConverters(Converter::class)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun getSimpleDao(): WeatherDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var instance: AppDatabase? = null
|
||||||
|
private val LOCK = Any()
|
||||||
|
|
||||||
|
// create an instance of room database or use previously created instance
|
||||||
|
operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
|
||||||
|
instance ?: buildDatabase(context).also {
|
||||||
|
instance = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDatabase(context: Context) =
|
||||||
|
Room.databaseBuilder(
|
||||||
|
context.applicationContext,
|
||||||
|
AppDatabase::class.java,
|
||||||
|
"MyDatabase.db"
|
||||||
|
).addTypeConverter(Converter(context))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
27
app/src/main/java/com/appttude/h_mal/data/room/Converter.kt
Normal file
27
app/src/main/java/com/appttude/h_mal/data/room/Converter.kt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.appttude.h_mal.data.room
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.ProvidedTypeConverter
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import com.appttude.h_mal.model.weather.FullWeather
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import org.kodein.di.KodeinAware
|
||||||
|
import org.kodein.di.android.kodein
|
||||||
|
import org.kodein.di.generic.instance
|
||||||
|
|
||||||
|
@ProvidedTypeConverter
|
||||||
|
class Converter(context: Context) : KodeinAware {
|
||||||
|
override val kodein by kodein(context)
|
||||||
|
private val gson by instance<Gson>()
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fullWeatherToString(fullWeather: FullWeather): String {
|
||||||
|
return gson.toJson(fullWeather)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun stringToFullWeather(string: String): FullWeather {
|
||||||
|
return gson.fromJson(string, FullWeather::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
35
app/src/main/java/com/appttude/h_mal/data/room/WeatherDao.kt
Normal file
35
app/src/main/java/com/appttude/h_mal/data/room/WeatherDao.kt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package com.appttude.h_mal.data.room
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.appttude.h_mal.data.room.entity.CURRENT_LOCATION
|
||||||
|
import com.appttude.h_mal.data.room.entity.EntityItem
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface WeatherDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun upsertFullWeather(item: EntityItem)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun upsertListOfFullWeather(items: List<EntityItem>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM EntityItem WHERE id = :userId LIMIT 1")
|
||||||
|
fun getCurrentFullWeather(userId: String) : LiveData<EntityItem>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM EntityItem WHERE id = :userId LIMIT 1")
|
||||||
|
fun getCurrentFullWeatherSingle(userId: String) : EntityItem
|
||||||
|
|
||||||
|
@Query("SELECT * FROM EntityItem WHERE id != :id")
|
||||||
|
fun getAllFullWeatherWithoutCurrent(id: String = CURRENT_LOCATION) : LiveData<List<EntityItem>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM EntityItem WHERE id != :id")
|
||||||
|
fun getWeatherListWithoutCurrent(id: String = CURRENT_LOCATION) : List<EntityItem>
|
||||||
|
|
||||||
|
@Query("DELETE FROM EntityItem WHERE id = :userId")
|
||||||
|
fun deleteEntry(userId: String): Int
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.appttude.h_mal.data.room.entity
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import com.appttude.h_mal.model.weather.FullWeather
|
||||||
|
|
||||||
|
|
||||||
|
const val CURRENT_LOCATION = "CurrentLocation"
|
||||||
|
@Entity
|
||||||
|
data class EntityItem(
|
||||||
|
@PrimaryKey(autoGenerate = false)
|
||||||
|
val id: String,
|
||||||
|
val weather: FullWeather
|
||||||
|
)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.appttude.h_mal.helper
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import java.lang.reflect.ParameterizedType
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
object GenericsHelper {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <CLASS : Any> Any.getGenericClassAt(position: Int): KClass<CLASS> =
|
||||||
|
((javaClass.genericSuperclass as? ParameterizedType)
|
||||||
|
?.actualTypeArguments?.getOrNull(position) as? Class<CLASS>)
|
||||||
|
?.kotlin
|
||||||
|
?: throw IllegalStateException("Can not find class from generic argument")
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Create a view binding out of the the generic [VB]
|
||||||
|
// *
|
||||||
|
// * @sample inflateBindingByType(getGenericClassAt(0), layoutInflater)
|
||||||
|
// */
|
||||||
|
// fun <VB: ViewBinding> inflateBindingByType(
|
||||||
|
// genericClassAt: KClass<VB>,
|
||||||
|
// layoutInflater: LayoutInflater
|
||||||
|
// ): VB = try {
|
||||||
|
// @Suppress("UNCHECKED_CAST")
|
||||||
|
//
|
||||||
|
// genericClassAt.java.methods.first { viewBinding ->
|
||||||
|
// viewBinding.parameterTypes.size == 1
|
||||||
|
// && viewBinding.parameterTypes.getOrNull(0) == LayoutInflater::class.java
|
||||||
|
// }.invoke(null, layoutInflater) as VB
|
||||||
|
// } catch (exception: Exception) {
|
||||||
|
// println ("generic class failed at = $genericClassAt")
|
||||||
|
// exception.printStackTrace()
|
||||||
|
// throw IllegalStateException("Can not inflate binding from generic")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fun <VB: ViewBinding> LayoutInflater.inflateBindingByType(
|
||||||
|
// container: ViewGroup?,
|
||||||
|
// genericClassAt: KClass<VB>
|
||||||
|
// ): VB = try {
|
||||||
|
// @Suppress("UNCHECKED_CAST")
|
||||||
|
// genericClassAt.java.methods.first { inflateFun ->
|
||||||
|
// inflateFun.parameterTypes.size == 3
|
||||||
|
// && inflateFun.parameterTypes.getOrNull(0) == LayoutInflater::class.java
|
||||||
|
// && inflateFun.parameterTypes.getOrNull(1) == ViewGroup::class.java
|
||||||
|
// && inflateFun.parameterTypes.getOrNull(2) == Boolean::class.java
|
||||||
|
// }.invoke(null, this, container, false) as VB
|
||||||
|
// } catch (exception: Exception) {
|
||||||
|
// throw IllegalStateException("Can not inflate binding from generic")
|
||||||
|
// }
|
||||||
|
}
|
||||||
151
app/src/main/java/com/appttude/h_mal/helper/ServicesHelper.kt
Normal file
151
app/src/main/java/com/appttude/h_mal/helper/ServicesHelper.kt
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package com.appttude.h_mal.helper
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.annotation.RequiresPermission
|
||||||
|
import com.appttude.h_mal.data.location.LocationProvider
|
||||||
|
import com.appttude.h_mal.data.repository.Repository
|
||||||
|
import com.appttude.h_mal.data.repository.SettingsRepository
|
||||||
|
import com.appttude.h_mal.data.room.entity.CURRENT_LOCATION
|
||||||
|
import com.appttude.h_mal.data.room.entity.EntityItem
|
||||||
|
import com.appttude.h_mal.model.weather.FullWeather
|
||||||
|
import com.appttude.h_mal.model.widget.InnerWidgetCellData
|
||||||
|
import com.appttude.h_mal.model.widget.InnerWidgetData
|
||||||
|
import com.appttude.h_mal.model.widget.WidgetData
|
||||||
|
import com.appttude.h_mal.model.widget.WidgetWeatherCollection
|
||||||
|
import com.appttude.h_mal.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 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_COARSE_LOCATION)
|
||||||
|
suspend fun fetchData(): Boolean {
|
||||||
|
if (!repository.isSearchValid(CURRENT_LOCATION)) return false
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// Get location
|
||||||
|
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 = FullWeather(weather).apply {
|
||||||
|
temperatureUnit = "°C"
|
||||||
|
locationString = currentLocation
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getWidgetWeather(): WidgetData? {
|
||||||
|
return try {
|
||||||
|
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
|
||||||
|
val epoc = System.currentTimeMillis()
|
||||||
|
|
||||||
|
result.weather.let {
|
||||||
|
val bitmap = it.current?.icon
|
||||||
|
val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon)
|
||||||
|
val temp = it.current?.temp?.toInt().toString()
|
||||||
|
|
||||||
|
WidgetData(location, bitmap, temp, epoc)
|
||||||
|
}
|
||||||
|
} 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)?.forEach { dailyWeather ->
|
||||||
|
val day = dailyWeather.dt?.toSmallDayName()
|
||||||
|
val bitmap = withContext(Dispatchers.Main) {
|
||||||
|
getBitmapFromUrl(dailyWeather.icon)
|
||||||
|
}
|
||||||
|
val temp = dailyWeather.max?.toInt().toString()
|
||||||
|
|
||||||
|
val item = InnerWidgetData(day, bitmap, temp)
|
||||||
|
list.add(item)
|
||||||
|
}
|
||||||
|
list.toList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getWidgetWeatherCollection(): WidgetWeatherCollection? {
|
||||||
|
return try {
|
||||||
|
val result = repository.loadSingleCurrentWeatherFromRoom(CURRENT_LOCATION)
|
||||||
|
|
||||||
|
val widgetData = result.weather.let {
|
||||||
|
val bitmap = it.current?.icon
|
||||||
|
val location = locationProvider.getLocationNameFromLatLong(it.lat, it.lon)
|
||||||
|
val temp = it.current?.temp?.toInt().toString()
|
||||||
|
val epoc = System.currentTimeMillis()
|
||||||
|
|
||||||
|
WidgetData(location, bitmap, temp, epoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
val list = mutableListOf<InnerWidgetCellData>()
|
||||||
|
|
||||||
|
result.weather.daily?.drop(1)?.dropLast(2)?.forEach { dailyWeather ->
|
||||||
|
val day = dailyWeather.dt?.toSmallDayName()
|
||||||
|
val icon = dailyWeather.icon
|
||||||
|
val temp = dailyWeather.max?.toInt().toString()
|
||||||
|
|
||||||
|
val item = InnerWidgetCellData(day, icon, temp)
|
||||||
|
list.add(item)
|
||||||
|
}
|
||||||
|
list.toList()
|
||||||
|
|
||||||
|
WidgetWeatherCollection(widgetData, list)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
Color.BLACK
|
||||||
|
else
|
||||||
|
Color.TRANSPARENT
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.appttude.h_mal.model.forecast
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.appttude.h_mal.model.weather.DailyWeather
|
||||||
|
import com.appttude.h_mal.utils.toDayName
|
||||||
|
import com.appttude.h_mal.utils.toDayString
|
||||||
|
import com.appttude.h_mal.utils.toTime
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
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 {
|
||||||
|
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.appttude.h_mal.model.forecast
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.appttude.h_mal.data.room.entity.EntityItem
|
||||||
|
import com.appttude.h_mal.model.weather.Hour
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
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?
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.appttude.h_mal.model.types
|
||||||
|
|
||||||
|
enum class LocationType{
|
||||||
|
City,
|
||||||
|
Town
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.appttude.h_mal.model.weather
|
||||||
|
|
||||||
|
import com.appttude.h_mal.data.network.response.forecast.Current
|
||||||
|
import com.appttude.h_mal.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
|
||||||
|
) {
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.appttude.h_mal.model.weather
|
||||||
|
|
||||||
|
import com.appttude.h_mal.data.network.response.forecast.DailyItem
|
||||||
|
import com.appttude.h_mal.utils.generateIconUrlString
|
||||||
|
|
||||||
|
|
||||||
|
data class DailyWeather(
|
||||||
|
val dt: Int?,
|
||||||
|
val sunrise: Int?,
|
||||||
|
val sunset: Int?,
|
||||||
|
val min: Double? = null,
|
||||||
|
val max: Double? = null,
|
||||||
|
val average: Double? = null,
|
||||||
|
var feelsLike: Double?,
|
||||||
|
val pressure: Int?,
|
||||||
|
val humidity: Int?,
|
||||||
|
val dewPoint: Double?,
|
||||||
|
val windSpeed: Double?,
|
||||||
|
val windDeg: Int?,
|
||||||
|
val icon: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val main: String? = null,
|
||||||
|
val id: Int? = null,
|
||||||
|
val clouds: Int?,
|
||||||
|
val pop: Double?,
|
||||||
|
val uvi: Double?,
|
||||||
|
val rain: Double?
|
||||||
|
){
|
||||||
|
|
||||||
|
constructor(dailyItem: DailyItem): this(
|
||||||
|
dailyItem.dt,
|
||||||
|
dailyItem.sunrise,
|
||||||
|
dailyItem.sunset,
|
||||||
|
dailyItem.temp?.min,
|
||||||
|
dailyItem.temp?.max,
|
||||||
|
dailyItem.temp?.day,
|
||||||
|
dailyItem.feelsLike?.day,
|
||||||
|
dailyItem.pressure,
|
||||||
|
dailyItem.humidity,
|
||||||
|
dailyItem.dewPoint,
|
||||||
|
dailyItem.windSpeed,
|
||||||
|
dailyItem.windDeg,
|
||||||
|
generateIconUrlString(dailyItem.weather?.getOrNull(0)?.icon),
|
||||||
|
dailyItem.weather?.get(0)?.description,
|
||||||
|
dailyItem.weather?.get(0)?.main,
|
||||||
|
dailyItem.weather?.get(0)?.id,
|
||||||
|
dailyItem.clouds,
|
||||||
|
dailyItem.pop,
|
||||||
|
dailyItem.uvi,
|
||||||
|
dailyItem.rain
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.appttude.h_mal.model.weather
|
||||||
|
|
||||||
|
import com.appttude.h_mal.data.network.response.forecast.WeatherResponse
|
||||||
|
|
||||||
|
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,
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
21
app/src/main/java/com/appttude/h_mal/model/weather/Hour.kt
Normal file
21
app/src/main/java/com/appttude/h_mal/model/weather/Hour.kt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package com.appttude.h_mal.model.weather
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.appttude.h_mal.utils.generateIconUrlString
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import com.appttude.h_mal.data.network.response.forecast.Hour as ForecastHour
|
||||||
|
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Hour(
|
||||||
|
val dt: Int? = null,
|
||||||
|
val temp: Double? = null,
|
||||||
|
val icon: String? = null
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
constructor(hour: ForecastHour) : this(
|
||||||
|
hour.dt,
|
||||||
|
hour.temp,
|
||||||
|
generateIconUrlString(hour.weather?.getOrNull(0)?.icon)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.appttude.h_mal.model.widget
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
|
||||||
|
data class WidgetData(
|
||||||
|
val location: String?,
|
||||||
|
val icon: String?,
|
||||||
|
val currentTemp: String?,
|
||||||
|
val timeStamp: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InnerWidgetData(
|
||||||
|
val date: String?,
|
||||||
|
val icon: Bitmap?,
|
||||||
|
val highTemp: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
data class InnerWidgetCellData(
|
||||||
|
val date: String?,
|
||||||
|
val icon: String?,
|
||||||
|
val highTemp: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class WidgetWeatherCollection(
|
||||||
|
val widgetData: WidgetData,
|
||||||
|
val forecast: List<InnerWidgetCellData>
|
||||||
|
)
|
||||||
53
app/src/main/java/com/appttude/h_mal/ui/MainActivity.kt
Normal file
53
app/src/main/java/com/appttude/h_mal/ui/MainActivity.kt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package com.appttude.h_mal.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.R
|
||||||
|
import com.appttude.h_mal.monoWeather.ui.settings.UnitSettingsActivity
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
app/src/main/java/com/appttude/h_mal/utils/Constants.kt
Normal file
3
app/src/main/java/com/appttude/h_mal/utils/Constants.kt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package com.appttude.h_mal.utils
|
||||||
|
|
||||||
|
val FALLBACK_TIME: Long = 300000L
|
||||||
24
app/src/main/java/com/appttude/h_mal/utils/Event.kt
Normal file
24
app/src/main/java/com/appttude/h_mal/utils/Event.kt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package com.appttude.h_mal.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used with livedata<T> to make observation lifecycle aware
|
||||||
|
* Display livedata response only once
|
||||||
|
*/
|
||||||
|
open class Event<out T>(private val content: T) {
|
||||||
|
|
||||||
|
var hasBeenHandled = false
|
||||||
|
private set // Allow external read but not write
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content and prevents its use again.
|
||||||
|
*/
|
||||||
|
fun getContentIfNotHandled(): T? {
|
||||||
|
return if (hasBeenHandled) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
hasBeenHandled = true
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.appttude.h_mal.utils
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
inline fun <reified T> parcelableCreator(
|
||||||
|
crossinline create: (Parcel) -> T) =
|
||||||
|
object : Parcelable.Creator<T> {
|
||||||
|
override fun createFromParcel(source: Parcel) = create(source)
|
||||||
|
override fun newArray(size: Int) = arrayOfNulls<T>(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun <T : Any?> tryOrNullSuspended(
|
||||||
|
call: suspend () -> T?
|
||||||
|
): T? {
|
||||||
|
|
||||||
|
return try {
|
||||||
|
call.invoke()
|
||||||
|
}catch (e: Exception){
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : Any?> tryOrNull(
|
||||||
|
call: () -> T?
|
||||||
|
): T? {
|
||||||
|
|
||||||
|
return try {
|
||||||
|
call.invoke()
|
||||||
|
}catch (e: Exception){
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a suspend function
|
||||||
|
* @param call - the body of the lambda
|
||||||
|
*
|
||||||
|
* @sample
|
||||||
|
* fun getNumber() = 2
|
||||||
|
* suspend fun getSuspendNumber{ getNumber() }
|
||||||
|
*
|
||||||
|
* Both equal 2.
|
||||||
|
*/
|
||||||
|
suspend fun <T: Any> createSuspend(
|
||||||
|
call: () -> T?
|
||||||
|
): T?{
|
||||||
|
|
||||||
|
return suspendCoroutine { cont ->
|
||||||
|
cont.resume(call())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.appttude.h_mal.utils
|
||||||
|
|
||||||
|
fun printToLog(msg: String) {
|
||||||
|
println("widget monitoring: $msg")
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.appttude.h_mal.utils
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.NavDirections
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
|
||||||
|
fun Fragment.navigateToFragment(newFragment: Fragment){
|
||||||
|
childFragmentManager.beginTransaction()
|
||||||
|
.add(R.id.container, newFragment)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun View.navigateTo(navigationId: Int) {
|
||||||
|
Navigation.findNavController(this).navigate(navigationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.navigateTo(navDirections: NavDirections) {
|
||||||
|
Navigation.findNavController(this).navigate(navDirections)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Fragment.navigateTo(navigationId: Int) {
|
||||||
|
Navigation.findNavController(requireView()).navigate(navigationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Fragment.navigateTo(navDirections: NavDirections) {
|
||||||
|
Navigation.findNavController(requireView()).navigate(navDirections)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Fragment.goBack() = Navigation.findNavController(requireView()).popBackStack()
|
||||||
23
app/src/main/java/com/appttude/h_mal/utils/NetworkUtils.kt
Normal file
23
app/src/main/java/com/appttude/h_mal/utils/NetworkUtils.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.appttude.h_mal.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
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.appttude.h_mal.utils
|
||||||
|
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.OffsetTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
fun Int.toDayString(): String {
|
||||||
|
return try {
|
||||||
|
val date = Date(this.makeMilliseconds())
|
||||||
|
val format = SimpleDateFormat("MMM d", Locale.getDefault())
|
||||||
|
format.format(date)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
"Unable to parse date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.makeMilliseconds(): Long = this * 1000L
|
||||||
|
|
||||||
|
fun Int.toDayName(): String {
|
||||||
|
return try {
|
||||||
|
val date = Date(this.makeMilliseconds())
|
||||||
|
val format = SimpleDateFormat("EEEE", Locale.getDefault())
|
||||||
|
format.format(date)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
"Unable to parse date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.toSmallDayName(): String {
|
||||||
|
return try {
|
||||||
|
val date = Date(this.makeMilliseconds())
|
||||||
|
val format = SimpleDateFormat("EEE", Locale.getDefault())
|
||||||
|
format.format(date)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
"Unable to parse date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int?.toTime(): String? {
|
||||||
|
return this?.makeMilliseconds()?.let {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
OffsetTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC).format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||||
|
} else {
|
||||||
|
val date = Date(it)
|
||||||
|
val format = SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||||
|
format.format(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
12
app/src/main/java/com/appttude/h_mal/utils/StringUtils.kt
Normal file
12
app/src/main/java/com/appttude/h_mal/utils/StringUtils.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package com.appttude.h_mal.utils
|
||||||
|
|
||||||
|
|
||||||
|
fun generateIconUrlString(icon: String?): String?{
|
||||||
|
return icon?.let {
|
||||||
|
StringBuilder()
|
||||||
|
.append("http://openweathermap.org/img/wn/")
|
||||||
|
.append(it)
|
||||||
|
.append("@2x.png")
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/src/main/java/com/appttude/h_mal/utils/ViewUtils.kt
Normal file
46
app/src/main/java/com/appttude/h_mal/utils/ViewUtils.kt
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package com.appttude.h_mal.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
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.squareup.picasso.Picasso
|
||||||
|
|
||||||
|
fun View.show() {
|
||||||
|
this.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.hide() {
|
||||||
|
this.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.displayToast(message: String) {
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Fragment.displayToast(message: String) {
|
||||||
|
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ViewGroup.generateView(layoutId: Int): View = LayoutInflater
|
||||||
|
.from(context)
|
||||||
|
.inflate(layoutId, this, false)
|
||||||
|
|
||||||
|
fun ImageView.loadImage(url: String?){
|
||||||
|
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() {
|
||||||
|
val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?
|
||||||
|
imm?.hideSoftInputFromWindow(view?.windowToken, 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.appttude.h_mal.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.appttude.h_mal.data.location.LocationProvider
|
||||||
|
import com.appttude.h_mal.data.repository.RepositoryImpl
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationViewModelFactory(
|
||||||
|
private val locationProvider: LocationProvider,
|
||||||
|
private val repository: RepositoryImpl
|
||||||
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
with(modelClass){
|
||||||
|
return when{
|
||||||
|
isAssignableFrom(WorldViewModel::class.java) -> WorldViewModel(locationProvider, repository)
|
||||||
|
isAssignableFrom(MainViewModel::class.java) -> MainViewModel(locationProvider, repository)
|
||||||
|
else -> throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.appttude.h_mal.viewmodel
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.annotation.RequiresPermission
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.appttude.h_mal.data.location.LocationProvider
|
||||||
|
import com.appttude.h_mal.data.repository.Repository
|
||||||
|
import com.appttude.h_mal.data.room.entity.CURRENT_LOCATION
|
||||||
|
import com.appttude.h_mal.data.room.entity.EntityItem
|
||||||
|
import com.appttude.h_mal.model.forecast.WeatherDisplay
|
||||||
|
import com.appttude.h_mal.utils.Event
|
||||||
|
import com.appttude.h_mal.viewmodel.baseViewModels.WeatherViewModel
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class MainViewModel(
|
||||||
|
private val locationProvider: LocationProvider,
|
||||||
|
private val repository: Repository
|
||||||
|
): WeatherViewModel(){
|
||||||
|
|
||||||
|
val weatherLiveData = MutableLiveData<WeatherDisplay>()
|
||||||
|
|
||||||
|
val operationState = MutableLiveData<Event<Boolean>>()
|
||||||
|
val operationError = MutableLiveData<Event<String>>()
|
||||||
|
val operationRefresh = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
repository.loadCurrentWeatherFromRoom(CURRENT_LOCATION).observeForever {
|
||||||
|
it?.let {
|
||||||
|
val weather = WeatherDisplay(it)
|
||||||
|
weatherLiveData.postValue(weather)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresPermission(value = Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||||
|
fun fetchData(){
|
||||||
|
if (!repository.isSearchValid(CURRENT_LOCATION)){
|
||||||
|
operationRefresh.postValue(Event(false))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
operationState.postValue(Event(true))
|
||||||
|
try {
|
||||||
|
// Get location
|
||||||
|
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
|
||||||
|
repository.saveLastSavedAt(CURRENT_LOCATION)
|
||||||
|
repository.saveCurrentWeatherToRoom(entityItem)
|
||||||
|
}catch (e: Exception){
|
||||||
|
operationError.postValue(Event(e.message!!))
|
||||||
|
}finally {
|
||||||
|
operationState.postValue(Event(false))
|
||||||
|
operationRefresh.postValue(Event(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
165
app/src/main/java/com/appttude/h_mal/viewmodel/WorldViewModel.kt
Normal file
165
app/src/main/java/com/appttude/h_mal/viewmodel/WorldViewModel.kt
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package com.appttude.h_mal.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import com.appttude.h_mal.data.location.LocationProvider
|
||||||
|
import com.appttude.h_mal.data.network.response.forecast.WeatherResponse
|
||||||
|
import com.appttude.h_mal.data.repository.Repository
|
||||||
|
import com.appttude.h_mal.data.room.entity.EntityItem
|
||||||
|
import com.appttude.h_mal.model.forecast.WeatherDisplay
|
||||||
|
import com.appttude.h_mal.model.types.LocationType
|
||||||
|
import com.appttude.h_mal.utils.Event
|
||||||
|
import com.appttude.h_mal.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
|
||||||
|
) : 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.loadRoomWeatherLiveData()
|
||||||
|
|
||||||
|
init {
|
||||||
|
weatherListLiveData.observeForever {
|
||||||
|
val list = it.map { data ->
|
||||||
|
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
|
||||||
|
if (repository.getSavedLocations().contains(locationName)){
|
||||||
|
operationError.postValue(Event("$locationName already exists"))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get weather from api
|
||||||
|
val entityItem = createWeatherEntity(locationName)
|
||||||
|
|
||||||
|
// retrieved location name
|
||||||
|
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
|
||||||
|
repository.saveCurrentWeatherToRoom(entityItem)
|
||||||
|
repository.saveLastSavedAt(retrievedLocation)
|
||||||
|
operationComplete.postValue(Event("$retrievedLocation saved"))
|
||||||
|
|
||||||
|
} catch (e: IOException) {
|
||||||
|
operationError.postValue(Event(e.message!!))
|
||||||
|
} finally {
|
||||||
|
operationState.postValue(Event(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>()
|
||||||
|
repository.loadWeatherList().forEach { locationName ->
|
||||||
|
// If search not valid move onto next in loop
|
||||||
|
if (!repository.isSearchValid(locationName)) return@forEach
|
||||||
|
|
||||||
|
try {
|
||||||
|
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 {
|
||||||
|
operationState.postValue(Event(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteLocation(locationName: String){
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
operationState.postValue(Event(true))
|
||||||
|
try {
|
||||||
|
val success = repository.deleteSavedWeatherEntry(locationName)
|
||||||
|
if (!success){
|
||||||
|
operationError.postValue(Event("Failed to delete"))
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
operationError.postValue(Event(e.message!!))
|
||||||
|
} finally {
|
||||||
|
operationState.postValue(Event(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.appttude.h_mal.viewmodel.baseViewModels
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.appttude.h_mal.data.network.response.forecast.WeatherResponse
|
||||||
|
import com.appttude.h_mal.data.room.entity.EntityItem
|
||||||
|
import com.appttude.h_mal.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.appttude.h_mal.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?) {}
|
||||||
|
}
|
||||||
32
app/src/main/java/com/appttude/h_mal/widget/NewAppWidget.kt
Normal file
32
app/src/main/java/com/appttude/h_mal/widget/NewAppWidget.kt
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package com.appttude.h_mal.widget
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import com.appttude.h_mal.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package com.appttude.h_mal.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.R
|
||||||
|
import com.appttude.h_mal.widget.WidgetState.*
|
||||||
|
import com.appttude.h_mal.widget.WidgetState.Companion.getWidgetState
|
||||||
|
import com.appttude.h_mal.helper.ServicesHelper
|
||||||
|
import com.appttude.h_mal.model.widget.InnerWidgetCellData
|
||||||
|
import com.appttude.h_mal.model.widget.WidgetWeatherCollection
|
||||||
|
import com.appttude.h_mal.ui.MainActivity
|
||||||
|
import com.appttude.h_mal.utils.isInternetAvailable
|
||||||
|
import com.appttude.h_mal.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/src/main/java/com/appttude/h_mal/widget/WidgetState.kt
Normal file
28
app/src/main/java/com/appttude/h_mal/widget/WidgetState.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package com.appttude.h_mal.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.dialog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.Html
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
|
interface DeclarationBuilder{
|
||||||
|
val link: String
|
||||||
|
val message: String
|
||||||
|
|
||||||
|
fun Context.readFromResources(@StringRes id: Int) = resources.getString(id)
|
||||||
|
|
||||||
|
fun buildMessage(): CharSequence? {
|
||||||
|
val link1 = "<font color='blue'><a href=\"$link\">here</a></font>"
|
||||||
|
val message = "$message See my privacy policy: $link1"
|
||||||
|
return Html.fromHtml(message, Html.FROM_HTML_MODE_LEGACY)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.dialog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsDeclarationDialog(context: Context) : BaseDeclarationDialog(context) {
|
||||||
|
|
||||||
|
override val link: String = "https://sites.google.com/view/hmaldev/home/monochrome"
|
||||||
|
override val message: String = "Hi, thank you for downloading my app. Google play isn't letting me upload my app to the Playstore until I have a privacy declaration :(. My app is basically used to demonstrate my code=ing to potential employers and others. I do NOT store or process any information. The location permission in the app is there just to provide the end user with weather data."
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BaseDeclarationDialog(val context: Context): DeclarationBuilder {
|
||||||
|
abstract override val link: String
|
||||||
|
abstract override val message: String
|
||||||
|
|
||||||
|
lateinit var dialog: AlertDialog
|
||||||
|
|
||||||
|
fun showDialog(agreeCallback: () -> Unit = { }, disagreeCallback: () -> Unit = { }) {
|
||||||
|
val myMessage = buildMessage()
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(context)
|
||||||
|
.setPositiveButton("agree") { _, _ ->
|
||||||
|
agreeCallback()
|
||||||
|
}
|
||||||
|
.setNegativeButton("disagree") { _, _ ->
|
||||||
|
disagreeCallback()
|
||||||
|
}
|
||||||
|
.setMessage(myMessage)
|
||||||
|
.setCancelable(false)
|
||||||
|
|
||||||
|
dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
// Make the textview clickable. Must be called after show()
|
||||||
|
val msgTxt = dialog.findViewById<View>(android.R.id.message) as TextView?
|
||||||
|
msgTxt?.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismiss() = dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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.application.LOCATION_PERMISSION_REQUEST
|
||||||
|
import com.appttude.h_mal.utils.Event
|
||||||
|
import com.appttude.h_mal.utils.displayToast
|
||||||
|
import com.appttude.h_mal.utils.hide
|
||||||
|
import com.appttude.h_mal.utils.show
|
||||||
|
import com.appttude.h_mal.viewmodel.ApplicationViewModelFactory
|
||||||
|
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() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
|
||||||
|
class EmptyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
|
||||||
|
val icon: ImageView = itemView.findViewById(R.id.icon)
|
||||||
|
var bodyTV: TextView = itemView.findViewById(R.id.body_text)
|
||||||
|
var headerTV: TextView = itemView.findViewById(R.id.header_text)
|
||||||
|
|
||||||
|
fun bindData(@DrawableRes imageRes: Int?,header: String, body: String){
|
||||||
|
imageRes?.let { icon.setImageResource(it) }
|
||||||
|
headerTV.text = header
|
||||||
|
bodyTV.text = body
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui
|
||||||
|
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
|
||||||
|
val tabs = setOf(R.id.nav_home, R.id.nav_world)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui
|
||||||
|
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.monoWeather.ui.home.adapter.WeatherRecyclerAdapter
|
||||||
|
import com.appttude.h_mal.utils.navigateTo
|
||||||
|
import com.appttude.h_mal.viewmodel.WorldViewModel
|
||||||
|
import kotlinx.android.synthetic.main.fragment_home.*
|
||||||
|
|
||||||
|
|
||||||
|
class WorldItemFragment : BaseFragment(R.layout.fragment_home) {
|
||||||
|
|
||||||
|
private val viewModel by getFragmentViewModel<WorldViewModel>()
|
||||||
|
private var param1: String? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
param1 = WorldItemFragmentArgs.fromBundle(requireArguments()).locationName
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val recyclerAdapter = WeatherRecyclerAdapter {
|
||||||
|
val directions =
|
||||||
|
WorldItemFragmentDirections.actionWorldItemFragmentToFurtherDetailsFragment(it)
|
||||||
|
navigateTo(directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
param1?.let { viewModel.getSingleLocation(it) }
|
||||||
|
|
||||||
|
viewModel.singleWeatherLiveData.observe(viewLifecycleOwner, Observer {
|
||||||
|
recyclerAdapter.addCurrent(it)
|
||||||
|
swipe_refresh.isRefreshing = false
|
||||||
|
})
|
||||||
|
|
||||||
|
forecast_listview.apply {
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = recyclerAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
swipe_refresh.apply {
|
||||||
|
setOnRefreshListener {
|
||||||
|
param1?.let {
|
||||||
|
viewModel.fetchDataForSingleLocation(it)
|
||||||
|
isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
|
||||||
|
viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
|
||||||
|
viewModel.operationRefresh.observe(viewLifecycleOwner, refreshObserver(swipe_refresh))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.details
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.model.forecast.Forecast
|
||||||
|
import kotlinx.android.synthetic.main.activity_further_info.*
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple [Fragment] subclass.
|
||||||
|
* Use the [FurtherInfoFragment.newInstance] factory method to
|
||||||
|
* create an instance of this fragment.
|
||||||
|
*/
|
||||||
|
class FurtherInfoFragment : Fragment() {
|
||||||
|
private var param1: Forecast? = null
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
param1 = FurtherInfoFragmentArgs.fromBundle(requireArguments()).forecast
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.activity_further_info, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
maxtemp.text = param1?.mainTemp.appendWith(requireContext().getString(R.string.degrees))
|
||||||
|
averagetemp.text = param1?.averageTemp.appendWith(requireContext().getString(R.string.degrees))
|
||||||
|
minimumtemp.text = param1?.minorTemp.appendWith(requireContext().getString(R.string.degrees))
|
||||||
|
windtext.text = param1?.windText.appendWith(" km")
|
||||||
|
preciptext.text = param1?.precipitation.appendWith(" %")
|
||||||
|
cloudtext.text = param1?.cloud.appendWith(" %")
|
||||||
|
humiditytext.text = param1?.humidity.appendWith(" %")
|
||||||
|
uvtext.text = param1?.uvi
|
||||||
|
sunrisetext.text = param1?.sunrise
|
||||||
|
sunsettext.text = param1?.sunset
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String?.appendWith(suffix: String): String?{
|
||||||
|
return this?.let {
|
||||||
|
StringBuilder().append(it).append(suffix).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.home
|
||||||
|
|
||||||
|
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.Navigation.findNavController
|
||||||
|
import androidx.navigation.ui.onNavDestinationSelected
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.application.LOCATION_PERMISSION_REQUEST
|
||||||
|
import com.appttude.h_mal.model.forecast.Forecast
|
||||||
|
import com.appttude.h_mal.monoWeather.dialog.PermissionsDeclarationDialog
|
||||||
|
import com.appttude.h_mal.monoWeather.ui.BaseFragment
|
||||||
|
import com.appttude.h_mal.monoWeather.ui.home.adapter.WeatherRecyclerAdapter
|
||||||
|
import com.appttude.h_mal.utils.navigateTo
|
||||||
|
import com.appttude.h_mal.viewmodel.MainViewModel
|
||||||
|
import kotlinx.android.synthetic.main.fragment_home.*
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple [Fragment] subclass.
|
||||||
|
* create an instance of this fragment.
|
||||||
|
*/
|
||||||
|
class HomeFragment : BaseFragment(R.layout.fragment_home) {
|
||||||
|
|
||||||
|
private val viewModel by getFragmentViewModel<MainViewModel>()
|
||||||
|
|
||||||
|
lateinit var dialog: PermissionsDeclarationDialog
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
|
||||||
|
val recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
|
||||||
|
navigateToFurtherDetails(it)
|
||||||
|
})
|
||||||
|
|
||||||
|
forecast_listview.adapter = recyclerAdapter
|
||||||
|
dialog = PermissionsDeclarationDialog(requireContext())
|
||||||
|
|
||||||
|
swipe_refresh.apply {
|
||||||
|
setOnRefreshListener {
|
||||||
|
getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
|
||||||
|
viewModel.fetchData()
|
||||||
|
isRefreshing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.weatherLiveData.observe(viewLifecycleOwner) {
|
||||||
|
recyclerAdapter.addCurrent(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
|
||||||
|
viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
|
||||||
|
viewModel.operationRefresh.observe(viewLifecycleOwner, refreshObserver(swipe_refresh))
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
dialog.showDialog(agreeCallback = {
|
||||||
|
getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
|
||||||
|
viewModel.fetchData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
override fun permissionsGranted() {
|
||||||
|
viewModel.fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToFurtherDetails(forecast: Forecast){
|
||||||
|
val directions = HomeFragmentDirections
|
||||||
|
.actionHomeFragmentToFurtherDetailsFragment(forecast)
|
||||||
|
navigateTo(directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
// Inflate the menu; this adds items to the action bar if it is present.
|
||||||
|
inflater.inflate(R.menu.menu_main, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
val navController = findNavController(requireActivity(), R.id.container)
|
||||||
|
return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.home.adapter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.model.forecast.WeatherDisplay
|
||||||
|
import com.appttude.h_mal.utils.loadImage
|
||||||
|
|
||||||
|
class ViewHolderCurrent(listItemView: View) : RecyclerView.ViewHolder(listItemView) {
|
||||||
|
|
||||||
|
var locationTV: TextView = listItemView.findViewById(R.id.location_main_4)
|
||||||
|
var conditionTV: TextView = listItemView.findViewById(R.id.condition_main_4)
|
||||||
|
var weatherIV: ImageView = listItemView.findViewById(R.id.icon_main_4)
|
||||||
|
var avgTempTV: TextView = listItemView.findViewById(R.id.temp_main_4)
|
||||||
|
var tempUnit: TextView = listItemView.findViewById(R.id.temp_unit_4)
|
||||||
|
|
||||||
|
fun bindData(weather: WeatherDisplay?){
|
||||||
|
locationTV.text = weather?.displayName
|
||||||
|
conditionTV.text = weather?.description
|
||||||
|
weatherIV.loadImage(weather?.iconURL)
|
||||||
|
avgTempTV.text = weather?.averageTemp?.toInt().toString()
|
||||||
|
tempUnit.text = weather?.unit
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.home.adapter
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.model.forecast.Forecast
|
||||||
|
import com.appttude.h_mal.model.forecast.WeatherDisplay
|
||||||
|
import com.appttude.h_mal.monoWeather.ui.EmptyViewHolder
|
||||||
|
import com.appttude.h_mal.monoWeather.ui.home.adapter.forecast.ViewHolderForecast
|
||||||
|
import com.appttude.h_mal.monoWeather.ui.home.adapter.forecastDaily.ViewHolderForecastDaily
|
||||||
|
import com.appttude.h_mal.monoWeather.ui.home.adapter.further.ViewHolderFurtherDetails
|
||||||
|
import com.appttude.h_mal.utils.generateView
|
||||||
|
|
||||||
|
class WeatherRecyclerAdapter(
|
||||||
|
private val itemClick: (Forecast) -> Unit
|
||||||
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
|
var weather: WeatherDisplay? = null
|
||||||
|
|
||||||
|
fun addCurrent(current: WeatherDisplay){
|
||||||
|
weather = current
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
return when (getDataType(viewType)){
|
||||||
|
is ViewType.Empty -> {
|
||||||
|
val emptyViewHolder = parent.generateView(R.layout.empty_state_layout)
|
||||||
|
EmptyViewHolder(emptyViewHolder)
|
||||||
|
}
|
||||||
|
is ViewType.Current -> {
|
||||||
|
val viewCurrent = parent.generateView(R.layout.mono_item_one)
|
||||||
|
ViewHolderCurrent(viewCurrent)
|
||||||
|
}
|
||||||
|
is ViewType.ForecastHourly -> {
|
||||||
|
val viewForecast = parent.generateView(R.layout.mono_item_forecast)
|
||||||
|
ViewHolderForecast(viewForecast)
|
||||||
|
}
|
||||||
|
is ViewType.Further -> {
|
||||||
|
val viewFurther = parent.generateView(R.layout.mono_item_two)
|
||||||
|
ViewHolderFurtherDetails(viewFurther)
|
||||||
|
}
|
||||||
|
is ViewType.ForecastDaily -> {
|
||||||
|
val viewForecast = parent.generateView(R.layout.list_item_forecast)
|
||||||
|
ViewHolderForecastDaily(viewForecast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ViewType{
|
||||||
|
object Empty : ViewType()
|
||||||
|
object Current : ViewType()
|
||||||
|
object ForecastHourly : ViewType()
|
||||||
|
object ForecastDaily : ViewType()
|
||||||
|
object Further : ViewType()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDataType(type: Int): ViewType {
|
||||||
|
return when (type){
|
||||||
|
0 -> ViewType.Empty
|
||||||
|
1 -> ViewType.Current
|
||||||
|
2 -> ViewType.ForecastHourly
|
||||||
|
3 -> ViewType.Further
|
||||||
|
4 -> ViewType.ForecastDaily
|
||||||
|
else -> ViewType.Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
if (weather == null) return 0
|
||||||
|
return when(position){
|
||||||
|
0 -> 1
|
||||||
|
1 -> 3
|
||||||
|
2 -> 2
|
||||||
|
in 3 until (itemCount) -> 4
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
when (getDataType(getItemViewType(position))){
|
||||||
|
is ViewType.Empty -> {
|
||||||
|
holder as EmptyViewHolder
|
||||||
|
}
|
||||||
|
is ViewType.Current -> {
|
||||||
|
val viewHolderCurrent = holder as ViewHolderCurrent
|
||||||
|
viewHolderCurrent.bindData(weather)
|
||||||
|
}
|
||||||
|
is ViewType.Further -> {
|
||||||
|
val viewHolderCurrent = holder as ViewHolderFurtherDetails
|
||||||
|
viewHolderCurrent.bindData(weather)
|
||||||
|
}
|
||||||
|
is ViewType.ForecastHourly -> {
|
||||||
|
val viewHolderForecast = holder as ViewHolderForecast
|
||||||
|
viewHolderForecast.bindView(weather?.hourly)
|
||||||
|
}
|
||||||
|
is ViewType.ForecastDaily -> {
|
||||||
|
val viewHolderForecast = holder as ViewHolderForecastDaily
|
||||||
|
weather?.forecast?.getOrNull(position - 3)?.let { f ->
|
||||||
|
viewHolderForecast.bindView(f)
|
||||||
|
viewHolderForecast.itemView.setOnClickListener {
|
||||||
|
itemClick.invoke(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return if (weather == null) 0 else 3 + (weather?.forecast?.size ?: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.home.adapter.forecast
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.model.weather.Hour
|
||||||
|
import com.appttude.h_mal.utils.loadImage
|
||||||
|
import com.appttude.h_mal.utils.toTime
|
||||||
|
|
||||||
|
class GridCellHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var dayTV: TextView = itemView.findViewById(R.id.widget_item_day)
|
||||||
|
var weatherIV: ImageView = itemView.findViewById(R.id.widget_item_image)
|
||||||
|
var mainTempTV: TextView = itemView.findViewById(R.id.widget_item_temp_high)
|
||||||
|
|
||||||
|
fun bindView(hour: Hour?) {
|
||||||
|
dayTV.text = hour?.dt?.toTime()
|
||||||
|
weatherIV.loadImage(hour?.icon)
|
||||||
|
mainTempTV.text = hour?.temp?.toInt()?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.home.adapter.forecast
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.model.weather.Hour
|
||||||
|
import com.appttude.h_mal.utils.generateView
|
||||||
|
|
||||||
|
class GridForecastAdapter(): RecyclerView.Adapter<RecyclerView.ViewHolder>(){
|
||||||
|
var weather: MutableList<Hour> = mutableListOf()
|
||||||
|
|
||||||
|
fun addCurrent(current: List<Hour>?){
|
||||||
|
weather.clear()
|
||||||
|
current?.let { weather.addAll(it) }
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
val viewCurrent = parent.generateView(R.layout.mono_forecast_grid_item)
|
||||||
|
return GridCellHolder(viewCurrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
|
val view = holder as GridCellHolder
|
||||||
|
val forecast = weather[position]
|
||||||
|
view.bindView(forecast)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = weather.size
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.home.adapter.forecast
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.model.weather.Hour
|
||||||
|
|
||||||
|
class ViewHolderForecast(
|
||||||
|
itemView: View
|
||||||
|
) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var recyclerView: RecyclerView = itemView.findViewById(R.id.forecast_recyclerview)
|
||||||
|
|
||||||
|
fun bindView(forecasts: List<Hour>?) {
|
||||||
|
val adapter = GridForecastAdapter()
|
||||||
|
adapter.addCurrent(forecasts)
|
||||||
|
recyclerView.adapter = adapter
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.home.adapter.forecastDaily
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.model.forecast.Forecast
|
||||||
|
import com.appttude.h_mal.utils.loadImage
|
||||||
|
|
||||||
|
class ViewHolderForecastDaily(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var dateTV: TextView = itemView.findViewById(R.id.list_date)
|
||||||
|
var dayTV: TextView = itemView.findViewById(R.id.db_condition)
|
||||||
|
var weatherIV: ImageView = itemView.findViewById(R.id.db_icon)
|
||||||
|
var mainTempTV: TextView = itemView.findViewById(R.id.db_main_temp)
|
||||||
|
var tempUnits: TextView = itemView.findViewById(R.id.db_temp_unit)
|
||||||
|
|
||||||
|
fun bindView(forecast: Forecast?) {
|
||||||
|
dateTV.text = forecast?.date
|
||||||
|
dayTV.text = forecast?.day
|
||||||
|
weatherIV.loadImage(forecast?.weatherIcon)
|
||||||
|
mainTempTV.text = forecast?.mainTemp
|
||||||
|
tempUnits.text = itemView.context.getString(R.string.degrees)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.home.adapter.further
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.utils.generateView
|
||||||
|
import kotlinx.android.synthetic.monoWeather.mono_item_two_cell.view.*
|
||||||
|
|
||||||
|
|
||||||
|
class GridAdapter(
|
||||||
|
context: Context,
|
||||||
|
list: List<Pair<Int, String>>
|
||||||
|
) : ArrayAdapter<Pair<Int, String>>(context, 0, list){
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val view = convertView ?: parent.generateView(R.layout.mono_item_two_cell)
|
||||||
|
|
||||||
|
val item = getItem(position)
|
||||||
|
|
||||||
|
return view.apply {
|
||||||
|
mono_item_cell.setImageResource(item!!.first)
|
||||||
|
mono_text_cell.text = item.second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.home.adapter.further
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.GridView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.model.forecast.WeatherDisplay
|
||||||
|
|
||||||
|
class ViewHolderFurtherDetails(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
var grid: GridView = itemView.findViewById(R.id.grid_mono)
|
||||||
|
|
||||||
|
fun bindData(weather: WeatherDisplay?){
|
||||||
|
grid.adapter = GridAdapter(itemView.context, listOf<Pair<Int, String>>(
|
||||||
|
Pair(R.drawable.breeze,"${weather?.windSpeed ?: "0"} km"),
|
||||||
|
Pair(R.drawable.water_drop,"${weather?.precipitation ?: "0"} %" ),
|
||||||
|
Pair(R.drawable.cloud_symbol,"${weather?.clouds ?: "0"} %" )
|
||||||
|
))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.settings
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.preference.PreferenceActivity
|
||||||
|
import android.preference.PreferenceFragment
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.widget.NewAppWidget
|
||||||
|
|
||||||
|
|
||||||
|
class UnitSettingsActivity : PreferenceActivity() {
|
||||||
|
private var prefListener: OnSharedPreferenceChangeListener? = null
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
PreferenceManager.setDefaultValues(this, R.xml.prefs_screen, false)
|
||||||
|
fragmentManager.beginTransaction().replace(android.R.id.content, MyPreferenceFragment()).commit()
|
||||||
|
|
||||||
|
//listener on changed sort order preference:
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
prefListener = OnSharedPreferenceChangeListener { _, key ->
|
||||||
|
if (key == "temp_units") {
|
||||||
|
val intent = Intent(baseContext, NewAppWidget::class.java)
|
||||||
|
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||||
|
val ids = AppWidgetManager.getInstance(application).getAppWidgetIds(ComponentName(application, NewAppWidget::class.java))
|
||||||
|
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == "widget_black_background"){
|
||||||
|
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
|
||||||
|
val widgetManager = AppWidgetManager.getInstance(this)
|
||||||
|
val ids = widgetManager.getAppWidgetIds(ComponentName(this, NewAppWidget::class.java))
|
||||||
|
AppWidgetManager.getInstance(this).notifyAppWidgetViewDataChanged(ids, R.id.whole_widget_view)
|
||||||
|
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||||
|
sendBroadcast(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefs.registerOnSharedPreferenceChangeListener(prefListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MyPreferenceFragment : PreferenceFragment() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
addPreferencesFromResource(R.xml.prefs_screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.widget
|
||||||
|
|
||||||
|
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||||
|
import android.app.Activity
|
||||||
|
import android.appwidget.AppWidgetManager.*
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat.checkSelfPermission
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.monoWeather.dialog.DeclarationBuilder
|
||||||
|
import com.appttude.h_mal.utils.displayToast
|
||||||
|
import kotlinx.android.synthetic.monoWeather.permissions_declaration_dialog.*
|
||||||
|
|
||||||
|
const val PERMISSION_CODE = 401
|
||||||
|
|
||||||
|
class WidgetLocationPermissionActivity : AppCompatActivity(), DeclarationBuilder {
|
||||||
|
override val link: String = "https://sites.google.com/view/hmaldev/home/monochrome"
|
||||||
|
override var message: String = ""
|
||||||
|
|
||||||
|
private var mAppWidgetId = INVALID_APPWIDGET_ID
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
message = readFromResources(R.string.widget_declaration)
|
||||||
|
|
||||||
|
// Set the result to CANCELED. This will cause the widget host to cancel
|
||||||
|
// out of the widget placement if the user presses the back button.
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
|
||||||
|
// Find the widget id from the intent.
|
||||||
|
intent.extras?.let {
|
||||||
|
mAppWidgetId = it.getInt(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this activity was started with an intent without an app widget ID, finish with an error.
|
||||||
|
if (mAppWidgetId == INVALID_APPWIDGET_ID) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentView(R.layout.permissions_declaration_dialog)
|
||||||
|
findViewById<TextView>(R.id.declaration_text).apply {
|
||||||
|
text = buildMessage()
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
submit.setOnClickListener {
|
||||||
|
if (checkSelfPermission(this, ACCESS_COARSE_LOCATION) != PERMISSION_GRANTED) {
|
||||||
|
requestPermissions(arrayOf(ACCESS_COARSE_LOCATION), PERMISSION_CODE)
|
||||||
|
} else {
|
||||||
|
submitWidget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel.setOnClickListener { finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == PERMISSION_CODE) {
|
||||||
|
if (grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
|
||||||
|
submitWidget()
|
||||||
|
} else {
|
||||||
|
displayToast("Location Permission denied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submitWidget() {
|
||||||
|
sendUpdateIntent()
|
||||||
|
finishCurrencyWidgetActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishCurrencyWidgetActivity() {
|
||||||
|
// Make sure we pass back the original appWidgetId
|
||||||
|
val resultValue = intent
|
||||||
|
resultValue.putExtra(EXTRA_APPWIDGET_ID, mAppWidgetId)
|
||||||
|
setResult(Activity.RESULT_OK, resultValue)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendUpdateIntent() {
|
||||||
|
// It is the responsibility of the configuration activity to update the app widget
|
||||||
|
// Send update broadcast to widget app class
|
||||||
|
Intent(this@WidgetLocationPermissionActivity,
|
||||||
|
WidgetLocationPermissionActivity::class.java
|
||||||
|
).apply {
|
||||||
|
action = ACTION_APPWIDGET_UPDATE
|
||||||
|
|
||||||
|
// Put current app widget ID into extras and send broadcast
|
||||||
|
putExtra(EXTRA_APPWIDGET_IDS, intArrayOf(mAppWidgetId))
|
||||||
|
sendBroadcast(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.appttude.h_mal.monoWeather.ui.world
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.lifecycle.observe
|
||||||
|
import com.appttude.h_mal.R
|
||||||
|
import com.appttude.h_mal.monoWeather.ui.BaseFragment
|
||||||
|
import com.appttude.h_mal.utils.displayToast
|
||||||
|
import com.appttude.h_mal.utils.goBack
|
||||||
|
import com.appttude.h_mal.utils.hideKeyboard
|
||||||
|
import com.appttude.h_mal.viewmodel.WorldViewModel
|
||||||
|
import kotlinx.android.synthetic.main.activity_add_forecast.*
|
||||||
|
|
||||||
|
|
||||||
|
class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
|
||||||
|
|
||||||
|
private val viewModel by getFragmentViewModel<WorldViewModel>()
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
submit.setOnClickListener {
|
||||||
|
val locationName = location_name_tv.text?.trim()?.toString()
|
||||||
|
if (locationName.isNullOrBlank()){
|
||||||
|
location_name_tv.error = "Location cannot be blank"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
viewModel.fetchDataForSingleLocationSearch(locationName)
|
||||||
|
hideKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.operationState.observe(viewLifecycleOwner, progressBarStateObserver(progressBar))
|
||||||
|
viewModel.operationError.observe(viewLifecycleOwner, errorObserver())
|
||||||
|
|
||||||
|
viewModel.operationComplete.observe(viewLifecycleOwner) {
|
||||||
|
it?.getContentIfNotHandled()?.let { message ->
|
||||||
|
displayToast(message)
|
||||||
|
}
|
||||||
|
goBack()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user