Skip to content

Commit 9ac8ea1

Browse files
Feat (Progress Dialog): Compat to add new progress bar
1 parent cdba22b commit 9ac8ea1

File tree

3 files changed

+210
-7
lines changed

3 files changed

+210
-7
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import com.ichi2.anki.CrashReportData.HelpAction
4040
import com.ichi2.anki.CrashReportData.HelpAction.AnkiBackendLink
4141
import com.ichi2.anki.CrashReportData.HelpAction.OpenDeckOptions
4242
import com.ichi2.anki.common.annotations.UseContextParameter
43+
import com.ichi2.anki.dialogs.ProgressCompat
4344
import com.ichi2.anki.exception.StorageAccessException
4445
import com.ichi2.anki.libanki.Collection
4546
import com.ichi2.anki.pages.DeckOptionsDestination
@@ -417,17 +418,16 @@ suspend fun <T> Fragment.withProgress(
417418
block: suspend () -> T,
418419
): T = requireActivity().withProgress(messageId, block)
419420

420-
@Suppress("Deprecation") // ProgressDialog deprecation
421421
suspend fun <T> withProgressDialog(
422422
context: Activity,
423423
onCancel: (() -> Unit)?,
424424
delayMillis: Long = 600,
425425
@StringRes manualCancelButton: Int? = null,
426-
op: suspend (android.app.ProgressDialog) -> T,
426+
op: suspend (ProgressCompat) -> T,
427427
): T =
428428
coroutineScope {
429429
val dialog =
430-
android.app.ProgressDialog(context, R.style.AppCompatProgressDialogStyle).apply {
430+
ProgressCompat(context).apply {
431431
setCancelable(onCancel != null)
432432
if (manualCancelButton != null) {
433433
setCancelable(false)
@@ -436,10 +436,10 @@ suspend fun <T> withProgressDialog(
436436
onCancel?.let { it() }
437437
}
438438
} else {
439-
onCancel?.let {
439+
onCancel?.let { cancelAction ->
440440
setOnCancelListener {
441441
Timber.i("Progress dialog cancelled via cancel listener")
442-
it()
442+
cancelAction()
443443
}
444444
}
445445
}
@@ -478,7 +478,7 @@ suspend fun <T> withProgressDialog(
478478
op(dialog)
479479
} finally {
480480
dialogJob.cancel()
481-
dismissDialogIfShowing(dialog)
481+
dialog.dismiss()
482482
context.runOnUiThread { context.window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) }
483483
if (dialogIsOurs) {
484484
AnkiDroidApp.instance.progressDialogShown = false
@@ -533,7 +533,7 @@ data class ProgressContext(
533533
)
534534

535535
@Suppress("Deprecation") // ProgressDialog deprecation
536-
private fun ProgressContext.updateDialog(dialog: android.app.ProgressDialog) {
536+
private fun ProgressContext.updateDialog(dialog: ProgressCompat) {
537537
// ideally this would show a progress bar, but MaterialDialog does not support
538538
// setting progress after starting with indeterminate progress, so we just use
539539
// this for now
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright (c) 2025 Shaan Narendran <shaannaren06@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki.dialogs
18+
19+
import android.content.Context
20+
import android.content.DialogInterface
21+
import android.os.Handler
22+
import android.os.Looper
23+
import android.view.LayoutInflater
24+
import android.widget.TextView
25+
import androidx.appcompat.app.AlertDialog
26+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
27+
import com.google.android.material.progressindicator.CircularProgressIndicator
28+
import com.ichi2.anki.R
29+
30+
/**
31+
* Replacement for [android.app.ProgressDialog] deprecation on API 26+. This class should be used
32+
* instead of the platform [android.app.ProgressDialog].
33+
*
34+
* @see android.app.ProgressDialog
35+
*/
36+
class ProgressCompat(
37+
private val context: Context,
38+
) {
39+
companion object {
40+
private val PROGRESS_PATTERN = Regex("(\\d+)\\s*/\\s*(\\d+)")
41+
}
42+
43+
private var dialog: AlertDialog? = null
44+
private var circularIndicator: CircularProgressIndicator? = null
45+
private var messageView: TextView? = null
46+
private var pendingMessage: CharSequence? = null
47+
private var negativeButtonAction: Pair<String, (DialogInterface, Int) -> Unit>? = null
48+
private var onCancelListener: DialogInterface.OnCancelListener? = null
49+
private var isCancelable = false
50+
private var max = 100
51+
private var progress = 0
52+
53+
private val handler by lazy { Handler(Looper.getMainLooper()) }
54+
55+
private inline fun runOnUi(crossinline action: () -> Unit) {
56+
if (Looper.myLooper() == Looper.getMainLooper()) action() else handler.post { action() }
57+
}
58+
59+
fun setCancelable(flag: Boolean) {
60+
isCancelable = flag
61+
}
62+
63+
fun setOnCancelListener(listener: DialogInterface.OnCancelListener?) {
64+
onCancelListener = listener
65+
}
66+
67+
fun setButton(
68+
whichButton: Int,
69+
text: CharSequence,
70+
listener: (DialogInterface, Int) -> Unit,
71+
) {
72+
if (whichButton == DialogInterface.BUTTON_NEGATIVE) {
73+
negativeButtonAction = text.toString() to listener
74+
}
75+
}
76+
77+
/**
78+
* Updates the message text.
79+
*
80+
* This method attempts to parse progress numbers from the [message] string.
81+
* If the message contains a pattern like "50 / 100", the progress bar will
82+
* automatically update to match those values.
83+
*
84+
* @param message The text to display, or null to clear.
85+
*/
86+
fun setMessage(message: CharSequence?) =
87+
runOnUi {
88+
pendingMessage = message
89+
messageView?.text = message
90+
91+
message?.let { msg ->
92+
PROGRESS_PATTERN.find(msg)?.destructured?.let { (curr, total) ->
93+
runCatching {
94+
val t = total.toInt()
95+
// FIX: Changed _max to max
96+
if (max != t) setMaxInternal(t)
97+
setProgressInternal(curr.toInt())
98+
}
99+
return@runOnUi
100+
}
101+
// If no digits found, ensure we are in indeterminate mode (spinner)
102+
if (circularIndicator?.isIndeterminate == false && progress == 0) {
103+
circularIndicator?.isIndeterminate = true
104+
}
105+
}
106+
}
107+
108+
fun setMax(max: Int) = runOnUi { setMaxInternal(max) }
109+
110+
private fun setMaxInternal(max: Int) {
111+
this.max = max
112+
circularIndicator?.max = max
113+
}
114+
115+
fun setProgress(value: Int) = runOnUi { setProgressInternal(value) }
116+
117+
private fun setProgressInternal(value: Int) {
118+
progress = value
119+
circularIndicator?.apply {
120+
if (isIndeterminate) isIndeterminate = false
121+
setProgressCompat(value, true)
122+
}
123+
}
124+
125+
fun incrementProgressBy(diff: Int) = runOnUi { setProgressInternal(progress + diff) }
126+
127+
var isIndeterminate: Boolean
128+
get() = circularIndicator?.isIndeterminate ?: false
129+
set(value) = runOnUi { circularIndicator?.isIndeterminate = value }
130+
131+
fun show() =
132+
runOnUi {
133+
if (dialog?.isShowing == true) return@runOnUi
134+
135+
val view = LayoutInflater.from(context).inflate(R.layout.dialog_circular_progress, null)
136+
circularIndicator =
137+
view.findViewById<CircularProgressIndicator>(R.id.circular_progress).apply {
138+
isIndeterminate = false
139+
// FIX: Explicitly reference the class member using 'this@ProgressCompat.max'
140+
max = this@ProgressCompat.max
141+
setProgressCompat(progress, false)
142+
}
143+
messageView = view.findViewById<TextView>(R.id.progress_message).apply { text = pendingMessage }
144+
145+
dialog =
146+
MaterialAlertDialogBuilder(context)
147+
.setView(view)
148+
.setCancelable(isCancelable)
149+
.setOnCancelListener(onCancelListener)
150+
.apply {
151+
negativeButtonAction?.let { (text, listener) -> setNegativeButton(text, listener) }
152+
}.create()
153+
154+
dialog?.show()
155+
}
156+
157+
fun dismiss() =
158+
runOnUi {
159+
runCatching { dialog?.dismiss() }
160+
}
161+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Copyright (c) 2025 Shaan Narendran <shaannaren06@gmail.com>
4+
~
5+
~ This program is free software; you can redistribute it and/or modify it under
6+
~ the terms of the GNU General Public License as published by the Free Software
7+
~ Foundation; either version 3 of the License, or (at your option) any later
8+
~ version.
9+
~
10+
~ This program is distributed in the hope that it will be useful, but WITHOUT ANY
11+
~ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
12+
~ PARTICULAR PURPOSE. See the GNU General Public License for more details.
13+
~
14+
~ You should have received a copy of the GNU General Public License along with
15+
~ this program. If not, see <http://www.gnu.org/licenses/>.
16+
-->
17+
18+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
19+
xmlns:app="http://schemas.android.com/apk/res-auto"
20+
android:layout_width="match_parent"
21+
android:layout_height="wrap_content"
22+
android:orientation="horizontal"
23+
android:padding="24dp"
24+
android:gravity="center_vertical">
25+
26+
<com.google.android.material.progressindicator.CircularProgressIndicator
27+
android:id="@+id/circular_progress"
28+
android:layout_width="wrap_content"
29+
android:layout_height="wrap_content"
30+
android:layout_marginEnd="24dp"
31+
app:indicatorSize="40dp"
32+
app:trackThickness="4dp"
33+
android:max="100" />
34+
35+
<TextView
36+
android:id="@+id/progress_message"
37+
android:layout_width="wrap_content"
38+
android:layout_height="wrap_content"
39+
android:text="@string/dialog_processing"
40+
android:textAppearance="?attr/textAppearanceBody1" />
41+
42+
</LinearLayout>

0 commit comments

Comments
 (0)