Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GSoC] migrated AnkiStatsTaskHandler.createReviewSummaryStatistics() to suspend function #11667

Merged
merged 2 commits into from
Jun 22, 2022
Merged
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
1 change: 1 addition & 0 deletions AnkiDroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1943,7 +1944,7 @@ open class DeckPicker : NavigationDrawerActivity(), StudyOptionsListener, SyncEr
return UpdateDeckListListener(this)
}

private class UpdateDeckListListener<T : AbstractDeckTreeNode>(deckPicker: DeckPicker?) : TaskListenerWithContext<DeckPicker, Void, List<TreeNode<T>>?>(deckPicker) {
private class UpdateDeckListListener<T : AbstractDeckTreeNode>(private val deckPicker: DeckPicker?) : TaskListenerWithContext<DeckPicker, Void, List<TreeNode<T>>?>(deckPicker) {
override fun actualOnPreExecute(context: DeckPicker) {
if (!context.colIsOpen()) {
context.showProgressBar()
Expand All @@ -1966,7 +1967,9 @@ 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?.launchCatching {
AnkiStatsTaskHandler.createReviewSummaryStatistics(context.col, context.mReviewSummaryTextView)
}
Timber.d("Startup - Deck List UI Completed")
}
}
Expand Down
102 changes: 39 additions & 63 deletions AnkiDroid/src/main/java/com/ichi2/anki/stats/AnkiStatsTaskHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -112,64 +108,10 @@ class AnkiStatsTaskHandler private constructor(
}
}

@Suppress("deprecation") // #7108: AsyncTask
class DeckPreviewStatistics : android.os.AsyncTask<Pair<Collection, TextView?>?, Void?, String?>() {
private var mTextView: WeakReference<TextView>? = null
private var mIsRunning = true
override fun doInBackground(vararg params: Pair<Collection, TextView?>?): 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
Expand All @@ -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()
}
}
}
}
}
}
}
37 changes: 37 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/async/CoroutineHelpers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/****************************************************************************************
* Copyright (c) 2022 Divyansh Kushwaha <kushwaha.divyansh.dxn@gmail.com> *
* *
* 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 <http://www.gnu.org/licenses/>. *
****************************************************************************************/

/*
* 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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -38,6 +43,8 @@ class AnkiStatsTaskHandlerTest : RobolectricTest() {
@Mock
private lateinit var mView: TextView

private val testDispatcher = StandardTestDispatcher()

@Before
override fun setUp() {
super.setUp()
Expand All @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down