From 89c805483445df22023e5e674173470a2dcd2684 Mon Sep 17 00:00:00 2001 From: Divyansh Kushwaha Date: Tue, 21 Jun 2022 14:06:55 +0530 Subject: [PATCH 1/2] migrated AnkiStatsTaskHandler.createReviewSummaryStatistics() to suspend function --- AnkiDroid/build.gradle | 1 + .../main/java/com/ichi2/anki/DeckPicker.kt | 13 ++- .../ichi2/anki/stats/AnkiStatsTaskHandler.kt | 102 +++++++----------- .../anki/stats/AnkiStatsTaskHandlerTest.kt | 15 ++- build.gradle | 1 + 5 files changed, 62 insertions(+), 70 deletions(-) diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle index faf5921c16c5..834821288471 100644 --- a/AnkiDroid/build.gradle +++ b/AnkiDroid/build.gradle @@ -324,6 +324,7 @@ dependencies { testImplementation 'androidx.test.ext:junit:1.1.3' testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" testImplementation "io.mockk:mockk:1.12.4" testImplementation 'org.apache.commons:commons-exec:1.3' // obtaining the OS diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index f495b86e2ec6..a0300ae9b467 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -49,6 +49,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -106,6 +107,8 @@ import com.ichi2.ui.BadgeDrawableBuilder import com.ichi2.utils.* import com.ichi2.utils.Permissions.hasStorageAccessPermission import com.ichi2.widget.WidgetStatus +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.launch import timber.log.Timber import java.io.File import kotlin.math.abs @@ -1943,7 +1946,7 @@ open class DeckPicker : NavigationDrawerActivity(), StudyOptionsListener, SyncEr return UpdateDeckListListener(this) } - private class UpdateDeckListListener(deckPicker: DeckPicker?) : TaskListenerWithContext>?>(deckPicker) { + private class UpdateDeckListListener(private val deckPicker: DeckPicker?) : TaskListenerWithContext>?>(deckPicker) { override fun actualOnPreExecute(context: DeckPicker) { if (!context.colIsOpen()) { context.showProgressBar() @@ -1966,7 +1969,13 @@ open class DeckPicker : NavigationDrawerActivity(), StudyOptionsListener, SyncEr context.mDueTree = result.map { x -> x.unsafeCastToType(AbstractDeckTreeNode::class.java) } context.renderPage() // Update the mini statistics bar as well - AnkiStatsTaskHandler.createReviewSummaryStatistics(context.col, context.mReviewSummaryTextView) + deckPicker?.lifecycleScope?.launch( + CoroutineExceptionHandler { _, throwable -> + Timber.w(throwable) + } + ) { + AnkiStatsTaskHandler.createReviewSummaryStatistics(context.col, context.mReviewSummaryTextView) + } Timber.d("Startup - Deck List UI Completed") } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/stats/AnkiStatsTaskHandler.kt b/AnkiDroid/src/main/java/com/ichi2/anki/stats/AnkiStatsTaskHandler.kt index 71c0dcfad7c8..f8e21ed56028 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/stats/AnkiStatsTaskHandler.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/stats/AnkiStatsTaskHandler.kt @@ -16,7 +16,6 @@ package com.ichi2.anki.stats import android.R -import android.util.Pair import android.view.View import android.webkit.WebView import android.widget.ProgressBar @@ -33,10 +32,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import timber.log.Timber -import java.lang.ref.WeakReference import java.net.URLEncoder -import java.util.concurrent.locks.Lock -import java.util.concurrent.locks.ReentrantLock import kotlin.math.roundToInt class AnkiStatsTaskHandler private constructor( @@ -112,64 +108,10 @@ class AnkiStatsTaskHandler private constructor( } } - @Suppress("deprecation") // #7108: AsyncTask - class DeckPreviewStatistics : android.os.AsyncTask?, Void?, String?>() { - private var mTextView: WeakReference? = null - private var mIsRunning = true - override fun doInBackground(vararg params: Pair?): String? { - // make sure only one task of CreateChartTask is running, first to run should get sLock - // only necessary on lower APIs because after honeycomb only one thread is used for all asynctasks - sLock.lock() - return try { - val collection = params[0]!!.first - val textView = params[0]!!.second - mTextView = WeakReference(textView) - if (!mIsRunning || collection == null || collection.dbClosed) { - Timber.d("Quitting DeckPreviewStatistics before execution") - return null - } else { - Timber.d("Starting DeckPreviewStatistics") - } - - // eventually put this in Stats (in desktop it is not though) - var cards: Int - var minutes: Int - val query = "select sum(case when ease > 0 then 1 else 0 end), " + /* cards, excludes rescheduled cards https://github.com/ankidroid/Anki-Android/issues/8592 */ - "sum(time)/1000 from revlog where id > " + (collection.sched.dayCutoff - Stats.SECONDS_PER_DAY) * 1000 - Timber.d("DeckPreviewStatistics query: %s", query) - collection.db - .query(query).use { cur -> - cur.moveToFirst() - cards = cur.getInt(0) - minutes = (cur.getInt(1) / 60.0).roundToInt() - } - val res = textView!!.resources - val span = res.getQuantityString(com.ichi2.anki.R.plurals.in_minutes, minutes, minutes) - res.getQuantityString(com.ichi2.anki.R.plurals.studied_cards_today, cards, cards, span) - } finally { - sLock.unlock() - } - } - - override fun onCancelled() { - mIsRunning = false - } - - override fun onPostExecute(todayStatString: String?) { - val textView = mTextView!!.get() - if (todayStatString != null && mIsRunning && textView != null) { - textView.text = todayStatString - textView.visibility = View.VISIBLE - textView.invalidate() - } - } - } - companion object { @JvmStatic var instance: AnkiStatsTaskHandler? = null private set - private val sLock: Lock = ReentrantLock() private val mutex = Mutex() @JvmStatic @Synchronized @@ -184,12 +126,46 @@ class AnkiStatsTaskHandler private constructor( return instance } - @Suppress("deprecation") // #7108: AsyncTask @JvmStatic - fun createReviewSummaryStatistics(col: Collection, view: TextView): DeckPreviewStatistics { - val deckPreviewStatistics = DeckPreviewStatistics() - deckPreviewStatistics.execute(Pair(col, view)) - return deckPreviewStatistics + suspend fun createReviewSummaryStatistics( + col: Collection, + view: TextView, + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, + defaultDispatcher: CoroutineDispatcher = Dispatchers.Main + ): Unit = mutex.withLock { + withContext(defaultDispatcher) { + val todayStatString = if (!isActive || col.dbClosed) { + Timber.d("Quitting DeckPreviewStatistics before execution") + null + } else { + Timber.d("Starting DeckPreviewStatistics") + // eventually put this in Stats (in desktop it is not though) + var cards: Int + var minutes: Int + /* cards, excludes rescheduled cards https://github.com/ankidroid/Anki-Android/issues/8592 */ + val query = "select sum(case when ease > 0 then 1 else 0 end), " + + "sum(time)/1000 from revlog where id > " + (col.sched.dayCutoff - Stats.SECONDS_PER_DAY) * 1000 + Timber.d("DeckPreviewStatistics query: %s", query) + col.db + .query(query).use { cur -> + cur.moveToFirst() + cards = cur.getInt(0) + minutes = (cur.getInt(1) / 60.0).roundToInt() + } + val res = view.resources + val span = res.getQuantityString(com.ichi2.anki.R.plurals.in_minutes, minutes, minutes) + res.getQuantityString(com.ichi2.anki.R.plurals.studied_cards_today, cards, cards, span) + } + todayStatString?.let { + withContext(mainDispatcher) { + view.apply { + text = it + visibility = View.VISIBLE + invalidate() + } + } + } + } } } } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/stats/AnkiStatsTaskHandlerTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/stats/AnkiStatsTaskHandlerTest.kt index fa81ec3cba5e..be542c2d1549 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/stats/AnkiStatsTaskHandlerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/stats/AnkiStatsTaskHandlerTest.kt @@ -21,6 +21,10 @@ import com.ichi2.anki.RobolectricTest import com.ichi2.anki.stats.AnkiStatsTaskHandler.Companion.createReviewSummaryStatistics import com.ichi2.annotations.NeedsTest import com.ichi2.libanki.Collection +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -30,6 +34,7 @@ import org.mockito.MockitoAnnotations import org.mockito.kotlin.whenever import java.util.concurrent.ExecutionException +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class AnkiStatsTaskHandlerTest : RobolectricTest() { @Mock @@ -38,6 +43,8 @@ class AnkiStatsTaskHandlerTest : RobolectricTest() { @Mock private lateinit var mView: TextView + private val testDispatcher = StandardTestDispatcher() + @Before override fun setUp() { super.setUp() @@ -49,12 +56,10 @@ class AnkiStatsTaskHandlerTest : RobolectricTest() { @Test @Throws(ExecutionException::class, InterruptedException::class) @NeedsTest("explain this test") - @Suppress("deprecation") // #7108: AsyncTask - fun testCreateReviewSummaryStatistics() { + fun testCreateReviewSummaryStatistics() = runTest(testDispatcher) { verify(mCol, atMost(0))!!.db - val result = createReviewSummaryStatistics(mCol, mView) - result.get() - advanceRobolectricLooper() + createReviewSummaryStatistics(mCol, mView, testDispatcher, testDispatcher) + advanceUntilIdle() verify(mCol, atLeast(0))!!.db verify(mCol, atLeast(1))!!.dbClosed } diff --git a/build.gradle b/build.gradle index 20ca93b58e07..b07b1fa3b940 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ buildscript { ext.ankidroid_backend_version = '0.1.11' ext.hamcrest_version = '2.2' ext.junit_version = '5.8.2' + ext.coroutines_version = '1.6.2' repositories { google() From dba8981d405e1f7ba33136f3bf7bf38df67bef75 Mon Sep 17 00:00:00 2001 From: Divyansh Kushwaha Date: Wed, 22 Jun 2022 13:32:35 +0530 Subject: [PATCH 2/2] added extension function launchCatching --- .../main/java/com/ichi2/anki/DeckPicker.kt | 8 +--- .../java/com/ichi2/async/CoroutineHelpers.kt | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/async/CoroutineHelpers.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index a0300ae9b467..a8f442c58f0d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -107,8 +107,6 @@ import com.ichi2.ui.BadgeDrawableBuilder import com.ichi2.utils.* import com.ichi2.utils.Permissions.hasStorageAccessPermission import com.ichi2.widget.WidgetStatus -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.launch import timber.log.Timber import java.io.File import kotlin.math.abs @@ -1969,11 +1967,7 @@ open class DeckPicker : NavigationDrawerActivity(), StudyOptionsListener, SyncEr context.mDueTree = result.map { x -> x.unsafeCastToType(AbstractDeckTreeNode::class.java) } context.renderPage() // Update the mini statistics bar as well - deckPicker?.lifecycleScope?.launch( - CoroutineExceptionHandler { _, throwable -> - Timber.w(throwable) - } - ) { + deckPicker?.lifecycleScope?.launchCatching { AnkiStatsTaskHandler.createReviewSummaryStatistics(context.col, context.mReviewSummaryTextView) } Timber.d("Startup - Deck List UI Completed") diff --git a/AnkiDroid/src/main/java/com/ichi2/async/CoroutineHelpers.kt b/AnkiDroid/src/main/java/com/ichi2/async/CoroutineHelpers.kt new file mode 100644 index 000000000000..172981c794da --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/async/CoroutineHelpers.kt @@ -0,0 +1,37 @@ +/**************************************************************************************** + * Copyright (c) 2022 Divyansh Kushwaha * + * * + * This program 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. * + * * + * This program 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 . * + ****************************************************************************************/ + +/* +* This file contains extension functions for different coroutine related actions. +*/ +package com.ichi2.async + +import androidx.lifecycle.LifecycleCoroutineScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.launch +import timber.log.Timber + +/* + * Launch a job that catches any uncaught errors and prints it to Log. + */ +fun LifecycleCoroutineScope.launchCatching(block: suspend () -> Unit) = + this.launch( + CoroutineExceptionHandler { _, throwable -> + Timber.w(throwable) + } + ) { + block() + }