- mid commit (broken)

This commit is contained in:
2023-08-04 20:26:17 +01:00
parent 4a37b724a6
commit e175558ce6
129 changed files with 205 additions and 4822 deletions

View File

@@ -14,18 +14,11 @@ commands:
description: checkout repo and android dependencies
steps:
- checkout
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Chmod permissions
command: sudo chmod +x ./gradlew
- android/restore-gradle-cache
- run:
name: Download Dependencies
command: ./gradlew androidDependencies
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- android/save-gradle-cache
run_tests:
description: run tests for flavour specified
parameters:

21
.idea/navEditor.xml generated
View File

@@ -79,6 +79,18 @@
</LayoutPositions>
</value>
</entry>
<entry key="settings_fragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="12" />
<option name="y" value="12" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="worldItemFragment">
<value>
<LayoutPositions>
@@ -88,6 +100,15 @@
<option name="y" value="408" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_worldItemFragment_to_furtherDetailsFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>

View File

@@ -1,9 +1,9 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-android-extensions'
id 'kotlin-kapt'
id 'androidx.navigation.safeargs'
id 'kotlin-parcelize'
}
android {
lintOptions {
@@ -11,12 +11,12 @@ android {
}
compileSdkVersion 33
defaultConfig {
applicationId "com.appttude.h_mal.atlas_weather"
applicationId "com.appttude.h_mal"
minSdkVersion 26
targetSdkVersion 33
versionCode 5
versionName "3.0"
testInstrumentationRunner "com.appttude.h_mal.atlas_weather.application.TestRunner"
testInstrumentationRunner "com.appttude.h_mal.application.TestRunner"
vectorDrawables.useSupportLibrary = true
Properties properties = new Properties()
@@ -61,6 +61,9 @@ android {
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += [
'-Xjvm-default=enable'
]
}
flavorDimensions "default"
@@ -71,7 +74,7 @@ android {
versionName "3.0.0"
}
monoWeather {
applicationId "com.appttude.h_mal.atlas_weather.monoWeather"
applicationId "com.appttude.h_mal.monoWeather"
versionCode 5
versionName "3.0.0"
}
@@ -106,6 +109,7 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
implementation 'androidx.preference:preference:1.2.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
// Unit testing
@@ -181,9 +185,9 @@ dependencies {
def coroutines_google_ver = "1.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutines_google_ver"
/ * Glide */
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
/ * Picasso */
implementation 'com.squareup.picasso:picasso:2.71828'
/ * screenshot library */
androidTestImplementation 'tools.fastlane:screengrab:2.1.1'
}

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
package com.appttude.h_mal.atlas_weather.testsuite
import android.Manifest
import android.app.Activity

View File

@@ -1,8 +1,7 @@
package com.appttude.h_mal.atlas_weather.monoWeather.testsuite
package com.appttude.h_mal.atlas_weather.testsuite
import android.Manifest
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.espresso.Espresso
@@ -10,13 +9,11 @@ import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.application.TestAppClass
import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import kotlinx.coroutines.runBlocking
import org.junit.After

View File

@@ -1,4 +1,4 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui.widget
package com.appttude.h_mal.atlas_weather.ui.widget
import android.appwidget.AppWidgetManager
import android.content.Intent

View File

@@ -4,7 +4,7 @@ package com.appttude.h_mal.atlas_weather.tests
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.monoWeather.testsuite.BaseTest
import com.appttude.h_mal.atlas_weather.testsuite.BaseTest
import com.appttude.h_mal.atlas_weather.utils.Stubs
import org.junit.Rule
import org.junit.Test

View File

@@ -1,12 +1,10 @@
package com.appttude.h_mal.atlas_weather.tests
import androidx.test.rule.GrantPermissionRule
import com.appttude.h_mal.atlas_weather.robot.homeScreen
import com.appttude.h_mal.atlas_weather.monoWeather.testsuite.BaseTest
import com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.testsuite.BaseTest
import com.appttude.h_mal.atlas_weather.ui.MainActivity
import com.appttude.h_mal.atlas_weather.utils.Stubs
import org.junit.Rule
import org.junit.Test
class HomePageUITest : BaseTest<MainActivity>() {

View File

@@ -5,7 +5,7 @@
tools:node="merge">
<application
android:name="com.appttude.h_mal.atlas_weather.application.AppClass"
android:name="com.appttude.h_mal.application.AppClass"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -14,7 +14,7 @@
android:theme="@style/AppTheme"
tools:node="merge">
<activity android:name=".atlasWeather.ui.MainActivity"
<activity android:name=".ui.MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar"

View File

@@ -1,8 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.notification
import android.graphics.Bitmap
data class NotificationData(
val temp: String,
val icon: Bitmap
)

View File

@@ -1,76 +0,0 @@
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

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

View File

@@ -1,55 +0,0 @@
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

@@ -1,65 +0,0 @@
package com.appttude.h_mal.atlas_weather.atlasWeather.ui
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.atlasWeather.ui.settings.UnitSettingsActivity
import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.android.synthetic.atlasWeather.activity_main.*
class MainActivity : BaseActivity(){
lateinit var navHost: NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
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 tabs = setOf(R.id.nav_home, R.id.nav_world)
val appBarConfiguration = AppBarConfiguration(tabs)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
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) {
R.id.action_settings -> {
val i = Intent(this, UnitSettingsActivity::class.java)
startActivity(i)
return true
}
}
return super.onOptionsItemSelected(item)
}
}

View File

@@ -1,48 +0,0 @@
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

@@ -1,44 +0,0 @@
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

@@ -1,99 +0,0 @@
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

@@ -1,6 +0,0 @@
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

@@ -1,26 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -1,104 +0,0 @@
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

@@ -1,88 +0,0 @@
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

@@ -1,56 +0,0 @@
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

@@ -1,73 +0,0 @@
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

@@ -1,96 +0,0 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,77 +0,0 @@
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

@@ -1,141 +0,0 @@
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

@@ -1,10 +0,0 @@
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

@@ -1,15 +0,0 @@
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

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.appttude.h_mal.atlas_weather.monoWeather.ui.widget.WidgetLocationPermissionActivity"
android:configure="com.appttude.h_mal.ui.widget.WidgetLocationPermissionActivity"
android:initialKeyguardLayout="@layout/weather_app_widget"
android:initialLayout="@layout/weather_app_widget"
android:minHeight="110.0dp"

View File

@@ -1,11 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.appttude.h_mal.atlas_weather">
xmlns:tools="http://schemas.android.com/tools"
package="com.appttude.h_mal">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SET_ALARM" />
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
<application android:networkSecurityConfig="@xml/network_security_config" />
<uses-feature
android:name="android.hardware.location"

View File

@@ -1,42 +0,0 @@
package com.appttude.h_mal.atlas_weather.application
import androidx.room.RoomDatabase
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.location.LocationProviderImpl
import com.appttude.h_mal.atlas_weather.data.network.Api
import com.appttude.h_mal.atlas_weather.data.network.NetworkModule
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.QueryParamsInterceptor
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.loggingInterceptor
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.google.gson.Gson
import org.kodein.di.Kodein
import org.kodein.di.android.x.androidXModule
import org.kodein.di.generic.bind
import org.kodein.di.generic.instance
import org.kodein.di.generic.provider
import org.kodein.di.generic.singleton
const val LOCATION_PERMISSION_REQUEST = 505
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

@@ -1,43 +0,0 @@
package com.appttude.h_mal.atlas_weather.application
import android.app.Application
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepositoryImpl
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
import com.appttude.h_mal.atlas_weather.viewmodel.ApplicationViewModelFactory
import com.google.gson.Gson
import org.kodein.di.Kodein
import org.kodein.di.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

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

View File

@@ -1,13 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.location
import com.appttude.h_mal.atlas_weather.model.types.LocationType
interface LocationProvider {
suspend fun getCurrentLatLong(): Pair<Double, Double>
fun getLatLongFromLocationName(location: String): Pair<Double, Double>
suspend fun getLocationNameFromLatLong(
lat: Double,
long: Double,
type: LocationType = LocationType.Town
): String
}

View File

@@ -1,114 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.location
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.annotation.SuppressLint
import android.content.Context
import android.location.Geocoder
import android.location.Location
import android.location.LocationManager
import android.os.HandlerThread
import androidx.annotation.RequiresPermission
import com.appttude.h_mal.atlas_weather.model.types.LocationType
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationRequest.PRIORITY_HIGH_ACCURACY
import com.google.android.gms.location.LocationRequest.PRIORITY_LOW_POWER
import com.google.android.gms.location.LocationResult
import com.google.android.gms.tasks.CancellationToken
import com.google.android.gms.tasks.OnTokenCanceledListener
import kotlinx.coroutines.tasks.await
import java.io.IOException
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LocationProviderImpl(
val applicationContext: Context
) : LocationProvider, LocationHelper(applicationContext) {
private var locationManager =
applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
private val client = FusedLocationProviderClient(applicationContext)
private val geoCoder: Geocoder by lazy { Geocoder(applicationContext, Locale.getDefault()) }
@RequiresPermission(value = ACCESS_COARSE_LOCATION)
override suspend fun getCurrentLatLong(): Pair<Double, Double> {
val location = client.lastLocation.await() ?: getAFreshLocation()
return location?.getLatLonPair() ?: throw IOException("Unable to get location")
}
override fun getLatLongFromLocationName(location: String): Pair<Double, Double> {
val locations = geoCoder.getFromLocationName(location, 1)
locations?.takeIf { it.isNotEmpty() }?.get(0)?.let {
return Pair(it.latitude, it.longitude)
}
throw IOException("No location found")
}
override suspend fun getLocationNameFromLatLong(
lat: Double, long: Double, type: LocationType
): String {
val address = getAddressFromLatLong(lat, long) ?: return "$lat $long"
return when (type) {
LocationType.Town -> {
val location = address
.municipalitySubdivision
?.takeIf { it.isNotBlank() }
?: address.municipality
location ?: throw IOException("No location municipalitySubdivision or municipality")
}
LocationType.City -> {
address.municipality ?: throw IOException("No location municipality")
}
}
}
@SuppressLint("MissingPermission")
private suspend fun getAFreshLocation(): Location? {
return client.getCurrentLocation(PRIORITY_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

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

View File

@@ -1,23 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network
import com.appttude.h_mal.atlas_weather.data.network.networkUtils.buildOkHttpClient
import com.appttude.h_mal.atlas_weather.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

@@ -1,6 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network
class NetworkModule : BaseNetworkModule() {
override fun baseUrl(): String = "https://api.openweathermap.org/data/2.5/"
}

View File

@@ -1,33 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,20 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network
import com.appttude.h_mal.atlas_weather.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

@@ -1,21 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.interceptors
import android.content.Context
import com.appttude.h_mal.atlas_weather.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

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

View File

@@ -1,28 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.interceptors
import com.appttude.h_mal.atlas_weather.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

@@ -1,49 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.network.networkUtils
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.atlas_weather.data.network.interceptors.NetworkInterceptor
import okhttp3.Interceptor
import okhttp3.OkHttpClient
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

@@ -1,48 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,51 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,18 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,48 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,24 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,24 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,18 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,28 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,50 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.prefs
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.appttude.h_mal.atlas_weather.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

@@ -1,21 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.repository
import androidx.lifecycle.LiveData
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.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

@@ -1,76 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.repository
import com.appttude.h_mal.atlas_weather.data.network.ResponseUnwrap
import com.appttude.h_mal.atlas_weather.data.network.WeatherApi
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.prefs.LOCATION_CONST
import com.appttude.h_mal.atlas_weather.data.prefs.PreferenceProvider
import com.appttude.h_mal.atlas_weather.data.room.AppDatabase
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.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

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

View File

@@ -1,14 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.repository
import com.appttude.h_mal.atlas_weather.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

@@ -1,42 +0,0 @@
package com.appttude.h_mal.atlas_weather.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.atlas_weather.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

@@ -1,27 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.room
import android.content.Context
import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import com.appttude.h_mal.atlas_weather.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

@@ -1,35 +0,0 @@
package com.appttude.h_mal.atlas_weather.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.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.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

@@ -1,14 +0,0 @@
package com.appttude.h_mal.atlas_weather.data.room.entity
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
const val CURRENT_LOCATION = "CurrentLocation"
@Entity
data class EntityItem(
@PrimaryKey(autoGenerate = false)
val id: String,
val weather: FullWeather
)

View File

@@ -1,51 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

@@ -1,151 +0,0 @@
package com.appttude.h_mal.atlas_weather.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.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.repository.SettingsRepository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.FullWeather
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetCellData
import com.appttude.h_mal.atlas_weather.model.widget.InnerWidgetData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetData
import com.appttude.h_mal.atlas_weather.model.widget.WidgetWeatherCollection
import com.appttude.h_mal.atlas_weather.utils.toSmallDayName
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import 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

@@ -1,84 +0,0 @@
package com.appttude.h_mal.atlas_weather.model.forecast
import android.os.Parcel
import android.os.Parcelable
import com.appttude.h_mal.atlas_weather.model.weather.DailyWeather
import com.appttude.h_mal.atlas_weather.utils.parcelableCreator
import com.appttude.h_mal.atlas_weather.utils.toDayName
import com.appttude.h_mal.atlas_weather.utils.toDayString
import com.appttude.h_mal.atlas_weather.utils.toTime
data class Forecast(
val date: String?,
val day: String?,
val condition: String?,
val weatherIcon: String?,
val mainTemp: String?,
val minorTemp: String?,
val averageTemp: String?,
val windText: String?,
val precipitation: String?,
val humidity: String?,
val uvi: String?,
val sunrise: String?,
val sunset: String?,
val cloud: String?
): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString())
constructor(dailyWeather: DailyWeather) : this(
dailyWeather.dt?.toDayString(),
dailyWeather.dt?.toDayName(),
dailyWeather.description,
dailyWeather.icon,
dailyWeather.max?.toInt().toString(),
dailyWeather.min?.toInt().toString(),
dailyWeather.average?.toInt().toString(),
dailyWeather.windSpeed?.toInt().toString(),
(dailyWeather.pop?.times(100))?.toInt().toString(),
dailyWeather.humidity?.toString(),
dailyWeather.uvi?.toInt().toString(),
dailyWeather.sunrise?.toTime(),
dailyWeather.sunset?.toTime(),
dailyWeather.clouds?.toString()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(date)
parcel.writeString(day)
parcel.writeString(condition)
parcel.writeString(weatherIcon)
parcel.writeString(mainTemp)
parcel.writeString(minorTemp)
parcel.writeString(averageTemp)
parcel.writeString(windText)
parcel.writeString(precipitation)
parcel.writeString(humidity)
parcel.writeString(uvi)
parcel.writeString(sunrise)
parcel.writeString(sunset)
}
override fun describeContents(): Int {
return 0
}
companion object{
@JvmField val CREATOR = parcelableCreator(::Forecast)
}
}

View File

@@ -1,43 +0,0 @@
package com.appttude.h_mal.atlas_weather.model.forecast
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.weather.Hour
data class WeatherDisplay(
val averageTemp: Double?,
var unit: String?,
var location: String?,
val iconURL: String?,
val description: String?,
val hourly: List<Hour>?,
val forecast: List<Forecast>?,
val windSpeed: String?,
val windDirection: String?,
val precipitation: String?,
val humidity: String?,
val clouds: String?,
val lat: Double = 0.00,
val lon: Double = 0.00,
var displayName: String?
){
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

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

View File

@@ -1,44 +0,0 @@
package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Current
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,
dailyItem.weather?.get(0)?.icon?.let { "https://openweathermap.org/img/wn/${it}@4x.png" },
dailyItem.weather?.get(0)?.description,
dailyItem.weather?.get(0)?.main,
dailyItem.weather?.get(0)?.id,
dailyItem.humidity,
dailyItem.windSpeed
)
}

View File

@@ -1,53 +0,0 @@
package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.DailyItem
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,
dailyItem.weather?.get(0)?.icon?.let { "https://openweathermap.org/img/wn/${it}@4x.png" },
dailyItem.weather?.get(0)?.description,
dailyItem.weather?.get(0)?.main,
dailyItem.weather?.get(0)?.id,
dailyItem.clouds,
dailyItem.pop,
dailyItem.uvi,
dailyItem.rain
)
}

View File

@@ -1,29 +0,0 @@
package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.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

@@ -1,19 +0,0 @@
package com.appttude.h_mal.atlas_weather.model.weather
import com.appttude.h_mal.atlas_weather.utils.generateIconUrlString
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.Hour as ForecastHour
data class Hour(
val dt: Int? = null,
val temp: Double? = null,
val icon: String? = null
){
constructor(hour: ForecastHour) : this(
hour.dt,
hour.temp,
generateIconUrlString(hour.weather?.getOrNull(0)?.icon)
)
}

View File

@@ -1,28 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

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

View File

@@ -1,24 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

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

View File

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

View File

@@ -1,32 +0,0 @@
package com.appttude.h_mal.atlas_weather.utils
import android.view.View
import androidx.fragment.app.Fragment
import androidx.navigation.NavDirections
import androidx.navigation.Navigation
import com.appttude.h_mal.atlas_weather.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

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

View File

@@ -1,61 +0,0 @@
package com.appttude.h_mal.atlas_weather.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

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

View File

@@ -1,51 +0,0 @@
package com.appttude.h_mal.atlas_weather.utils
import android.app.Activity
import android.content.Context
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.atlas_weather.R
import com.bumptech.glide.Glide
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?){
val c = Glide.with(this)
.load(url)
viewTreeObserver.addOnPreDrawListener {
c.override(width, height)
true
}
c.placeholder(R.drawable.ic_baseline_cloud_queue_24)
.error(R.drawable.ic_baseline_cloud_off_24)
.fitCenter()
.into(this)
}
fun Fragment.hideKeyboard() {
val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.hideSoftInputFromWindow(view?.windowToken, 0)
}

View File

@@ -1,24 +0,0 @@
package com.appttude.h_mal.atlas_weather.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.RepositoryImpl
class 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

@@ -1,66 +0,0 @@
package com.appttude.h_mal.atlas_weather.viewmodel
import android.Manifest
import androidx.annotation.RequiresPermission
import androidx.lifecycle.MutableLiveData
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.room.entity.CURRENT_LOCATION
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.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

@@ -1,165 +0,0 @@
package com.appttude.h_mal.atlas_weather.viewmodel
import androidx.lifecycle.MutableLiveData
import com.appttude.h_mal.atlas_weather.data.location.LocationProvider
import com.appttude.h_mal.atlas_weather.data.network.response.forecast.WeatherResponse
import com.appttude.h_mal.atlas_weather.data.repository.Repository
import com.appttude.h_mal.atlas_weather.data.room.entity.EntityItem
import com.appttude.h_mal.atlas_weather.model.forecast.WeatherDisplay
import com.appttude.h_mal.atlas_weather.model.types.LocationType
import com.appttude.h_mal.atlas_weather.utils.Event
import com.appttude.h_mal.atlas_weather.viewmodel.baseViewModels.WeatherViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.IOException
const val ALL_LOADED = "all_loaded"
class WorldViewModel(
private val locationProvider: LocationProvider,
private val repository: Repository
) : 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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,4 @@
android:layout_height="wrap_content"/>
</FrameLayout>
</RelativeLayout>

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
android:name="com.appttude.h_mal.atlas_weather.application.AppClass"
android:name="com.appttude.h_mal.application.AppClass"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -15,7 +15,7 @@
android:theme="@style/AppTheme"
tools:node="merge">
<activity
android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.MainActivity"
android:name="com.appttude.h_mal.ui.MainActivity"
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar"
@@ -27,18 +27,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.settings.UnitSettingsActivity"
android:label="Settings" />
<activity android:name="com.appttude.h_mal.atlas_weather.monoWeather.ui.widget.WidgetLocationPermissionActivity"
<activity android:name="com.appttude.h_mal.monoWeather.ui.widget.WidgetLocationPermissionActivity"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver android:name="com.appttude.h_mal.atlas_weather.monoWeather.widget.NewAppWidget"
<receiver android:name="com.appttude.h_mal.widget.NewAppWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -52,7 +49,7 @@
</receiver>
<service
android:name="com.appttude.h_mal.atlas_weather.monoWeather.widget.WidgetJobServiceIntent"
android:name="com.appttude.h_mal.widget.WidgetJobServiceIntent"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>

View File

@@ -1,23 +0,0 @@
package com.appttude.h_mal.atlas_weather.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)
@RequiresApi(Build.VERSION_CODES.N)
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

@@ -1,48 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.dialog
import android.content.Context
import android.os.Build
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import com.appttude.h_mal.atlas_weather.R
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

@@ -1,142 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.appttude.h_mal.atlas_weather.application.LOCATION_PERMISSION_REQUEST
import com.appttude.h_mal.atlas_weather.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 com.appttude.h_mal.atlas_weather.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>> {
when (it.getContentIfNotHandled()) {
true -> progressBar.fadeIn()
false -> 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

@@ -1,21 +0,0 @@
package com.appttude.h_mal.atlas_weather.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.atlas_weather.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

@@ -1,65 +0,0 @@
package com.appttude.h_mal.atlas_weather.monoWeather.ui
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.appttude.h_mal.atlas_weather.R
import com.appttude.h_mal.atlas_weather.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 tabs = setOf(R.id.nav_home, R.id.nav_world)
val appBarConfiguration = AppBarConfiguration(tabs)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
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) {
R.id.action_settings -> {
val i = Intent(this, UnitSettingsActivity::class.java)
startActivity(i)
return true
}
android.R.id.home -> onBackPressed()
}
return super.onOptionsItemSelected(item)
}
}

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