mirror of
https://github.com/hmalik144/Weather-apps.git
synced 2025-12-10 02:05:20 +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