UI updates launched from view models are now launched in a Dispatchers.Main context as some devices will throw an exception for UI thread operations on a non-UI thread

This commit is contained in:
Deniz Düzgören
2019-10-26 21:13:02 +02:00
parent 2561453811
commit e3f5fe7f6a
5 changed files with 60 additions and 52 deletions
@@ -1,16 +1,20 @@
package com.denizd.substitutionplan.database package com.denizd.substitutionplan.database
import android.annotation.TargetApi
import android.app.Application import android.app.Application
import android.database.Cursor import android.database.Cursor
import android.media.RingtoneManager import android.media.RingtoneManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.denizd.substitutionplan.data.Caller
import com.denizd.substitutionplan.data.SubstUtil import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.models.Ringtone import com.denizd.substitutionplan.models.Ringtone
internal class SettingsRepository(private val application: Application) { internal class SettingsRepository(private val application: Application) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(application) private val prefs = PreferenceManager.getDefaultSharedPreferences(application)
val notificationChannel = SubstUtil.getNotificationChannel(application, prefs)
@TargetApi(26)
fun getNotificationChannel() = SubstUtil.getNotificationChannel(application, prefs)
val ringtones: List<Ringtone> by lazy { val ringtones: List<Ringtone> by lazy {
lateinit var ringtoneCursor: Cursor lateinit var ringtoneCursor: Cursor
@@ -56,5 +60,12 @@ internal class SettingsRepository(private val application: Application) {
prefs.edit().putString(key, value).apply() prefs.edit().putString(key, value).apply()
} }
fun clearTimes() {
prefs.edit().putString("timeNew", "").putString("newFoodTime", "").apply()
}
fun forceRefresh(): Pair<String, Boolean> {
clearTimes()
return SubstRepository(application).fetchDataOnline(Caller.SETTINGS)
}
} }
@@ -17,6 +17,7 @@ import android.widget.*
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
@@ -32,6 +33,7 @@ import com.denizd.substitutionplan.models.Colour
import com.denizd.substitutionplan.viewmodels.SettingsViewModel import com.denizd.substitutionplan.viewmodels.SettingsViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessaging
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@@ -51,6 +53,7 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
private lateinit var binding: ContentSettingsBinding private lateinit var binding: ContentSettingsBinding
private lateinit var viewModel: SettingsViewModel private lateinit var viewModel: SettingsViewModel
private lateinit var snackBarContainer: CoordinatorLayout
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
@@ -66,6 +69,7 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this)[SettingsViewModel::class.java] viewModel = ViewModelProviders.of(this)[SettingsViewModel::class.java]
snackBarContainer = view.rootView.findViewById(R.id.coordination) // TODO replace findViewById with ViewBinding
binding.apply { binding.apply {
val thisFragment = this@SettingsFragment val thisFragment = this@SettingsFragment
@@ -92,7 +96,6 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
// Set on long click listeners // Set on long click listeners
buttonCustomiseColours.setOnLongClickListener(thisFragment) buttonCustomiseColours.setOnLongClickListener(thisFragment)
buttonCoursesHelp.setOnLongClickListener(thisFragment)
buttonOrderHelp.setOnLongClickListener(thisFragment) buttonOrderHelp.setOnLongClickListener(thisFragment)
buttonForcedRefresh.setOnLongClickListener(thisFragment) buttonForcedRefresh.setOnLongClickListener(thisFragment)
@@ -203,14 +206,13 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
R.id.button_courses_help -> createDialog(getString(R.string.courses_help_dialog_title), getString(R.string.courses_help_dialog_text)) R.id.button_courses_help -> createDialog(getString(R.string.courses_help_dialog_title), getString(R.string.courses_help_dialog_text))
R.id.button_order_help -> createDialog(getString(R.string.ordering_systems_dialog_title), getString(R.string.ordering_systems_dialog_text)) R.id.button_order_help -> createDialog(getString(R.string.ordering_systems_dialog_title), getString(R.string.ordering_systems_dialog_text))
R.id.button_forced_refresh -> { R.id.button_forced_refresh -> {
// val repo = SubstRepository(activity?.application!!) viewModel.forceRefresh { result, error ->
// repo.putAndApplyString("timeNew", "") val snackBar = Snackbar.make(snackBarContainer, result, Snackbar.LENGTH_LONG)
// repo.putAndApplyString("newFoodTime", "") if (error) {
// repo.fetchDataOnline(Caller.SETTINGS) snackBar.setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.colorError))
// TODO needs async }
// TODO put in viewModel snackBar.show()
// TODO create viewModel }
// TODO remove TODOs
} }
R.id.button_visit_website -> { R.id.button_visit_website -> {
try { try {
@@ -244,29 +246,12 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
customiseColoursButtonLongPressed = !customiseColoursButtonLongPressed customiseColoursButtonLongPressed = !customiseColoursButtonLongPressed
true true
} }
R.id.button_courses_help -> {
val link = Uri.parse("https://www.youtube.com/watch?v=Jc2xfYuLWgE") // 'Freak'
makeToast(String(Character.toChars(0x2764)))
try {
val intent = Intent(Intent.ACTION_VIEW, link)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).setPackage("com.google.android.youtube")
startActivity(intent)
} catch (e: ActivityNotFoundException) {
try {
customTabsIntent.launchUrl(mContext, link)
} catch (e: ActivityNotFoundException) {
makeToast(getString(R.string.youtube_not_found))
}
}
true
}
R.id.button_order_help -> { R.id.button_order_help -> {
debugMenu() debugMenu()
true true
} }
R.id.button_forced_refresh -> { R.id.button_forced_refresh -> {
viewModel.setAndApplyString("timeNew", "") viewModel.clearTimes()
viewModel.setAndApplyString("newFoodTime", "")
makeToast(mContext.getString(R.string.force_refresh_cleared_times)) makeToast(mContext.getString(R.string.force_refresh_cleared_times))
true true
} }
@@ -332,6 +317,8 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
} }
// Functions for handling dialog creation // Functions for handling dialog creation
// TODO put this in view model layer
// TODO possibly replace with DialogFragment
private fun createDialog(title: String, text: String) { private fun createDialog(title: String, text: String) {
val alertDialog = AlertDialog.Builder(mContext) val alertDialog = AlertDialog.Builder(mContext)
val dialogView = View.inflate(mContext, R.layout.simple_dialog, null) val dialogView = View.inflate(mContext, R.layout.simple_dialog, null)
@@ -359,7 +346,7 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
private fun createRingtoneDialog() { private fun createRingtoneDialog() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
viewModel.notificationChannel viewModel.getNotificationChannel()
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, mContext.applicationContext.packageName) putExtra(Settings.EXTRA_APP_PACKAGE, mContext.applicationContext.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, "general") putExtra(Settings.EXTRA_CHANNEL_ID, "general")
@@ -602,14 +589,15 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
// Below this point follow string literals that I didn't bother putting in /res/values/strings // Below this point follow string literals that I didn't bother putting in /res/values/strings
// TODO check if this is displayed properly // TODO check if this is displayed properly
private val licences = """Libraries: private val licences = """
• jsoup HTML parser © 2009-2018 Jonathan Hedley, licensed under the open source MIT Licence Libraries:
Font: • jsoup HTML parser © 2009-2018 Jonathan Hedley, licensed under the open source MIT Licence
• Manrope © 2018-2019 Michael Sharanda, licensed under the SIL Open Font Licence 1.1
Icons: Font:
• bqlqn • Manrope © 2018-2019 Michael Sharanda, licensed under the SIL Open Font Licence 1.1
• fjstudio
• Freepik Icons:
Smashicons • Authors: bqlqn, fjstudio, Freepik, Smashicons
• © 2013-2019 Freepik Company S.L., licensed under Creative Commons BY 3.0""".trimIndent() • © 2013-2019 Freepik Company S.L., licensed under Creative Commons BY 3.0
""".trimIndent()
} }
@@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData
import com.denizd.substitutionplan.data.Caller import com.denizd.substitutionplan.data.Caller
import com.denizd.substitutionplan.database.SubstRepository import com.denizd.substitutionplan.database.SubstRepository
import com.denizd.substitutionplan.models.Food import com.denizd.substitutionplan.models.Food
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -23,9 +24,9 @@ internal class FoodViewModel(application: Application) : AndroidViewModel(applic
fun refresh(updateUi: (result: String, error: Boolean) -> Unit) { fun refresh(updateUi: (result: String, error: Boolean) -> Unit) {
GlobalScope.launch { GlobalScope.launch {
val task = GlobalScope.async { repo.fetchDataOnline(Caller.FOODMENU) } val task = async { repo.fetchDataOnline(Caller.FOODMENU) }
val result = task.await() val result = task.await()
updateUi(result.first, result.second) launch(Dispatchers.Main) { updateUi(result.first, result.second) }
} }
} }
} }
@@ -1,10 +1,11 @@
package com.denizd.substitutionplan.viewmodels package com.denizd.substitutionplan.viewmodels
import android.annotation.TargetApi
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.database.SettingsRepository import com.denizd.substitutionplan.database.SettingsRepository
import com.denizd.substitutionplan.models.Ringtone import com.denizd.substitutionplan.models.Ringtone
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -25,7 +26,7 @@ internal class SettingsViewModel(application: Application) : AndroidViewModel(ap
get() = repo.getBool("autoRefresh") get() = repo.getBool("autoRefresh")
set(it) = repo.setAndApplyBool("autoRefresh", it) set(it) = repo.setAndApplyBool("autoRefresh", it)
var shouldUseAppSorting: Boolean var shouldUseAppSorting: Boolean
get() = repo.getBool("app_specific_sorting") get() = repo.getBool("app_specific_sorting", true)
set(it) = repo.setAndApplyBool("app_specific_sorting", it) set(it) = repo.setAndApplyBool("app_specific_sorting", it)
@@ -43,16 +44,17 @@ internal class SettingsViewModel(application: Application) : AndroidViewModel(ap
get() = repo.getInt("themeInt") get() = repo.getInt("themeInt")
set(it) = repo.setAndApplyInt("themeInt", it) set(it) = repo.setAndApplyInt("themeInt", it)
val notificationChannel = repo.notificationChannel @TargetApi(26)
fun getNotificationChannel() = repo.getNotificationChannel()
lateinit var ringtones: List<Ringtone> lateinit var ringtones: List<Ringtone>
val ringtonesInitialised = ::ringtones.isInitialized val ringtonesInitialised = ::ringtones.isInitialized
fun getRingtones(updateUi: () -> Unit) { fun getRingtones(updateUi: () -> Unit) {
GlobalScope.launch { GlobalScope.launch {
val task = GlobalScope.async { repo.ringtones } val task = async { repo.ringtones }
ringtones = task.await() ringtones = task.await()
updateUi() launch(Dispatchers.Main) { updateUi() }
} }
} }
@@ -62,4 +64,11 @@ internal class SettingsViewModel(application: Application) : AndroidViewModel(ap
fun setAndApplyBool(key: String, value: Boolean) = repo.setAndApplyBool(key, value) fun setAndApplyBool(key: String, value: Boolean) = repo.setAndApplyBool(key, value)
fun getString(key: String, default: String = "", ifNull: String = "") = repo.getString(key, default) ?: ifNull fun getString(key: String, default: String = "", ifNull: String = "") = repo.getString(key, default) ?: ifNull
fun setAndApplyString(key: String, value: String) = repo.setAndApplyString(key, value) fun setAndApplyString(key: String, value: String) = repo.setAndApplyString(key, value)
fun clearTimes() = repo.clearTimes()
fun forceRefresh(updateUi: (result: String, error: Boolean) -> Unit) = GlobalScope.launch {
val task = async { repo.forceRefresh() }
val result = task.await()
launch(Dispatchers.Main) { updateUi(result.first, result.second) }
}
} }
@@ -4,6 +4,7 @@ import android.app.Application
import android.content.res.Configuration import android.content.res.Configuration
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.denizd.substitutionplan.data.Caller import com.denizd.substitutionplan.data.Caller
import com.denizd.substitutionplan.database.SubstRepository import com.denizd.substitutionplan.database.SubstRepository
import com.denizd.substitutionplan.models.Substitution import com.denizd.substitutionplan.models.Substitution
@@ -37,11 +38,9 @@ internal class SubstViewModel(application: Application) : AndroidViewModel(appli
return repo.getGridColumnCount(config) return repo.getGridColumnCount(config)
} }
fun refresh(updateUi: (result: String, error: Boolean) -> Unit) { fun refresh(updateUi: (result: String, error: Boolean) -> Unit) = GlobalScope.launch {
GlobalScope.launch { val task = async { repo.fetchDataOnline(Caller.SUBSTITUTION) }
val task = GlobalScope.async { repo.fetchDataOnline(Caller.SUBSTITUTION) } val result = task.await()
val result = task.await() launch(Dispatchers.Main) { updateUi(result.first, result.second) }
updateUi(result.first, result.second)
}
} }
} }