For #747. Fix coroutines leaks in `HistoryFragment`.

nightly-build-test
Denys M 5 years ago committed by Jeff Boek
parent b05e9ab14b
commit 13f144f212

@ -18,13 +18,14 @@ fun String.replace(pairs: Map<String, String>): String {
return result return result
} }
fun String?.urlToHost(): String { /**
return try { * Try to parse and get host part if this [String] is valid URL.
val url = URL(this) * Returns **null** otherwise.
url.host */
} catch (e: MalformedURLException) { fun String?.getHostFromUrl(): String? = try {
"" URL(this).host
} } catch (e: MalformedURLException) {
null
} }
fun String?.urlToTrimmedHost(): String { fun String?.urlToTrimmedHost(): String {
@ -40,8 +41,8 @@ fun String?.urlToTrimmedHost(): String {
else -> url.host else -> url.host
} }
} catch (e: MalformedURLException) { } catch (e: MalformedURLException) {
this.urlToHost() this.getHostFromUrl() ?: ""
} catch (e: StringIndexOutOfBoundsException) { } catch (e: StringIndexOutOfBoundsException) {
this.urlToHost() this.getHostFromUrl() ?: ""
} }
} }

@ -8,7 +8,6 @@ import android.content.DialogInterface
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter import android.graphics.PorterDuffColorFilter
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -23,11 +22,12 @@ import androidx.fragment.app.Fragment
import androidx.navigation.Navigation import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_history.view.* import kotlinx.android.synthetic.main.fragment_history.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext
import mozilla.components.concept.storage.VisitType import mozilla.components.concept.storage.VisitType
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
@ -37,6 +37,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getHostFromUrl
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getAutoDisposeObservable
@ -45,17 +46,14 @@ import org.mozilla.fenix.share.ShareTab
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import java.util.concurrent.TimeUnit
@SuppressWarnings("TooManyFunctions") @SuppressWarnings("TooManyFunctions")
class HistoryFragment : Fragment(), CoroutineScope, BackHandler { class HistoryFragment : Fragment(), CoroutineScope by MainScope(), BackHandler {
private lateinit var job: Job
private lateinit var historyComponent: HistoryComponent private lateinit var historyComponent: HistoryComponent
private val navigation by lazy { Navigation.findNavController(requireView()) } private val navigation by lazy { Navigation.findNavController(requireView()) }
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -76,7 +74,6 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
job = Job()
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
@ -95,8 +92,8 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
} }
override fun onDestroy() { override fun onDestroy() {
coroutineContext.cancel()
super.onDestroy() super.onDestroy()
job.cancel()
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -117,9 +114,8 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
launch(Dispatchers.IO) {
reloadData() launch { reloadData() }
}
} }
// This method triggers the complexity warning. However it's actually not that hard to understand. // This method triggers the complexity warning. However it's actually not that hard to understand.
@ -139,10 +135,10 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
is HistoryAction.BackPressed -> getManagedEmitter<HistoryChange>() is HistoryAction.BackPressed -> getManagedEmitter<HistoryChange>()
.onNext(HistoryChange.ExitEditMode) .onNext(HistoryChange.ExitEditMode)
is HistoryAction.Delete.All -> { is HistoryAction.Delete.All -> {
activity?.let { activity?.let { activity ->
AlertDialog.Builder( AlertDialog.Builder(
ContextThemeWrapper( ContextThemeWrapper(
it, activity,
R.style.DeleteDialogStyle R.style.DeleteDialogStyle
) )
).apply { ).apply {
@ -151,7 +147,7 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
dialog.cancel() dialog.cancel()
} }
setPositiveButton(R.string.history_clear_dialog) { dialog: DialogInterface, _ -> setPositiveButton(R.string.history_clear_dialog) { dialog: DialogInterface, _ ->
launch(Dispatchers.IO) { launch {
requireComponents.core.historyStorage.deleteEverything() requireComponents.core.historyStorage.deleteEverything()
reloadData() reloadData()
} }
@ -161,13 +157,14 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
}.show() }.show()
} }
} }
is HistoryAction.Delete.One -> launch(Dispatchers.IO) { is HistoryAction.Delete.One -> launch {
requireComponents.core.historyStorage.deleteVisit(it.item.url, it.item.visitedAt) requireComponents.core.historyStorage.deleteVisit(it.item.url, it.item.visitedAt)
reloadData() reloadData()
} }
is HistoryAction.Delete.Some -> launch(Dispatchers.IO) { is HistoryAction.Delete.Some -> launch {
it.items.forEach { item -> val storage = requireComponents.core.historyStorage
requireComponents.core.historyStorage.deleteVisit(item.url, item.visitedAt) for (item in it.items) {
storage.deleteVisit(item.url, item.visitedAt)
} }
reloadData() reloadData()
} }
@ -199,7 +196,7 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
val components = context?.applicationContext?.components!! val components = context?.applicationContext?.components!!
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected() val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected()
CoroutineScope(Main).launch { GlobalScope.launch(Main) {
deleteSelectedHistory(selectedHistory, components) deleteSelectedHistory(selectedHistory, components)
reloadData() reloadData()
} }
@ -243,33 +240,29 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
VisitType.FRAMED_LINK, VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT VisitType.REDIRECT_PERMANENT
) )
// Until we have proper pagination, only display a limited set of history to avoid blowing up the UI. // Until we have proper pagination, only display a limited set of history to avoid blowing up the UI.
// See https://github.com/mozilla-mobile/fenix/issues/1393 // See https://github.com/mozilla-mobile/fenix/issues/1393
@SuppressWarnings("MagicNumber") val sinceTimeMs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(HISTORY_TIME_DAYS)
val historyCutoffMs = 1000L * 60 * 60 * 24 * 3 // past few days val items = requireComponents.core.historyStorage
val items = requireComponents.core.historyStorage.getDetailedVisits( .getDetailedVisits(sinceTimeMs, excludeTypes = excludeTypes)
System.currentTimeMillis() - historyCutoffMs, excludeTypes = excludeTypes
)
// We potentially have a large amount of visits, and multiple processing steps. // We potentially have a large amount of visits, and multiple processing steps.
// Wrapping iterator in a sequence should make this a little more efficient. // Wrapping iterator in a sequence should make this a little more efficient.
.asSequence() .asSequence()
.sortedByDescending { it.visitTime } .sortedByDescending { it.visitTime }
.mapIndexed { id, item -> .mapIndexed { id, item ->
HistoryItem( val title = item.title
id, if (TextUtils.isEmpty(item.title!!)) try { ?.takeIf(String::isNotEmpty)
URL(item.url).host ?: item.url.getHostFromUrl()
} catch (e: MalformedURLException) { ?: item.url
item.url
} else item.title!!, item.url, item.visitTime HistoryItem(id, title, item.url, item.visitTime)
)
} }
.toList() .toList()
coroutineScope { withContext(Main) {
launch(Dispatchers.Main) { getManagedEmitter<HistoryChange>()
getManagedEmitter<HistoryChange>().onNext(HistoryChange.Change(items)) .onNext(HistoryChange.Change(items))
}
} }
} }
@ -288,3 +281,5 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler {
Navigation.findNavController(view!!).navigate(directions) Navigation.findNavController(view!!).navigate(directions)
} }
} }
private const val HISTORY_TIME_DAYS = 3L

@ -4,7 +4,7 @@
private object Versions { private object Versions {
const val kotlin = "1.3.30" const val kotlin = "1.3.30"
const val coroutines = "1.2.0-alpha-2" const val coroutines = "1.2.1"
const val android_gradle_plugin = "3.3.2" const val android_gradle_plugin = "3.3.2"
const val rxAndroid = "2.1.0" const val rxAndroid = "2.1.0"
const val rxKotlin = "2.3.0" const val rxKotlin = "2.3.0"
@ -65,6 +65,7 @@ object Deps {
const val tools_kotlingradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}" const val tools_kotlingradle = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}"
const val kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}" const val kotlin_stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"
const val kotlin_coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}" const val kotlin_coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
const val kotlin_coroutines_android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}"
const val allopen = "org.jetbrains.kotlin:kotlin-allopen:${Versions.kotlin}" const val allopen = "org.jetbrains.kotlin:kotlin-allopen:${Versions.kotlin}"

Loading…
Cancel
Save