- mid commit (broken)

This commit is contained in:
2023-08-06 17:01:29 +01:00
parent e175558ce6
commit 89be7be19f
102 changed files with 4636 additions and 0 deletions

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui
import androidx.appcompat.app.AppCompatActivity
abstract class BaseActivity : AppCompatActivity(){
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
package com.appttude.h_mal.data.network
interface Api

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.data.network.interceptors
import okhttp3.Interceptor
interface NetworkInterceptor : Interceptor

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
package com.appttude.h_mal.data.repository
interface SettingsRepository {
fun isNotificationsEnabled(): Boolean
fun setFirstTime()
fun isBlackBackground(): Boolean
}

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

@@ -0,0 +1,3 @@
package com.appttude.h_mal.utils
val FALLBACK_TIME: Long = 300000L

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

@@ -0,0 +1,154 @@
package com.appttude.h_mal.monoWeather.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.appttude.h_mal.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() {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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