Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
Prince-kushwaha committed Sep 12, 2024
2 parents f237453 + a8abb47 commit 564b8a2
Show file tree
Hide file tree
Showing 337 changed files with 2,789 additions and 816 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report_form.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ body:
id: contact
attributes:
label: Checked for duplicates?
description: Quickly search our [open issues](https://github.com/ankidroid/Anki-Android/issues) to see if a similar issue exists
description: Quickly search our [issues](https://github.com/ankidroid/Anki-Android/issues?q=is%3Aissue) to see if a similar issue exists
options:
- label: This issue is not a duplicate
required: true
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/compare_apk_size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ jobs:
timeout-minutes: 5
with:
cache-read-only: true
gradle-home-cache-cleanup: true

- name: Checkout PR
uses: actions/checkout@v4
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ jobs:
# Builds on other branches will only read from main branch cache writes
# Comment this and the with: above out for performance testing on a branch
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
gradle-home-cache-cleanup: true

- name: Warm Gradle Cache
# This makes sure we fetch gradle network resources with a retry
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/opencollective_notices.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
If you are interested in compensation for this work, the process with details is here:
https://github.com/ankidroid/Anki-Android/wiki/OpenCollective-Payment-Process#how-to-get-paid
> [!IMPORTANT]
> PLEASE NOTE: The process was updated in August 2024. Re-read the Payment Process page if you have not already.
We only post one comment per person per month to avoid spamming you, regardless of the number of PRs merged, but this note applies to all PRs merged for this month
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/tests_emulator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ jobs:
# Builds on other branches will only read from main branch cache writes
# Comment this and the with: above out for performance testing on a branch
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
gradle-home-cache-cleanup: true

# This appears to be 'Cache Size: ~1230 MB (1290026823 B)' based on watching action logs
# Repo limit is 10GB; branch caches are independent; branches may read default branch cache.
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/tests_unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,6 @@ jobs:
# Builds on other branches will only read from main branch cache writes
# Comment this and the with: above out for performance testing on a branch
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
# gradle-home-cache-cleanup is temporarily disabled to investigate cache pollution issues
# Requested in: https://github.com/gradle/actions/issues/167#issuecomment-2052352341
# gradle-home-cache-cleanup: true

- name: Gradle Dependency Download
uses: nick-invision/retry@v3
Expand Down
27 changes: 27 additions & 0 deletions AnkiDroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,33 @@
</intent-filter>
</activity>

<!-- A widget that displays a deck name and number of cards to review on the Android home screen.
The way to add it depends on the phone. It usually consists in a long press on the screen, followed by finding a "widget" button"-->
<receiver
android:name="com.ichi2.widget.cardanalysis.CardAnalysisWidget"
android:label="@string/card_analysis_extra_widget_description"
android:exported="false"
>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_provider_card_analysis" />
</receiver>

<!-- Configuration view for the CardAnalysisWidget above.
It is opened when adding a new widget and
by configuration button which appears when the widget is hold or resized.-->
<activity
android:name="com.ichi2.widget.cardanalysis.CardAnalysisWidgetConfig"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>

<receiver android:name="com.ichi2.widget.WidgetPermissionReceiver"
android:exported="true">
<intent-filter>
Expand Down
131 changes: 76 additions & 55 deletions AnkiDroid/src/main/java/com/ichi2/anki/AndroidTtsPlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ import android.os.Bundle
import android.speech.tts.TextToSpeech
import android.speech.tts.TextToSpeech.ERROR
import androidx.annotation.CheckResult
import com.ichi2.anki.AndroidTtsError.TtsErrorCode
import com.ichi2.compat.UtteranceProgressListenerCompat
import com.ichi2.libanki.TTSTag
import com.ichi2.libanki.TtsPlayer
import com.ichi2.libanki.TtsPlayer.TtsCompletionStatus
import com.ichi2.libanki.TtsVoice
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
Expand Down Expand Up @@ -72,7 +72,8 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List<Tt
}

override fun onError(utteranceId: String?, errorCode: Int) {
scope.launch(Dispatchers.IO) { ttsCompletedChannel.send(AndroidTtsError.failure(errorCode)) }
val error = AndroidTtsError.fromErrorCode(errorCode)
scope.launch(Dispatchers.IO) { ttsCompletedChannel.send(TtsCompletionStatus.failure(error)) }
}
})
}
Expand All @@ -86,13 +87,13 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List<Tt
val match = voiceForTag(tag)
if (match == null) {
Timber.w("could not find voice for %s", tag)
return AndroidTtsError.failure(TtsErrorCode.APP_MISSING_VOICE)
return TtsCompletionStatus.failure(AndroidTtsError.MissingVoiceError(tag))
}

val voice = match.voice
if (voice !is AndroidTtsVoice) {
Timber.w("Invalid voice for %s", tag)
return AndroidTtsError.failure(TtsErrorCode.APP_INVALID_VOICE)
return TtsCompletionStatus.failure(AndroidTtsError.InvalidVoiceError)
}

return play(tag, voice).also { result ->
Expand All @@ -106,12 +107,12 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List<Tt
it.voice = voice.voice
tag.speed?.let { speed ->
if (it.setSpeechRate(speed) == ERROR) {
return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.failure(TtsErrorCode.APP_SPEECH_RATE_FAILED))
return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.SpeechRateFailed)
}
}
// if it's already playing: stop it
it.stopPlaying()
} ?: return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.failure(TtsErrorCode.APP_TTS_INIT_FAILED))
} ?: return@suspendCancellableCoroutine continuation.resume(AndroidTtsError.InitFailed)

Timber.d("tts text '%s' to be played for locale (%s)", tag.fieldText, tag.lang)
continuation.ensureActive()
Expand Down Expand Up @@ -163,56 +164,72 @@ class AndroidTtsPlayer(private val context: Context, private val voices: List<Tt
}
}

class AndroidTtsError(@Suppress("unused") val errorCode: TtsErrorCode) : TtsPlayer.TtsError() {
enum class TtsErrorCode(var code: Int) {
ERROR(TextToSpeech.ERROR),
ERROR_SYNTHESIS(TextToSpeech.ERROR_SYNTHESIS),
ERROR_INVALID_REQUEST(TextToSpeech.ERROR_INVALID_REQUEST),
ERROR_NETWORK(TextToSpeech.ERROR_NETWORK),
ERROR_NETWORK_TIMEOUT(TextToSpeech.ERROR_NETWORK_TIMEOUT),
ERROR_NOT_INSTALLED_YET(TextToSpeech.ERROR_NOT_INSTALLED_YET),
ERROR_OUTPUT(TextToSpeech.ERROR_OUTPUT),
ERROR_SERVICE(TextToSpeech.ERROR_SERVICE),
APP_UNKNOWN(0),
APP_MISSING_VOICE(2),
APP_INVALID_VOICE(3),
APP_SPEECH_RATE_FAILED(4),
APP_TTS_INIT_FAILED(5),
APP_TTS_INIT_TIMEOUT(6)
;

/** A string which google will relate to the TTS Engine in most cases */
val developerString: String
get() =
when (this) {
ERROR -> "ERROR"
ERROR_SYNTHESIS -> "ERROR_SYNTHESIS"
ERROR_INVALID_REQUEST -> "ERROR_INVALID_REQUEST"
ERROR_NETWORK -> "ERROR_NETWORK"
ERROR_NETWORK_TIMEOUT -> "ERROR_NETWORK_TIMEOUT"
ERROR_NOT_INSTALLED_YET -> "ERROR_NOT_INSTALLED_YET"
ERROR_OUTPUT -> "ERROR_OUTPUT"
ERROR_SERVICE -> "ERROR_SERVICE"
APP_MISSING_VOICE -> "APP_MISSING_VOICE"
APP_INVALID_VOICE -> "APP_INVALID_VOICE"
APP_SPEECH_RATE_FAILED -> "APP_SPEECH_RATE_FAILED"
APP_TTS_INIT_FAILED -> "APP_TTS_INIT_FAILED"
APP_TTS_INIT_TIMEOUT -> "APP_TTS_INIT_TIMEOUT"
APP_UNKNOWN -> "APP_UNKNOWN"
}

companion object {
fun fromErrorCode(errorCode: Int): TtsErrorCode =
entries.firstOrNull { it.code == errorCode } ?: APP_UNKNOWN
}
}
sealed class AndroidTtsError : TtsPlayer.TtsError() {
// Ankidroid specific errors
data object UnknownError : AndroidTtsError()
data class MissingVoiceError(val tag: TTSTag) : AndroidTtsError()
data object InvalidVoiceError : AndroidTtsError()
data object SpeechRateFailed : AndroidTtsError()
data object InitFailed : AndroidTtsError()
data object InitTimeout : AndroidTtsError()

// Android Errors
/** @see TextToSpeech.ERROR */
private data object AndroidGenericError : AndroidTtsError()

/** @see TextToSpeech.ERROR_SYNTHESIS */
private data object AndroidSynthesisError : AndroidTtsError()

/** @see TextToSpeech.ERROR_INVALID_REQUEST */
private data object AndroidInvalidRequest : AndroidTtsError()

/** @see TextToSpeech.ERROR_NETWORK */
private data object AndroidNetworkError : AndroidTtsError()

/** @see TextToSpeech.ERROR_NETWORK_TIMEOUT */
private data object AndroidNetworkTimeoutError : AndroidTtsError()

/** @see TextToSpeech.ERROR_NOT_INSTALLED_YET */
private data object AndroidNotInstalledYet : AndroidTtsError()

/** @see TextToSpeech.ERROR_OUTPUT */
private data object AndroidOutputError : AndroidTtsError()

/** @see TextToSpeech.ERROR_SERVICE */
private data object AndroidServiceError : AndroidTtsError()

/** A string which google will relate to the TTS Engine in most cases */
val developerString: String
get() =
when (this) {
is AndroidGenericError -> "ERROR"
is AndroidSynthesisError -> "ERROR_SYNTHESIS"
is AndroidInvalidRequest -> "ERROR_INVALID_REQUEST"
is AndroidNetworkError -> "ERROR_NETWORK_ERROR"
is AndroidNetworkTimeoutError -> "ERROR_NETWORK_TIMEOUT"
is AndroidNotInstalledYet -> "ERROR_NOT_INSTALLED_YET"
is AndroidOutputError -> "ERROR_OUTPUT"
is AndroidServiceError -> "ERROR_SERVICE"
is MissingVoiceError -> "APP_MISSING_VOICE"
is InvalidVoiceError -> "APP_INVALID_VOICE"
is SpeechRateFailed -> "APP_SPEECH_RATE_FAILED"
is InitFailed -> "APP_TTS_INIT_FAILED"
is InitTimeout -> "APP_TTS_INIT_TIMEOUT"
is UnknownError -> "APP_UNKNOWN"
}

companion object {
fun failure(errorCode: TtsErrorCode): TtsCompletionStatus =
TtsCompletionStatus.failure(AndroidTtsError(errorCode))

fun failure(errorCode: Int): TtsCompletionStatus =
failure(TtsErrorCode.fromErrorCode(errorCode))
fun fromErrorCode(errorCode: Int): AndroidTtsError =
when (errorCode) {
ERROR -> AndroidGenericError
TextToSpeech.ERROR_SYNTHESIS -> AndroidSynthesisError
TextToSpeech.ERROR_INVALID_REQUEST -> AndroidInvalidRequest
TextToSpeech.ERROR_NETWORK -> AndroidNetworkError
TextToSpeech.ERROR_OUTPUT -> AndroidOutputError
TextToSpeech.ERROR_NOT_INSTALLED_YET -> AndroidNotInstalledYet
TextToSpeech.ERROR_SERVICE -> AndroidServiceError
else -> UnknownError
}
}
}

Expand All @@ -221,7 +238,11 @@ fun TtsPlayer.TtsError.localizedErrorMessage(context: Context): String =
// TODO: Do we want a human readable string here as well - snackbar has limited room
// but developerString is currently not translated as it returns
// developerString: ERROR_NETWORK_TIMEOUT, so "Audio error (ERROR_NETWORK_TIMEOUT)"
context.getString(R.string.tts_voices_playback_error_new, this.errorCode.developerString)
context.getString(R.string.tts_voices_playback_error_new, this.developerString)
} else {
this.toString()
}

private fun CancellableContinuation<TtsCompletionStatus>.resume(error: AndroidTtsError) {
resume(TtsCompletionStatus.failure(error))
}
2 changes: 2 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import com.ichi2.utils.ExceptionUtil
import com.ichi2.utils.KotlinCleanup
import com.ichi2.utils.LanguageUtil
import com.ichi2.utils.Permissions
import com.ichi2.widget.cardanalysis.CardAnalysisWidget
import com.ichi2.widget.deckpicker.DeckPickerWidget
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -298,6 +299,7 @@ open class AnkiDroidApp : Application(), Configuration.Provider, ChangeManager.S
Timber.d("ChangeSubscriber - opExecuted called with changes: $changes")
if (changes.studyQueues) {
DeckPickerWidget.updateDeckPickerWidgets(this)
CardAnalysisWidget.updateCardAnalysisWidgets(this)
} else {
Timber.d("No relevant changes to update the widget")
}
Expand Down
5 changes: 5 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,11 @@ open class DeckPicker :
showDatabaseErrorDialog(DatabaseErrorDialogType.DIALOG_CONFIRM_RESTORE_BACKUP)
return true
}
R.id.action_export_collection -> {
Timber.i("DeckPicker:: Export menu item selected")
ExportDialogFragment.newInstance().show(supportFragmentManager, "exportDialog")
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class DeckSpinnerSelection(
* Displays a [DeckSelectionDialog]
*/
suspend fun displayDeckSelectionDialog() {
val decks = fromCollection(includeFiltered = false).toMutableList()
val decks = fromCollection(includeFiltered = showFilteredDecks).toMutableList()
if (showAllDecks) {
decks.add(SelectableDeck(ALL_DECKS_ID, context.resources.getString(R.string.card_browser_all_decks)))
}
Expand Down
59 changes: 59 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2024 Anoop <[email protected]>
*
* 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

import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.libanki.Collection
import com.ichi2.libanki.Consts

/**
* Checks if a given deck, including its subdecks if specified, is empty.
*
* @param deckId The ID of the deck to check.
* @param includeSubdecks If true, includes subdecks in the check. Default is true.
* @return `true` if the deck (and subdecks if specified) is empty, otherwise `false`.
*/
private fun Collection.isDeckEmpty(deckId: Long, includeSubdecks: Boolean = true): Boolean {
val deckIds = decks.deckAndChildIds(deckId)
val totalCardCount = decks.cardCount(*deckIds.toLongArray(), includeSubdecks = includeSubdecks)
return totalCardCount == 0
}

/**
* Checks if the default deck is empty.
*
* This method runs on an IO thread and accesses the collection to determine if the default deck (with ID 1) is empty.
*
* @return `true` if the default deck is empty, otherwise `false`.
*/
suspend fun isDefaultDeckEmpty(): Boolean = withCol { isDeckEmpty(Consts.DEFAULT_DECK_ID) }

/**
* Returns whether the deck picker displays any deck.
* Technically, it means that there is a non-default deck, or that the default deck is non-empty.
*
* This function is specifically implemented to address an issue where the default deck
* isn't handled correctly when a second deck is added to the
* collection. In this case, the deck tree may incorrectly appear as non-empty when it contains
* only the default deck and no other cards.
*
*/
suspend fun isCollectionEmpty(): Boolean {
val tree = withCol { sched.deckDueTree() }
val onlyDefaultDeckAvailable = tree.children.singleOrNull()?.did == Consts.DEFAULT_DECK_ID
return onlyDefaultDeckAvailable && isDefaultDeckEmpty()
}
12 changes: 12 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/TtsVoices.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ object TtsVoices {
/** A job which populates [availableLocaleData] */
private var buildLocalesJob: Job? = null

/**
* The package name of the default speech synthesis engine.
*
* @return Package name of the Tts engine that the user has chosen as their default.
* 'null' if the system has no engine or if an error occurs
*
* @see TextToSpeech.getDefaultEngine
*/
var ttsEngine: String? = null
private set

/**
* Returns the list of available locales for use in TTS
*
Expand Down Expand Up @@ -188,6 +199,7 @@ object TtsVoices {
textToSpeech = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) {
Timber.v("TTS creation success")
ttsEngine = textToSpeech?.defaultEngine
continuation.resume(textToSpeech)
} else {
Timber.e("TTS creation failed. status: %d", status)
Expand Down
Loading

0 comments on commit 564b8a2

Please sign in to comment.