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
import android.annotation.TargetApi
import android.app.Application
import android.database.Cursor
import android.media.RingtoneManager
import androidx.preference.PreferenceManager
import com.denizd.substitutionplan.data.Caller
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.models.Ringtone
internal class SettingsRepository(private val application: 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 {
lateinit var ringtoneCursor: Cursor
@@ -56,5 +60,12 @@ internal class SettingsRepository(private val application: Application) {
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.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
@@ -32,6 +33,7 @@ import com.denizd.substitutionplan.models.Colour
import com.denizd.substitutionplan.viewmodels.SettingsViewModel
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.button.MaterialButton
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.messaging.FirebaseMessaging
import kotlin.collections.ArrayList
@@ -51,6 +53,7 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
private lateinit var binding: ContentSettingsBinding
private lateinit var viewModel: SettingsViewModel
private lateinit var snackBarContainer: CoordinatorLayout
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -66,6 +69,7 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProviders.of(this)[SettingsViewModel::class.java]
snackBarContainer = view.rootView.findViewById(R.id.coordination) // TODO replace findViewById with ViewBinding
binding.apply {
val thisFragment = this@SettingsFragment
@@ -92,7 +96,6 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
// Set on long click listeners
buttonCustomiseColours.setOnLongClickListener(thisFragment)
buttonCoursesHelp.setOnLongClickListener(thisFragment)
buttonOrderHelp.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_order_help -> createDialog(getString(R.string.ordering_systems_dialog_title), getString(R.string.ordering_systems_dialog_text))
R.id.button_forced_refresh -> {
// val repo = SubstRepository(activity?.application!!)
// repo.putAndApplyString("timeNew", "")
// repo.putAndApplyString("newFoodTime", "")
// repo.fetchDataOnline(Caller.SETTINGS)
// TODO needs async
// TODO put in viewModel
// TODO create viewModel
// TODO remove TODOs
viewModel.forceRefresh { result, error ->
val snackBar = Snackbar.make(snackBarContainer, result, Snackbar.LENGTH_LONG)
if (error) {
snackBar.setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.colorError))
}
snackBar.show()
}
}
R.id.button_visit_website -> {
try {
@@ -244,29 +246,12 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
customiseColoursButtonLongPressed = !customiseColoursButtonLongPressed
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 -> {
debugMenu()
true
}
R.id.button_forced_refresh -> {
viewModel.setAndApplyString("timeNew", "")
viewModel.setAndApplyString("newFoodTime", "")
viewModel.clearTimes()
makeToast(mContext.getString(R.string.force_refresh_cleared_times))
true
}
@@ -332,6 +317,8 @@ internal class SettingsFragment : Fragment(), View.OnClickListener, View.OnLongC
}
// Functions for handling dialog creation
// TODO put this in view model layer
// TODO possibly replace with DialogFragment
private fun createDialog(title: String, text: String) {
val alertDialog = AlertDialog.Builder(mContext)
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() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
viewModel.notificationChannel
viewModel.getNotificationChannel()
val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, mContext.applicationContext.packageName)
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
// TODO check if this is displayed properly
private val licences = """Libraries:
• jsoup HTML parser © 2009-2018 Jonathan Hedley, licensed under the open source MIT Licence
Font:
• Manrope © 2018-2019 Michael Sharanda, licensed under the SIL Open Font Licence 1.1
Icons:
• bqlqn
• fjstudio
• Freepik
Smashicons
• © 2013-2019 Freepik Company S.L., licensed under Creative Commons BY 3.0""".trimIndent()
private val licences = """
Libraries:
• jsoup HTML parser © 2009-2018 Jonathan Hedley, licensed under the open source MIT Licence
Font:
• Manrope © 2018-2019 Michael Sharanda, licensed under the SIL Open Font Licence 1.1
Icons:
• Authors: bqlqn, fjstudio, Freepik, Smashicons
• © 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.database.SubstRepository
import com.denizd.substitutionplan.models.Food
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@@ -23,9 +24,9 @@ internal class FoodViewModel(application: Application) : AndroidViewModel(applic
fun refresh(updateUi: (result: String, error: Boolean) -> Unit) {
GlobalScope.launch {
val task = GlobalScope.async { repo.fetchDataOnline(Caller.FOODMENU) }
val task = async { repo.fetchDataOnline(Caller.FOODMENU) }
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
import android.annotation.TargetApi
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.denizd.substitutionplan.data.SubstUtil
import com.denizd.substitutionplan.database.SettingsRepository
import com.denizd.substitutionplan.models.Ringtone
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@@ -25,7 +26,7 @@ internal class SettingsViewModel(application: Application) : AndroidViewModel(ap
get() = repo.getBool("autoRefresh")
set(it) = repo.setAndApplyBool("autoRefresh", it)
var shouldUseAppSorting: Boolean
get() = repo.getBool("app_specific_sorting")
get() = repo.getBool("app_specific_sorting", true)
set(it) = repo.setAndApplyBool("app_specific_sorting", it)
@@ -43,16 +44,17 @@ internal class SettingsViewModel(application: Application) : AndroidViewModel(ap
get() = repo.getInt("themeInt")
set(it) = repo.setAndApplyInt("themeInt", it)
val notificationChannel = repo.notificationChannel
@TargetApi(26)
fun getNotificationChannel() = repo.getNotificationChannel()
lateinit var ringtones: List<Ringtone>
val ringtonesInitialised = ::ringtones.isInitialized
fun getRingtones(updateUi: () -> Unit) {
GlobalScope.launch {
val task = GlobalScope.async { repo.ringtones }
val task = async { repo.ringtones }
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 getString(key: String, default: String = "", ifNull: String = "") = repo.getString(key, default) ?: ifNull
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 androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.denizd.substitutionplan.data.Caller
import com.denizd.substitutionplan.database.SubstRepository
import com.denizd.substitutionplan.models.Substitution
@@ -37,11 +38,9 @@ internal class SubstViewModel(application: Application) : AndroidViewModel(appli
return repo.getGridColumnCount(config)
}
fun refresh(updateUi: (result: String, error: Boolean) -> Unit) {
GlobalScope.launch {
val task = GlobalScope.async { repo.fetchDataOnline(Caller.SUBSTITUTION) }
val result = task.await()
updateUi(result.first, result.second)
}
fun refresh(updateUi: (result: String, error: Boolean) -> Unit) = GlobalScope.launch {
val task = async { repo.fetchDataOnline(Caller.SUBSTITUTION) }
val result = task.await()
launch(Dispatchers.Main) { updateUi(result.first, result.second) }
}
}