Skip to content
Open
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
38 changes: 18 additions & 20 deletions AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
package com.ichi2.anki

import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.net.Uri
import android.view.WindowManager
import android.view.WindowManager.BadTokenException
Expand All @@ -40,6 +38,7 @@ import com.ichi2.anki.CrashReportData.HelpAction
import com.ichi2.anki.CrashReportData.HelpAction.AnkiBackendLink
import com.ichi2.anki.CrashReportData.HelpAction.OpenDeckOptions
import com.ichi2.anki.common.annotations.UseContextParameter
import com.ichi2.anki.dialogs.CircularProgressDialog
import com.ichi2.anki.exception.StorageAccessException
import com.ichi2.anki.libanki.Collection
import com.ichi2.anki.pages.DeckOptionsDestination
Expand Down Expand Up @@ -410,21 +409,20 @@ suspend fun <T> Fragment.withProgress(
block: suspend () -> T,
): T = requireActivity().withProgress(messageId, block)

@Suppress("Deprecation") // ProgressDialog deprecation
suspend fun <T> withProgressDialog(
context: Activity,
onCancel: (() -> Unit)?,
delayMillis: Long = 600,
@StringRes manualCancelButton: Int? = null,
op: suspend (android.app.ProgressDialog) -> T,
op: suspend (CircularProgressDialog) -> T,
): T =
coroutineScope {
val dialog =
android.app.ProgressDialog(context, R.style.AppCompatProgressDialogStyle).apply {
CircularProgressDialog(context).apply {
setCancelable(onCancel != null)
if (manualCancelButton != null) {
setCancelable(false)
setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(manualCancelButton)) { _, _ ->
setButton(manualCancelButton) { _, _ ->
Timber.i("Progress dialog cancelled via cancel button")
onCancel?.let { it() }
}
Expand Down Expand Up @@ -477,9 +475,9 @@ suspend fun <T> withProgressDialog(
}
}

private fun dismissDialogIfShowing(dialog: Dialog) {
private fun dismissDialogIfShowing(dialog: CircularProgressDialog) {
try {
if (dialog.isShowing) {
if (dialog.isShowing()) {
dialog.dismiss()
}
} catch (e: Exception) {
Expand Down Expand Up @@ -523,18 +521,18 @@ data class ProgressContext(
var amount: Pair<Int, Int>? = null,
)

@Suppress("Deprecation") // ProgressDialog deprecation
private fun ProgressContext.updateDialog(dialog: android.app.ProgressDialog) {
// ideally this would show a progress bar, but MaterialDialog does not support
// setting progress after starting with indeterminate progress, so we just use
// this for now
// this code has since been updated to ProgressDialog, and the above not rechecked
val progressText =
amount?.let {
" ${it.first}/${it.second}"
} ?: ""
@Suppress("Deprecation") // ProgressDialog deprecation
dialog.setMessage(text + progressText)
private fun ProgressContext.updateDialog(dialog: CircularProgressDialog) {
// Update message
dialog.setMessage(text)

// Update progress if we have amount information
amount?.let { (current, total) ->
dialog.setIndeterminate(false)
dialog.setProgress(current, total)
} ?: run {
// No amount info, keep it indeterminate
dialog.setIndeterminate(true)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* Copyright (c) 2025 AnkiDroid Contributors
*
* 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/>.
*/

package com.ichi2.anki.dialogs

import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.ichi2.anki.R

class CircularProgressDialog(
context: Context,
) {
private val dialog: AlertDialog
private val progressIndicator: CircularProgressIndicator
private val percentageText: TextView
private val messageText: TextView

private var isDeterminate = false
private var currentProgress = 0
private var maxProgress = 100

init {
val view = LayoutInflater.from(context).inflate(R.layout.dialog_circular_progress, null)
progressIndicator = view.findViewById(R.id.circular_progress_indicator)
percentageText = view.findViewById(R.id.progress_percentage_text)
messageText = view.findViewById(R.id.progress_message_text)

dialog =
AlertDialog
.Builder(context)
.setView(view)
.setCancelable(false)
.create()
}

fun setMessage(message: String) {
messageText.text = message
messageText.visibility = if (message.isNotEmpty()) View.VISIBLE else View.GONE
updateAccessibility()
}

fun setMessage(
@StringRes messageId: Int,
) {
setMessage(messageText.context.getString(messageId))
}

fun setCancelable(cancelable: Boolean) {
dialog.setCancelable(cancelable)
}

fun setOnCancelListener(listener: DialogInterface.OnCancelListener?) {
dialog.setOnCancelListener(listener)
}

fun setButton(
@StringRes textId: Int,
listener: DialogInterface.OnClickListener,
) {
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, messageText.context.getString(textId), listener)
}

fun setIndeterminate(determinate: Boolean) {
isDeterminate = !determinate
progressIndicator.isIndeterminate = !isDeterminate

if (isDeterminate) {
percentageText.visibility = View.VISIBLE
updateProgress()
} else {
percentageText.visibility = View.GONE
}
updateAccessibility()
}

fun setProgress(progress: Int) {
currentProgress = progress.coerceIn(0, maxProgress)
if (isDeterminate) {
updateProgress()
}
}

fun setMax(max: Int) {
maxProgress = max.coerceAtLeast(1)
if (isDeterminate) {
updateProgress()
}
}

fun setProgress(
current: Int,
total: Int,
) {
setMax(total)
setProgress(current)
}

private fun updateProgress() {
val percentage =
if (maxProgress > 0) {
(currentProgress * 100) / maxProgress
} else {
0
}

progressIndicator.setProgressCompat(percentage, true)
percentageText.text = messageText.context.getString(R.string.progress_percentage, percentage)
updateAccessibility()
}

private fun updateAccessibility() {
val contentDescription =
if (isDeterminate) {
val percentage =
if (maxProgress > 0) {
(currentProgress * 100) / maxProgress
} else {
0
}
messageText.context.getString(
R.string.progress_accessibility_determinate,
messageText.text,
percentage,
)
} else {
messageText.context.getString(
R.string.progress_accessibility_indeterminate,
messageText.text,
)
}
progressIndicator.contentDescription = contentDescription
}

fun show() {
if (!dialog.isShowing) {
dialog.show()
}
}

fun dismiss() {
if (dialog.isShowing) {
dialog.dismiss()
}
}

fun isShowing(): Boolean = dialog.isShowing

companion object {
fun show(
context: Context,
message: String,
): CircularProgressDialog =
CircularProgressDialog(context).apply {
setMessage(message)
setIndeterminate(true)
show()
}

fun show(
context: Context,
@StringRes messageId: Int,
): CircularProgressDialog = show(context, context.getString(messageId))
}
}
52 changes: 52 additions & 0 deletions AnkiDroid/src/main/res/layout/dialog_circular_progress.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="24dp"
android:minWidth="280dp">

<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/circular_progress_indicator"
android:layout_width="64dp"
android:layout_height="64dp"
android:indeterminate="true"
app:indicatorSize="64dp"
app:trackThickness="4dp"
app:indicatorColor="?attr/colorPrimary"
app:trackColor="?attr/colorSurfaceVariant"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:progress="42" />

<TextView
android:id="@+id/progress_percentage_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?attr/colorOnSurface"
android:visibility="gone"
app:layout_constraintTop_toTopOf="@id/circular_progress_indicator"
app:layout_constraintBottom_toBottomOf="@id/circular_progress_indicator"
app:layout_constraintStart_toStartOf="@id/circular_progress_indicator"
app:layout_constraintEnd_toEndOf="@id/circular_progress_indicator"
tools:text="42%"
tools:visibility="visible" />

<TextView
android:id="@+id/progress_message_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="14sp"
android:textColor="?attr/colorOnSurfaceVariant"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/circular_progress_indicator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Processing..." />

</androidx.constraintlayout.widget.ConstraintLayout>
5 changes: 5 additions & 0 deletions AnkiDroid/src/main/res/values/02-strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -437,5 +437,10 @@ opening the system text to speech settings fails">Failed to open text to speech

<!-- Scheduler Upgrade Required Dialog title-->
<string name="scheduler_upgrade_required_dialog_title" comment="Shown in a dialog when user has imported a collection using a very old version of the Anki spaced repetition scheduler">Scheduler upgrade required</string>

<!-- Circular Progress Dialog -->
<string name="progress_percentage" comment="Format string for displaying progress as a percentage. %d is the percentage value">%d%%</string>
<string name="progress_accessibility_determinate" comment="Accessibility description for determinate progress. %1$s is the message, %2$d is the percentage">%1$s. Progress: %2$d percent</string>
<string name="progress_accessibility_indeterminate" comment="Accessibility description for indeterminate progress. %s is the message">%1$s. Progress indicator</string>
</resources>