mirror of
https://github.com/hmalik144/Weather-apps.git
synced 2026-03-18 15:36:04 +00:00
Playstore permissions declaration (#12)
* Declaration builder created Took 2 hours 34 minutes Took 2 minutes * - Fixed android S issues - Dialog box completed - Widget creation activity UI test added Took 1 hour 53 minutes * - Popup for google playstore permissions added - UI tests with stubbing added - linting clean ups - changes to fragments and base fragment Took 3 hours 57 minutes
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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
|
||||
|
||||
fun showDialog(agreeCallback: () -> Unit = { }, disagreeCallback: () -> Unit = { Unit }) {
|
||||
val myMessage = buildMessage()
|
||||
|
||||
val builder = AlertDialog.Builder(context)
|
||||
.setPositiveButton("agree") { _, _ ->
|
||||
agreeCallback()
|
||||
}
|
||||
.setNegativeButton("disagree") { _, _ ->
|
||||
disagreeCallback()
|
||||
}
|
||||
.setMessage(myMessage)
|
||||
.setCancelable(false)
|
||||
|
||||
val alertDialog = builder.create()
|
||||
alertDialog.show()
|
||||
|
||||
// Make the textview clickable. Must be called after show()
|
||||
val msgTxt = alertDialog.findViewById<View>(android.R.id.message) as TextView?
|
||||
msgTxt?.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,18 @@ 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
|
||||
@@ -24,7 +27,7 @@ import org.kodein.di.android.x.kodein
|
||||
import org.kodein.di.generic.instance
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
abstract class BaseFragment() : Fragment(), KodeinAware {
|
||||
abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId), KodeinAware {
|
||||
|
||||
override val kodein by kodein()
|
||||
val factory by instance<ApplicationViewModelFactory>()
|
||||
@@ -119,4 +122,21 @@ abstract class BaseFragment() : Fragment(), KodeinAware {
|
||||
}
|
||||
}
|
||||
|
||||
@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() {}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
|
||||
|
||||
class WorldItemFragment : BaseFragment() {
|
||||
class WorldItemFragment : BaseFragment(R.layout.fragment_home) {
|
||||
|
||||
private val viewModel by getFragmentViewModel<WorldViewModel>()
|
||||
private var param1: String? = null
|
||||
@@ -24,12 +24,6 @@ class WorldItemFragment : BaseFragment() {
|
||||
param1 = WorldItemFragmentArgs.fromBundle(requireArguments()).locationName
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.appttude.h_mal.atlas_weather.monoWeather.ui.home
|
||||
|
||||
import android.Manifest
|
||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
@@ -9,9 +10,10 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
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.model.forecast.Forecast
|
||||
import com.appttude.h_mal.atlas_weather.monoWeather.dialog.PermissionsDeclarationDialog
|
||||
import com.appttude.h_mal.atlas_weather.monoWeather.ui.BaseFragment
|
||||
import com.appttude.h_mal.atlas_weather.monoWeather.ui.home.adapter.WeatherRecyclerAdapter
|
||||
import com.appttude.h_mal.atlas_weather.utils.displayToast
|
||||
@@ -24,39 +26,29 @@ import kotlinx.android.synthetic.main.fragment_home.*
|
||||
* A simple [Fragment] subclass.
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
class HomeFragment : BaseFragment() {
|
||||
class HomeFragment : BaseFragment(R.layout.fragment_home) {
|
||||
|
||||
private val viewModel by getFragmentViewModel<MainViewModel>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
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)
|
||||
}
|
||||
val recyclerAdapter = WeatherRecyclerAdapter(itemClick = {
|
||||
navigateToFurtherDetails(it)
|
||||
})
|
||||
|
||||
forecast_listview.apply {
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
adapter = recyclerAdapter
|
||||
}
|
||||
forecast_listview.adapter = recyclerAdapter
|
||||
|
||||
|
||||
getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){
|
||||
viewModel.fetchData()
|
||||
}
|
||||
PermissionsDeclarationDialog(requireContext()).showDialog(agreeCallback = {
|
||||
getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
})
|
||||
|
||||
swipe_refresh.apply {
|
||||
setOnRefreshListener {
|
||||
getPermissionResult(Manifest.permission.ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST){
|
||||
getPermissionResult(ACCESS_COARSE_LOCATION, LOCATION_PERMISSION_REQUEST) {
|
||||
viewModel.fetchData()
|
||||
isRefreshing = true
|
||||
}
|
||||
@@ -73,16 +65,13 @@ class HomeFragment : BaseFragment() {
|
||||
}
|
||||
|
||||
@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")
|
||||
}
|
||||
}
|
||||
override fun permissionsGranted() {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
|
||||
private fun navigateToFurtherDetails(forecast: Forecast){
|
||||
val directions = HomeFragmentDirections
|
||||
.actionHomeFragmentToFurtherDetailsFragment(forecast)
|
||||
navigateTo(directions)
|
||||
}
|
||||
}
|
||||
@@ -6,20 +6,28 @@ import android.appwidget.AppWidgetManager.*
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.os.Bundle
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat.checkSelfPermission
|
||||
import com.appttude.h_mal.atlas_weather.R
|
||||
import com.appttude.h_mal.atlas_weather.monoWeather.dialog.DeclarationBuilder
|
||||
import com.appttude.h_mal.atlas_weather.utils.displayToast
|
||||
import kotlinx.android.synthetic.monoWeather.permissions_declaration_dialog.*
|
||||
|
||||
const val PERMISSION_CODE = 401
|
||||
|
||||
class WidgetLocationPermissionActivity : AppCompatActivity() {
|
||||
class WidgetLocationPermissionActivity : AppCompatActivity(), DeclarationBuilder {
|
||||
override val link: String = "https://sites.google.com/view/hmaldev/home/monochrome"
|
||||
override var message: String = ""
|
||||
|
||||
private var mAppWidgetId = INVALID_APPWIDGET_ID
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
message = readFromResources(R.string.widget_declaration)
|
||||
|
||||
// Set the result to CANCELED. This will cause the widget host to cancel
|
||||
// out of the widget placement if the user presses the back button.
|
||||
setResult(RESULT_CANCELED)
|
||||
@@ -36,6 +44,10 @@ class WidgetLocationPermissionActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
setContentView(R.layout.permissions_declaration_dialog)
|
||||
findViewById<TextView>(R.id.declaration_text).apply {
|
||||
text = buildMessage()
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
submit.setOnClickListener {
|
||||
if (checkSelfPermission(this, ACCESS_COARSE_LOCATION) != PERMISSION_GRANTED) {
|
||||
@@ -44,6 +56,8 @@ class WidgetLocationPermissionActivity : AppCompatActivity() {
|
||||
submitWidget()
|
||||
}
|
||||
}
|
||||
|
||||
cancel.setOnClickListener { finish() }
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
|
||||
@@ -14,16 +14,10 @@ import com.appttude.h_mal.atlas_weather.viewmodel.WorldViewModel
|
||||
import kotlinx.android.synthetic.main.activity_add_forecast.*
|
||||
|
||||
|
||||
class AddLocationFragment : BaseFragment() {
|
||||
class AddLocationFragment : BaseFragment(R.layout.activity_add_forecast) {
|
||||
|
||||
private val viewModel by getFragmentViewModel<WorldViewModel>()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import kotlinx.android.synthetic.main.fragment_add_location.world_recycler
|
||||
* A simple [Fragment] subclass.
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
class WorldFragment : BaseFragment() {
|
||||
class WorldFragment : BaseFragment(R.layout.fragment__two) {
|
||||
private val viewModel by getFragmentViewModel<WorldViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -31,12 +31,6 @@ class WorldFragment : BaseFragment() {
|
||||
viewModel.fetchAllLocations()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment__two, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ import com.appttude.h_mal.atlas_weather.utils.generateView
|
||||
import com.appttude.h_mal.atlas_weather.utils.loadImage
|
||||
|
||||
class WorldRecyclerAdapter(
|
||||
val itemClick: (WeatherDisplay) -> Unit,
|
||||
val itemLongClick: (String) -> Unit
|
||||
private val itemClick: (WeatherDisplay) -> Unit,
|
||||
private val itemLongClick: (String) -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
var weather: MutableList<WeatherDisplay> = mutableListOf()
|
||||
|
||||
@@ -78,23 +78,28 @@ class WorldRecyclerAdapter(
|
||||
return if (weather.size == 0) 1 else weather.size
|
||||
}
|
||||
|
||||
internal class WorldHolderCurrent(listItemView: View) : RecyclerView.ViewHolder(listItemView) {
|
||||
internal class WorldHolderCurrent(cellView: View) : BaseViewHolder<WeatherDisplay>(cellView) {
|
||||
|
||||
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_temp_unit)
|
||||
private val locationTV: TextView = cellView.findViewById(R.id.db_location)
|
||||
private val conditionTV: TextView = cellView.findViewById(R.id.db_condition)
|
||||
private val weatherIV: ImageView = cellView.findViewById(R.id.db_icon)
|
||||
private val avgTempTV: TextView = cellView.findViewById(R.id.db_main_temp)
|
||||
private val tempUnit: TextView = cellView.findViewById(R.id.db_temp_unit)
|
||||
|
||||
fun bindData(weather: WeatherDisplay?){
|
||||
locationTV.text = weather?.displayName
|
||||
conditionTV.text = weather?.description
|
||||
weatherIV.loadImage(weather?.iconURL)
|
||||
avgTempTV.text = weather?.forecast?.get(0)?.mainTemp
|
||||
override fun bindData(data: WeatherDisplay?){
|
||||
locationTV.text = data?.displayName
|
||||
conditionTV.text = data?.description
|
||||
weatherIV.loadImage(data?.iconURL)
|
||||
avgTempTV.text = data?.forecast?.get(0)?.mainTemp
|
||||
tempUnit.text = itemView.context.getString(R.string.degrees)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class BaseViewHolder<T : Any>(cellView: View) : RecyclerView.ViewHolder(cellView) {
|
||||
|
||||
abstract fun bindData(data : T?)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,31 +2,28 @@ package com.appttude.h_mal.atlas_weather.monoWeather.widget
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.TaskStackBuilder
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.widget.RemoteViews
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.app.JobIntentService
|
||||
import com.appttude.h_mal.atlas_weather.helper.ServicesHelper
|
||||
import com.squareup.picasso.Picasso
|
||||
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
|
||||
|
||||
abstract class BaseWidgetServiceIntentClass<T: AppWidgetProvider> : JobIntentService(){
|
||||
abstract class BaseWidgetServiceIntentClass<T : AppWidgetProvider> : JobIntentService() {
|
||||
|
||||
lateinit var appWidgetManager: AppWidgetManager
|
||||
lateinit var appWidgetIds: IntArray
|
||||
|
||||
fun initBaseWidget(componentName: ComponentName){
|
||||
fun initBaseWidget(componentName: ComponentName) {
|
||||
appWidgetManager = AppWidgetManager.getInstance(baseContext)
|
||||
appWidgetIds = appWidgetManager.getAppWidgetIds(componentName)
|
||||
}
|
||||
@@ -45,21 +42,24 @@ abstract class BaseWidgetServiceIntentClass<T: AppWidgetProvider> : JobIntentSer
|
||||
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
val idArray = intArrayOf(appWidgetId)
|
||||
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, idArray)
|
||||
return PendingIntent.getBroadcast(
|
||||
this, seconds, intentUpdate,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.getBroadcast(this, seconds, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getBroadcast(this, seconds, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create a pending intent used to navigate to activity:
|
||||
* @param activityClass
|
||||
*/
|
||||
fun <T: Activity> createClickingPendingIntent(activityClass: Class<T>): PendingIntent {
|
||||
fun <T : Activity> createClickingPendingIntent(activityClass: Class<T>): PendingIntent {
|
||||
val clickIntentTemplate = Intent(this, activityClass)
|
||||
|
||||
return TaskStackBuilder.create(this)
|
||||
.addNextIntentWithParentStack(clickIntentTemplate)
|
||||
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
fun setImageView(
|
||||
@@ -67,7 +67,7 @@ abstract class BaseWidgetServiceIntentClass<T: AppWidgetProvider> : JobIntentSer
|
||||
views: RemoteViews,
|
||||
@IdRes viewId: Int,
|
||||
appWidgetId: Int
|
||||
){
|
||||
) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
Picasso.get().load(path).into(views, viewId, intArrayOf(appWidgetId))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user