🛠️ Fix not thread-safe _state.update() calls

* Fixed multiple issues with update calls being executed simultaneously
This commit is contained in:
Acclorite 2024-12-28 16:02:16 +02:00
parent a25cb8f3eb
commit bc9c378e43
9 changed files with 344 additions and 226 deletions

View file

@ -10,8 +10,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.use_case.remote.CheckForUpdates import ua.acclorite.book_story.domain.use_case.remote.CheckForUpdates
@ -25,6 +26,8 @@ class AboutModel @Inject constructor(
private val checkForUpdates: CheckForUpdates private val checkForUpdates: CheckForUpdates
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(AboutState()) private val _state = MutableStateFlow(AboutState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -100,10 +103,18 @@ class AboutModel @Inject constructor(
} }
is AboutEvent.OnDismissDialog -> { is AboutEvent.OnDismissDialog -> {
_state.update { viewModelScope.launch {
it.copy(dialog = null) _state.update {
it.copy(dialog = null)
}
} }
} }
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -18,8 +18,9 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -55,6 +56,8 @@ class BookInfoModel @Inject constructor(
private val updateBookWithText: UpdateBookWithText private val updateBookWithText: UpdateBookWithText
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(BookInfoState()) private val _state = MutableStateFlow(BookInfoState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -384,12 +387,13 @@ class BookInfoModel @Inject constructor(
is BookInfoEvent.OnCheckCoverReset -> { is BookInfoEvent.OnCheckCoverReset -> {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
_state.update { if (_state.value.book.id == -1) return@launch
it.copy( canResetCover.execute(_state.value.book.id).apply {
canResetCover = canResetCover.execute( _state.update {
_state.value.book.id it.copy(
canResetCover = this
) )
) }
} }
} }
} }
@ -780,16 +784,16 @@ class BookInfoModel @Inject constructor(
return@launch return@launch
} }
eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob()
_state.update { _state.update {
BookInfoState( BookInfoState(
book = book book = book
) )
} }
eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob()
if (startUpdate) { if (startUpdate) {
onEvent( onEvent(
BookInfoEvent.OnCheckForTextUpdate( BookInfoEvent.OnCheckForTextUpdate(
@ -804,13 +808,14 @@ class BookInfoModel @Inject constructor(
fun resetScreen() { fun resetScreen() {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
_state.update {
BookInfoState()
}
eventJob.cancel() eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob() eventJob = SupervisorJob()
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -18,8 +18,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -47,6 +48,8 @@ class BrowseModel @Inject constructor(
private val insertBook: InsertBook private val insertBook: InsertBook
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(BrowseState()) private val _state = MutableStateFlow(BrowseState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -119,27 +122,31 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnRequestFocus -> { is BrowseEvent.OnRequestFocus -> {
if (!_state.value.hasFocused) { viewModelScope.launch(Dispatchers.Main) {
event.focusRequester.requestFocus() if (!_state.value.hasFocused) {
_state.update { event.focusRequester.requestFocus()
it.copy( _state.update {
hasFocused = true it.copy(
) hasFocused = true
)
}
} }
} }
} }
is BrowseEvent.OnSearchQueryChange -> { is BrowseEvent.OnSearchQueryChange -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
searchQuery = event.query it.copy(
) searchQuery = event.query
} )
changeSearchQueryJob?.cancel() }
changeSearchQueryJob = viewModelScope.launch(Dispatchers.IO) { changeSearchQueryJob?.cancel()
delay(500) changeSearchQueryJob = launch(Dispatchers.IO) {
yield() delay(500)
onEvent(BrowseEvent.OnSearch) yield()
onEvent(BrowseEvent.OnSearch)
}
} }
} }
@ -292,18 +299,22 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnShowFilterBottomSheet -> { is BrowseEvent.OnShowFilterBottomSheet -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
bottomSheet = BrowseScreen.FILTER_BOTTOM_SHEET it.copy(
) bottomSheet = BrowseScreen.FILTER_BOTTOM_SHEET
)
}
} }
} }
is BrowseEvent.OnDismissBottomSheet -> { is BrowseEvent.OnDismissBottomSheet -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
bottomSheet = null it.copy(
) bottomSheet = null
)
}
} }
} }
@ -413,20 +424,22 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnDismissPermissionDialog -> { is BrowseEvent.OnDismissPermissionDialog -> {
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R viewModelScope.launch {
val isPermissionGranted = if (!legacyPermission) { val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
Environment.isExternalStorageManager() val isPermissionGranted = if (!legacyPermission) {
} else event.storagePermissionState.status.isGranted Environment.isExternalStorageManager()
} else event.storagePermissionState.status.isGranted
storagePermissionJob?.cancel() storagePermissionJob?.cancel()
_state.update { it.copy(dialog = null) } _state.update { it.copy(dialog = null) }
if (isPermissionGranted) { if (isPermissionGranted) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getFilesFromDownloads() getFilesFromDownloads()
}
} else {
_state.update { it.copy(isError = true) }
} }
} else {
_state.update { it.copy(isError = true) }
} }
} }
@ -484,12 +497,14 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnDismissAddDialog -> { is BrowseEvent.OnDismissAddDialog -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
dialog = null it.copy(
) dialog = null
)
}
getAddDialogBooksJob?.cancel()
} }
getAddDialogBooksJob?.cancel()
} }
is BrowseEvent.OnActionAddDialog -> { is BrowseEvent.OnActionAddDialog -> {
@ -574,10 +589,12 @@ class BrowseModel @Inject constructor(
} }
is BrowseEvent.OnDismissDialog -> { is BrowseEvent.OnDismissDialog -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
dialog = null it.copy(
) dialog = null
)
}
} }
} }
} }
@ -601,8 +618,10 @@ class BrowseModel @Inject constructor(
} }
fun resetScreen() { fun resetScreen() {
storagePermissionJob?.cancel() viewModelScope.launch {
_state.update { it.copy(isError = false) } storagePermissionJob?.cancel()
_state.update { it.copy(isError = false) }
}
} }
fun filterList( fun filterList(
@ -710,4 +729,10 @@ class BrowseModel @Inject constructor(
) )
) )
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -11,8 +11,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -42,6 +43,8 @@ class HistoryModel @Inject constructor(
private val deleteWholeHistory: DeleteWholeHistory private val deleteWholeHistory: DeleteWholeHistory
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(HistoryState()) private val _state = MutableStateFlow(HistoryState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -136,27 +139,31 @@ class HistoryModel @Inject constructor(
} }
is HistoryEvent.OnRequestFocus -> { is HistoryEvent.OnRequestFocus -> {
if (!_state.value.hasFocused) { viewModelScope.launch(Dispatchers.Main) {
event.focusRequester.requestFocus() if (!_state.value.hasFocused) {
_state.update { event.focusRequester.requestFocus()
it.copy( _state.update {
hasFocused = true it.copy(
) hasFocused = true
)
}
} }
} }
} }
is HistoryEvent.OnSearchQueryChange -> { is HistoryEvent.OnSearchQueryChange -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
searchQuery = event.query it.copy(
) searchQuery = event.query
} )
searchQueryChange?.cancel() }
searchQueryChange = viewModelScope.launch(Dispatchers.IO) { searchQueryChange?.cancel()
delay(500) searchQueryChange = launch(Dispatchers.IO) {
yield() delay(500)
onEvent(HistoryEvent.OnSearch) yield()
onEvent(HistoryEvent.OnSearch)
}
} }
} }
@ -209,10 +216,12 @@ class HistoryModel @Inject constructor(
} }
is HistoryEvent.OnShowDeleteWholeHistoryDialog -> { is HistoryEvent.OnShowDeleteWholeHistoryDialog -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG it.copy(
) dialog = HistoryScreen.DELETE_WHOLE_HISTORY_DIALOG
)
}
} }
} }
@ -238,10 +247,12 @@ class HistoryModel @Inject constructor(
} }
is HistoryEvent.OnDismissDialog -> { is HistoryEvent.OnDismissDialog -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
dialog = null it.copy(
) dialog = null
)
}
} }
} }
} }
@ -314,4 +325,10 @@ class HistoryModel @Inject constructor(
) )
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -10,8 +10,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -32,6 +33,8 @@ class LibraryModel @Inject constructor(
private val moveBooks: UpdateBook private val moveBooks: UpdateBook
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(LibraryState()) private val _state = MutableStateFlow(LibraryState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -104,16 +107,18 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnSearchQueryChange -> { is LibraryEvent.OnSearchQueryChange -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
searchQuery = event.query it.copy(
) searchQuery = event.query
} )
searchQueryChange?.cancel() }
searchQueryChange = viewModelScope.launch(Dispatchers.IO) { searchQueryChange?.cancel()
delay(500) searchQueryChange = launch(Dispatchers.IO) {
yield() delay(500)
onEvent(LibraryEvent.OnSearch) yield()
onEvent(LibraryEvent.OnSearch)
}
} }
} }
@ -124,12 +129,14 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnRequestFocus -> { is LibraryEvent.OnRequestFocus -> {
if (!_state.value.hasFocused) { viewModelScope.launch(Dispatchers.Main) {
event.focusRequester.requestFocus() if (!_state.value.hasFocused) {
_state.update { event.focusRequester.requestFocus()
it.copy( _state.update {
hasFocused = true it.copy(
) hasFocused = true
)
}
} }
} }
} }
@ -163,10 +170,12 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnShowMoveDialog -> { is LibraryEvent.OnShowMoveDialog -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
dialog = LibraryScreen.MOVE_DIALOG it.copy(
) dialog = LibraryScreen.MOVE_DIALOG
)
}
} }
} }
@ -213,10 +222,12 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnShowDeleteDialog -> { is LibraryEvent.OnShowDeleteDialog -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
dialog = LibraryScreen.DELETE_DIALOG it.copy(
) dialog = LibraryScreen.DELETE_DIALOG
)
}
} }
} }
@ -249,10 +260,12 @@ class LibraryModel @Inject constructor(
} }
is LibraryEvent.OnDismissDialog -> { is LibraryEvent.OnDismissDialog -> {
_state.update { viewModelScope.launch {
it.copy( _state.update {
dialog = null it.copy(
) dialog = null
)
}
} }
} }
} }
@ -273,4 +286,10 @@ class LibraryModel @Inject constructor(
) )
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -11,8 +11,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure import ua.acclorite.book_story.domain.browse.toBrowseFilesStructure
import ua.acclorite.book_story.domain.browse.toBrowseLayout import ua.acclorite.book_story.domain.browse.toBrowseLayout
@ -45,6 +46,8 @@ class MainModel @Inject constructor(
private val getAllSettings: GetAllSettings private val getAllSettings: GetAllSettings
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _isReady = MutableStateFlow(false) private val _isReady = MutableStateFlow(false)
val isReady = _isReady.asStateFlow() val isReady = _isReady.asStateFlow()
@ -506,4 +509,10 @@ class MainModel @Inject constructor(
} }
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -22,8 +22,9 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -54,6 +55,8 @@ class ReaderModel @Inject constructor(
private val checkForTextUpdate: CheckForTextUpdate private val checkForTextUpdate: CheckForTextUpdate
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(ReaderState()) private val _state = MutableStateFlow(ReaderState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -550,13 +553,14 @@ class ReaderModel @Inject constructor(
return@launch return@launch
} }
eventJob.cancel()
eventJob.join()
eventJob = SupervisorJob()
_state.update { _state.update {
ReaderState(book = book) ReaderState(book = book)
} }
eventJob.cancel()
eventJob = SupervisorJob()
onEvent( onEvent(
ReaderEvent.OnMenuVisibility( ReaderEvent.OnMenuVisibility(
show = false, show = false,
@ -606,12 +610,14 @@ class ReaderModel @Inject constructor(
} }
private fun updateChapter(index: Int) { private fun updateChapter(index: Int) {
val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index) viewModelScope.launch {
_state.update { val (currentChapter, currentChapterProgress) = calculateCurrentChapter(index)
it.copy( _state.update {
currentChapter = currentChapter, it.copy(
currentChapterProgress = currentChapterProgress currentChapter = currentChapter,
) currentChapterProgress = currentChapterProgress
)
}
} }
} }
@ -672,12 +678,14 @@ class ReaderModel @Inject constructor(
fun resetScreen() { fun resetScreen() {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
_state.update {
ReaderState()
}
eventJob.cancel() eventJob.cancel()
eventJob = SupervisorJob() eventJob = SupervisorJob()
} }
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -15,8 +15,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
@ -42,6 +43,8 @@ class SettingsModel @Inject constructor(
private val deleteColorPreset: DeleteColorPreset private val deleteColorPreset: DeleteColorPreset
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(SettingsState()) private val _state = MutableStateFlow(SettingsState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -536,4 +539,10 @@ class SettingsModel @Inject constructor(
updateColorColorPresetJob?.cancel() updateColorColorPresetJob?.cancel()
deleteColorPresetJob?.cancel() deleteColorPresetJob?.cancel()
} }
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
}
}
} }

View file

@ -16,8 +16,9 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import ua.acclorite.book_story.presentation.core.util.launchActivity import ua.acclorite.book_story.presentation.core.util.launchActivity
import javax.inject.Inject import javax.inject.Inject
@ -27,6 +28,8 @@ class StartModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private val mutex = Mutex()
private val _state = MutableStateFlow(StartState()) private val _state = MutableStateFlow(StartState())
val state = _state.asStateFlow() val state = _state.asStateFlow()
@ -59,110 +62,114 @@ class StartModel @Inject constructor(
} }
is StartEvent.OnStoragePermissionRequest -> { is StartEvent.OnStoragePermissionRequest -> {
val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R viewModelScope.launch {
val legacyStoragePermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
val isPermissionGranted = if (legacyStoragePermission) { val isPermissionGranted = if (legacyStoragePermission) {
event.storagePermissionState.status.isGranted event.storagePermissionState.status.isGranted
} else Environment.isExternalStorageManager() } else Environment.isExternalStorageManager()
if (isPermissionGranted) {
_state.update {
it.copy(
storagePermissionGranted = true
)
}
return
}
if (legacyStoragePermission) {
if (!event.storagePermissionState.status.shouldShowRationale) {
event.storagePermissionState.launchPermissionRequest()
} else {
val uri = Uri.parse("package:${event.activity.packageName}")
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
intent.launchActivity(event.activity) {
return
}
}
}
if (!legacyStoragePermission) {
val uri = Uri.parse("package:${event.activity.packageName}")
val intent = Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
uri
)
intent.launchActivity(event.activity) {
return
}
}
storagePermissionJob?.cancel()
storagePermissionJob = viewModelScope.launch {
while (true) {
val granted = if (legacyStoragePermission) {
event.storagePermissionState.status.isGranted
} else Environment.isExternalStorageManager()
if (!granted) {
delay(1000)
yield()
continue
}
yield()
if (isPermissionGranted) {
_state.update { _state.update {
it.copy( it.copy(
storagePermissionGranted = true storagePermissionGranted = true
) )
} }
break return@launch
}
if (legacyStoragePermission) {
if (!event.storagePermissionState.status.shouldShowRationale) {
event.storagePermissionState.launchPermissionRequest()
} else {
val uri = Uri.parse("package:${event.activity.packageName}")
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
intent.launchActivity(event.activity) {
return@launch
}
}
}
if (!legacyStoragePermission) {
val uri = Uri.parse("package:${event.activity.packageName}")
val intent = Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
uri
)
intent.launchActivity(event.activity) {
return@launch
}
}
storagePermissionJob?.cancel()
storagePermissionJob = viewModelScope.launch {
while (true) {
val granted = if (legacyStoragePermission) {
event.storagePermissionState.status.isGranted
} else Environment.isExternalStorageManager()
if (!granted) {
delay(1000)
yield()
continue
}
yield()
_state.update {
it.copy(
storagePermissionGranted = true
)
}
break
}
} }
} }
} }
is StartEvent.OnNotificationsPermissionRequest -> { is StartEvent.OnNotificationsPermissionRequest -> {
if (event.notificationsPermissionState.status.isGranted) { viewModelScope.launch {
_state.update { if (event.notificationsPermissionState.status.isGranted) {
it.copy(
notificationsPermissionGranted = true
)
}
return
}
if (!event.notificationsPermissionState.status.shouldShowRationale) {
event.notificationsPermissionState.launchPermissionRequest()
} else {
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName)
intent.launchActivity(event.activity) {
return
}
}
notificationsPermissionJob?.cancel()
notificationsPermissionJob = viewModelScope.launch {
while (true) {
if (!event.notificationsPermissionState.status.isGranted) {
delay(1000)
yield()
continue
}
yield()
_state.update { _state.update {
it.copy( it.copy(
notificationsPermissionGranted = true notificationsPermissionGranted = true
) )
} }
return@launch
}
break if (!event.notificationsPermissionState.status.shouldShowRationale) {
event.notificationsPermissionState.launchPermissionRequest()
} else {
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
intent.putExtra(Settings.EXTRA_APP_PACKAGE, event.activity.packageName)
intent.launchActivity(event.activity) {
return@launch
}
}
notificationsPermissionJob?.cancel()
notificationsPermissionJob = viewModelScope.launch {
while (true) {
if (!event.notificationsPermissionState.status.isGranted) {
delay(1000)
yield()
continue
}
yield()
_state.update {
it.copy(
notificationsPermissionGranted = true
)
}
break
}
} }
} }
} }
@ -170,11 +177,19 @@ class StartModel @Inject constructor(
} }
fun resetScreen() { fun resetScreen() {
_state.update { viewModelScope.launch {
storagePermissionJob?.cancel() _state.update {
notificationsPermissionJob?.cancel() storagePermissionJob?.cancel()
notificationsPermissionJob?.cancel()
StartState() StartState()
}
}
}
private suspend inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
mutex.withLock {
this.value = function(this.value)
} }
} }
} }