Replace SingleThreadTaskRunner and AndroidTaskRunner with CoroutineTaskRunner

This commit is contained in:
Alinson S. Xavier 2026-04-10 22:09:14 -05:00
parent 400d543191
commit 61d8f358eb
9 changed files with 82 additions and 137 deletions

View File

@ -1578,17 +1578,6 @@
file="src/main/res/mipmap-anydpi-v26"/>
</issue>
<issue
id="StaticFieldLeak"
message="This `AsyncTask` class should be static or leaks might occur (org.isoron.uhabits.tasks.AndroidTaskRunner.CustomAsyncTask)"
errorLine1=" private inner class CustomAsyncTask(val task: Task) : AsyncTask&lt;Void?, Int?, Void?>() {"
errorLine2=" ~~~~~~~~~~~~~~~">
<location
file="src/main/java/org/isoron/uhabits/tasks/AndroidTaskRunner.kt"
line="57"
column="25"/>
</issue>
<issue
id="VectorPath"
message="Very long vector path (1667 characters), which is bad for performance. Considering reducing precision, removing minor details or rasterizing vector."

View File

@ -19,10 +19,11 @@
package org.isoron.uhabits
import android.content.Context
import kotlinx.coroutines.Dispatchers
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.tasks.SingleThreadTaskRunner
import org.isoron.uhabits.core.tasks.CoroutineTaskRunner
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.inject.AppContext
import org.isoron.uhabits.inject.HabitsApplicationComponent
@ -41,5 +42,8 @@ abstract class HabitsApplicationTestComponent(
@AppScope
@Provides
override fun taskRunner(): TaskRunner = SingleThreadTaskRunner()
override fun taskRunner(): TaskRunner = CoroutineTaskRunner(
mainDispatcher = Dispatchers.Main,
ioDispatcher = Dispatchers.IO
)
}

View File

@ -19,6 +19,7 @@
package org.isoron.uhabits.inject
import android.content.Context
import kotlinx.coroutines.Dispatchers
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
import org.isoron.platform.io.AndroidFileOpener
@ -35,6 +36,7 @@ import org.isoron.uhabits.core.models.sqlite.SQLiteHabitList
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.preferences.WidgetPreferences
import org.isoron.uhabits.core.reminders.ReminderScheduler
import org.isoron.uhabits.core.tasks.CoroutineTaskRunner
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.NotificationTray
import org.isoron.uhabits.core.ui.screens.habits.list.HabitCardListCache
@ -49,7 +51,6 @@ import org.isoron.uhabits.io.AndroidLogging
import org.isoron.uhabits.notifications.AndroidNotificationTray
import org.isoron.uhabits.preferences.SharedPreferencesStorage
import org.isoron.uhabits.receivers.ReminderController
import org.isoron.uhabits.tasks.AndroidTaskRunner
import org.isoron.uhabits.utils.DatabaseUtils
import org.isoron.uhabits.widgets.WidgetUpdater
import java.io.File
@ -137,7 +138,10 @@ abstract class HabitsApplicationComponent(
@AppScope
@Provides
open fun taskRunner(): TaskRunner = AndroidTaskRunner()
open fun taskRunner(): TaskRunner = CoroutineTaskRunner(
mainDispatcher = Dispatchers.Main,
ioDispatcher = Dispatchers.IO
)
@AppScope
@Provides

View File

@ -1,89 +0,0 @@
/*
* Copyright (C) 2016-2025 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.tasks
import android.os.AsyncTask
import kotlinx.coroutines.runBlocking
import org.isoron.uhabits.core.tasks.Task
import org.isoron.uhabits.core.tasks.TaskRunner
import java.util.HashMap
import java.util.LinkedList
class AndroidTaskRunner : TaskRunner {
private val activeTasks: LinkedList<CustomAsyncTask> = LinkedList()
private val taskToAsyncTask: HashMap<Task, CustomAsyncTask> = HashMap()
private val listeners: LinkedList<TaskRunner.Listener> = LinkedList<TaskRunner.Listener>()
override fun addListener(listener: TaskRunner.Listener) {
listeners.add(listener)
}
override fun execute(task: Task) {
task.onAttached(this)
CustomAsyncTask(task).execute()
}
override val activeTaskCount: Int
get() = activeTasks.size
override fun publishProgress(task: Task, progress: Int) {
val asyncTask = taskToAsyncTask[task] ?: return
asyncTask.publish(progress)
}
override fun removeListener(listener: TaskRunner.Listener) {
listeners.remove(listener)
}
private inner class CustomAsyncTask(val task: Task) : AsyncTask<Void?, Int?, Void?>() {
fun publish(progress: Int) {
publishProgress(progress)
}
@Deprecated("Deprecated in Java")
override fun doInBackground(vararg params: Void?): Void? {
if (isCancelled) return null
runBlocking { task.doInBackground() }
return null
}
@Deprecated("Deprecated in Java")
override fun onPostExecute(aVoid: Void?) {
if (isCancelled) return
task.onPostExecute()
activeTasks.remove(this)
taskToAsyncTask.remove(task)
for (l in listeners) l.onTaskFinished(task)
}
@Deprecated("Deprecated in Java")
override fun onPreExecute() {
if (isCancelled) return
for (l in listeners) l.onTaskStarted(task)
activeTasks.add(this)
taskToAsyncTask[task] = this
task.onPreExecute()
}
@Deprecated("Deprecated in Java")
override fun onProgressUpdate(vararg values: Int?) {
values[0]?.let { task.onProgressUpdate(it) }
}
}
}

View File

@ -18,12 +18,14 @@
*/
package org.isoron.uhabits
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.isoron.platform.time.LocalDate
import org.isoron.platform.time.setToday
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.memory.MemoryModelFactory
import org.isoron.uhabits.core.tasks.SingleThreadTaskRunner
import org.isoron.uhabits.core.tasks.CoroutineTaskRunner
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.test.HabitFixtures
import org.junit.After
import org.junit.Before
@ -33,7 +35,7 @@ open class BaseAndroidJVMTest {
private lateinit var habitList: HabitList
protected lateinit var fixtures: HabitFixtures
private lateinit var modelFactory: MemoryModelFactory
private lateinit var taskRunner: SingleThreadTaskRunner
private lateinit var taskRunner: TaskRunner
private lateinit var commandRunner: CommandRunner
@Before
@ -42,7 +44,10 @@ open class BaseAndroidJVMTest {
modelFactory = MemoryModelFactory()
habitList = modelFactory.buildHabitList()
fixtures = HabitFixtures(modelFactory, habitList)
taskRunner = SingleThreadTaskRunner()
taskRunner = CoroutineTaskRunner(
mainDispatcher = UnconfinedTestDispatcher(),
ioDispatcher = UnconfinedTestDispatcher()
)
commandRunner = CommandRunner(taskRunner)
}

View File

@ -18,33 +18,51 @@
*/
package org.isoron.uhabits.core.tasks
import org.isoron.platform.runSuspend
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SingleThreadTaskRunner : TaskRunner {
override val activeTaskCount: Int
get() = 0
class CoroutineTaskRunner(
mainDispatcher: CoroutineDispatcher,
private val ioDispatcher: CoroutineDispatcher
) : TaskRunner {
private val scope = CoroutineScope(SupervisorJob() + mainDispatcher)
private val listeners = mutableListOf<TaskRunner.Listener>()
private var activeCount = 0
override val activeTaskCount: Int get() = activeCount
private val listeners: MutableList<TaskRunner.Listener> = mutableListOf()
override fun addListener(listener: TaskRunner.Listener) {
listeners.add(listener)
}
override fun execute(task: Task) {
for (l in listeners) l.onTaskStarted(task)
if (!task.isCanceled()) {
task.onAttached(this)
task.onPreExecute()
runSuspend { task.doInBackground() }
task.onPostExecute()
}
for (l in listeners) l.onTaskFinished(task)
}
override fun publishProgress(task: Task, progress: Int) {
task.onProgressUpdate(progress)
}
override fun removeListener(listener: TaskRunner.Listener) {
listeners.remove(listener)
}
override fun execute(task: Task) {
task.onAttached(this)
scope.launch {
activeCount++
listeners.forEach { it.onTaskStarted(task) }
task.onPreExecute()
if (!task.isCanceled()) {
withContext(ioDispatcher) {
task.doInBackground()
}
}
task.onPostExecute()
activeCount--
listeners.forEach { it.onTaskFinished(task) }
}
}
override fun publishProgress(task: Task, progress: Int) {
scope.launch {
task.onProgressUpdate(progress)
}
}
}

View File

@ -33,6 +33,7 @@ import org.isoron.uhabits.core.models.NumericalHabitType.AT_MOST
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.tasks.ExportCSVTask
import org.isoron.uhabits.core.tasks.Task
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.ui.callbacks.CheckMarkDialogCallback
import org.isoron.uhabits.core.ui.callbacks.NumberPickerCallback
@ -107,10 +108,14 @@ open class ListHabitsBehavior(
}
open fun onRepairDB() {
taskRunner.execute {
habitList.repair()
screen.showMessage(Message.DATABASE_REPAIRED)
}
taskRunner.execute(object : Task {
override suspend fun doInBackground() {
habitList.repair()
}
override fun onPostExecute() {
screen.showMessage(Message.DATABASE_REPAIRED)
}
})
}
open fun onSendBugReport() {

View File

@ -18,6 +18,7 @@
*/
package org.isoron.uhabits.core
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.isoron.platform.io.Database
import org.isoron.platform.io.DatabaseOpener
import org.isoron.platform.io.FileOpener
@ -31,7 +32,8 @@ import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.memory.MemoryModelFactory
import org.isoron.uhabits.core.tasks.SingleThreadTaskRunner
import org.isoron.uhabits.core.tasks.CoroutineTaskRunner
import org.isoron.uhabits.core.tasks.TaskRunner
import org.isoron.uhabits.core.test.HabitFixtures
import kotlin.test.BeforeTest
@ -39,7 +41,7 @@ open class BaseUnitTest {
protected open lateinit var habitList: HabitList
protected lateinit var fixtures: HabitFixtures
protected lateinit var modelFactory: ModelFactory
protected lateinit var taskRunner: SingleThreadTaskRunner
protected lateinit var taskRunner: TaskRunner
protected open lateinit var commandRunner: CommandRunner
protected val fileOpener: FileOpener = createTestFileOpener()
private var _databaseOpener: DatabaseOpener? = null
@ -57,7 +59,10 @@ open class BaseUnitTest {
habitList = memoryModelFactory.buildHabitList()
fixtures = HabitFixtures(memoryModelFactory, habitList)
modelFactory = memoryModelFactory
taskRunner = SingleThreadTaskRunner()
taskRunner = CoroutineTaskRunner(
mainDispatcher = UnconfinedTestDispatcher(),
ioDispatcher = UnconfinedTestDispatcher()
)
commandRunner = CommandRunner(taskRunner)
}

View File

@ -21,18 +21,22 @@ package org.isoron.uhabits.core.tasks
import dev.mokkery.mock
import dev.mokkery.verify.VerifyMode.Companion.order
import dev.mokkery.verifySuspend
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.isoron.uhabits.core.BaseUnitTest
import kotlin.test.BeforeTest
import kotlin.test.Test
class SingleThreadTaskRunnerTest : BaseUnitTest() {
private lateinit var runner: SingleThreadTaskRunner
class CoroutineTaskRunnerTest : BaseUnitTest() {
private lateinit var runner: CoroutineTaskRunner
private var task: Task = mock()
@BeforeTest
override fun setUp() {
super.setUp()
runner = SingleThreadTaskRunner()
runner = CoroutineTaskRunner(
mainDispatcher = UnconfinedTestDispatcher(),
ioDispatcher = UnconfinedTestDispatcher()
)
}
@Test