Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 0 additions & 43 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.viewbinding.ViewBinding
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
Expand All @@ -62,7 +61,6 @@ import com.ichi2.anki.android.input.ShortcutGroup
import com.ichi2.anki.android.input.ShortcutGroupProvider
import com.ichi2.anki.android.input.shortcut
import com.ichi2.anki.common.annotations.LegacyNotifications
import com.ichi2.anki.common.utils.android.isRobolectric
import com.ichi2.anki.common.utils.annotation.KotlinCleanup
import com.ichi2.anki.dialogs.AsyncDialogFragment
import com.ichi2.anki.dialogs.DatabaseErrorDialog
Expand All @@ -88,12 +86,8 @@ import com.ichi2.compat.customtabs.CustomTabsFallback
import com.ichi2.compat.customtabs.CustomTabsHelper
import com.ichi2.themes.Themes
import com.ichi2.utils.AdaptionUtil
import com.ichi2.utils.HandlerUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
Expand Down Expand Up @@ -698,43 +692,6 @@ open class AnkiActivity(
return false
}

// TODO: Move this to an extension method once we have context parameters
protected fun <T> Flow<T>.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
this@launchCollectionInLifecycleScope.collect {
if (isRobolectric) {
// hack: lifecycleScope/runOnUiThread do not handle our
// test dispatcher overriding both IO and Main
// in tests, waitForAsyncTasksToComplete may be required.
HandlerUtils.postOnNewHandler { runBlocking { block(it) } }
} else {
block(it)
}
}
}
}
}

// see above:
protected fun <T> StateFlow<T>.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) {
lifecycleScope.launch {
var lastValue: T? = null
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
this@launchCollectionInLifecycleScope.collect {
// on re-resume, an unchanged value will be emitted for a StateFlow
if (lastValue == value) return@collect
lastValue = value
if (isRobolectric) {
HandlerUtils.postOnNewHandler { runBlocking { block(it) } }
} else {
block(it)
}
}
}
}
}

override val shortcuts
get(): ShortcutGroup? = null

Expand Down
1 change: 1 addition & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.ui.ResizablePaneManager
import com.ichi2.anki.ui.internationalization.toSentenceCase
import com.ichi2.anki.utils.ext.launchCollectionInLifecycleScope
import com.ichi2.anki.utils.ext.showDialogFragment
import com.ichi2.ui.CardBrowserSearchView
import com.ichi2.utils.AndroidUiUtils.hideKeyboard
Expand Down
1 change: 1 addition & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ import com.ichi2.anki.utils.Destination
import com.ichi2.anki.utils.ShortcutUtils
import com.ichi2.anki.utils.ext.dismissAllDialogFragments
import com.ichi2.anki.utils.ext.getSizeOfBitmapFromCollection
import com.ichi2.anki.utils.ext.launchCollectionInLifecycleScope
import com.ichi2.anki.utils.ext.setFragmentResultListener
import com.ichi2.anki.utils.ext.showDialogFragment
import com.ichi2.anki.utils.runWithOOMCheck
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
Expand Down Expand Up @@ -70,7 +69,6 @@ import com.ichi2.anki.browser.RepositionCardFragment.Companion.REQUEST_REPOSITIO
import com.ichi2.anki.browser.RepositionCardsRequest.ContainsNonNewCardsError
import com.ichi2.anki.browser.RepositionCardsRequest.RepositionData
import com.ichi2.anki.common.annotations.NeedsTest
import com.ichi2.anki.common.utils.android.isRobolectric
import com.ichi2.anki.common.utils.annotation.KotlinCleanup
import com.ichi2.anki.dialogs.BrowserOptionsDialog
import com.ichi2.anki.dialogs.CardBrowserOrderDialog
Expand Down Expand Up @@ -98,17 +96,15 @@ import com.ichi2.anki.ui.attachFastScroller
import com.ichi2.anki.undoAndShowSnackbar
import com.ichi2.anki.utils.ext.getCurrentDialogFragment
import com.ichi2.anki.utils.ext.ifNotZero
import com.ichi2.anki.utils.ext.launchCollectionInLifecycleScope
import com.ichi2.anki.utils.ext.setFragmentResultListener
import com.ichi2.anki.utils.ext.showDialogFragment
import com.ichi2.anki.utils.ext.visibleItemPositions
import com.ichi2.anki.utils.showDialogFragmentImpl
import com.ichi2.anki.withProgress
import com.ichi2.utils.HandlerUtils
import com.ichi2.utils.TagsUtil.getUpdatedTags
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.ankiweb.rsdroid.Translations
import timber.log.Timber

Expand Down Expand Up @@ -954,21 +950,6 @@ class CardBrowserFragment :

private fun requireCardBrowserActivity(): CardBrowser = requireActivity() as CardBrowser

// TODO: Move this to an extension method once we have context parameters
private fun <T> Flow<T>.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
this@launchCollectionInLifecycleScope.collect {
if (isRobolectric) {
HandlerUtils.postOnNewHandler { runBlocking { block(it) } }
} else {
block(it)
}
}
}
}
}

/**
* Updates the tags of selected/checked notes and saves them to the disk
* @param selectedTags list of checked tags
Expand Down
25 changes: 1 addition & 24 deletions AnkiDroid/src/main/java/com/ichi2/anki/pages/ImageOcclusion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,19 @@ import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.appbar.MaterialToolbar
import com.ichi2.anki.R
import com.ichi2.anki.SingleFragmentActivity
import com.ichi2.anki.common.annotations.NeedsTest
import com.ichi2.anki.common.utils.android.isRobolectric
import com.ichi2.anki.dialogs.DeckSelectionDialog
import com.ichi2.anki.dialogs.DiscardChangesDialog
import com.ichi2.anki.model.SelectableDeck
import com.ichi2.anki.pages.viewmodel.ImageOcclusionArgs
import com.ichi2.anki.pages.viewmodel.ImageOcclusionViewModel
import com.ichi2.anki.pages.viewmodel.ImageOcclusionViewModel.Companion.IO_ARGS_KEY
import com.ichi2.anki.startDeckSelection
import com.ichi2.utils.HandlerUtils
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import com.ichi2.anki.utils.ext.launchCollectionInLifecycleScope
import timber.log.Timber
import java.lang.IllegalArgumentException

/**
* Page provided by the backend, for a user to add or edit an image occlusion (IO) note
Expand Down Expand Up @@ -154,21 +146,6 @@ class ImageOcclusion :
}
}

// TODO: Move this to an extension method once we have context parameters
private fun <T> Flow<T>.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
this@launchCollectionInLifecycleScope.collect {
if (isRobolectric) {
HandlerUtils.postOnNewHandler { runBlocking { block(it) } }
} else {
block(it)
}
}
}
}
}

companion object {
/**
* @param args arguments for either adding or editing a note
Expand Down
61 changes: 61 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/utils/ext/Flow.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@
*/
package com.ichi2.anki.utils.ext

import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.ichi2.anki.AnkiActivity
import com.ichi2.anki.common.utils.android.isRobolectric
import com.ichi2.utils.HandlerUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun <T> Flow<T>.collectLatestIn(
scope: CoroutineScope,
Expand All @@ -37,3 +46,55 @@ fun <T> Flow<T>.collectIn(
scope.launch {
collect(collector)
}

context(fragment: Fragment)
fun <T> Flow<T>.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) {
fragment.lifecycleScope.launch {
fragment.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
this@launchCollectionInLifecycleScope.collect {
if (isRobolectric) {
HandlerUtils.postOnNewHandler { runBlocking { block(it) } }
} else {
block(it)
}
}
}
}
}

context(activity: AnkiActivity)
fun <T> Flow<T>.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) {
activity.lifecycleScope.launch {
activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
this@launchCollectionInLifecycleScope.collect {
if (isRobolectric) {
// hack: lifecycleScope/runOnUiThread do not handle our
// test dispatcher overriding both IO and Main
// in tests, waitForAsyncTasksToComplete may be required.
HandlerUtils.postOnNewHandler { runBlocking { block(it) } }
} else {
block(it)
}
}
}
}
}

context(activity: AnkiActivity)
fun <T> StateFlow<T>.launchCollectionInLifecycleScope(block: suspend (T) -> Unit) {
activity.lifecycleScope.launch {
var lastValue: T? = null
activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
this@launchCollectionInLifecycleScope.collect {
// on re-resume, an unchanged value will be emitted for a StateFlow
if (lastValue == value) return@collect
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious and want to know what will happen if the app is in background and value is updated (lets say) lastValue check will see the new value when the app resumes, compare it to the new value property, and potentially skip the UI update.

lastValue = value
if (isRobolectric) {
HandlerUtils.postOnNewHandler { runBlocking { block(it) } }
} else {
block(it)
}
}
}
}
}
6 changes: 5 additions & 1 deletion AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,11 @@ class CardBrowserTest : RobolectricTest() {
// Kill and restart the activity and ensure that display order is preserved
val outBundle = Bundle()
cardBrowserController.saveInstanceState(outBundle)
cardBrowserController.pause().stop().destroy()
cardBrowserController.pause().stop()
// fix Robolectric bug with launchCollectionInLifecycleScope
// method running after onStart without context
advanceRobolectricLooper()
cardBrowserController.destroy()
cardBrowserController =
Robolectric
.buildActivity(CardBrowser::class.java)
Expand Down
Loading