Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
xenonnn4w committed Aug 14, 2024
1 parent 3c34519 commit 242fd12
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 8 deletions.
40 changes: 33 additions & 7 deletions AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import android.content.Context
import android.content.Intent
import android.os.SystemClock
import android.widget.RemoteViews
import anki.collection.OpChanges
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.CrashReportService
import com.ichi2.anki.R
import com.ichi2.anki.Reviewer
import com.ichi2.anki.analytics.UsageAnalytics
import com.ichi2.libanki.ChangeManager
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
Expand Down Expand Up @@ -60,7 +62,7 @@ data class DeckPickerWidgetData(
* No user actions can be performed from this widget as of now; it is for display purposes only.
* There is only one way to configure the widget i.e. while adding it on home screen,
*/
class DeckPickerWidget : AnalyticsWidgetProvider() {
class DeckPickerWidget : AnalyticsWidgetProvider(), ChangeManager.Subscriber {

companion object {
/**
Expand Down Expand Up @@ -98,7 +100,7 @@ class DeckPickerWidget : AnalyticsWidgetProvider() {
val remoteViews = RemoteViews(context.packageName, R.layout.widget_deck_picker_large)

AnkiDroidApp.applicationScope.launch {
val deckData = getDeckNameAndStats(deckIds.toList().map { it })
val deckData = getDeckNameAndStats(deckIds.toList())

remoteViews.removeAllViews(R.id.deckCollection)

Expand Down Expand Up @@ -213,6 +215,9 @@ class DeckPickerWidget : AnalyticsWidgetProvider() {
}
setRecurringAlarm(context, widgetId)
}

// Subscribe to changes
ChangeManager.subscribe(this)
}

override fun onReceive(context: Context?, intent: Intent?) {
Expand Down Expand Up @@ -258,25 +263,46 @@ class DeckPickerWidget : AnalyticsWidgetProvider() {
}

/**
* Cancels the recurring alarm for the deleted widgets, and the preference values associated to those widgets.
* Updates the `DeckPickerWidget` when changes in the Anki collection affect deck data.
*
* This method is triggered by the `ChangeManager` when operations impact study queues, such as adding or
* removing cards from decks. It updates all relevant widgets with the latest deck information.
*
* @param context the context of the application
* @param appWidgetIds the array of widget IDs being deleted
* @param changes The `OpChanges` object containing details of the operations performed.
* @param handler The object that executed the operation. Updates are ignored if the handler is the current instance.
*/
override fun opExecuted(changes: OpChanges, handler: Any?) {
if (changes.studyQueues && handler !== this) {
val context = AnkiDroidApp.instance.applicationContext
val appWidgetManager = AppWidgetManager.getInstance(context)
val widgetPreferences = WidgetPreferences(context)

val widgetIds = widgetPreferences.getAllWidgetIds()

for (widgetId in widgetIds) {
val selectedDeckIds = widgetPreferences.getSelectedDeckIdsFromPreferencesDeckPickerWidget(widgetId)
if (selectedDeckIds.isNotEmpty()) {
updateWidget(context, appWidgetManager, widgetId, selectedDeckIds)
}
}
}
}

override fun onDeleted(context: Context?, appWidgetIds: IntArray?) {
// Ensure context is not null
if (context == null) {
Timber.e("Context is null in onDeleted")
return
}

val widgetPreferences = WidgetPreferences(context)

// Proceed with deleting widget preferences and canceling alarms
appWidgetIds?.forEach { widgetId ->
cancelRecurringAlarm(context, widgetId)
widgetPreferences.deleteDeckPickerWidgetData(widgetId)
}

// Unsubscribe from changes
ChangeManager.clearSubscribers()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.ichi2.anki.AnkiActivity
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.R
import com.ichi2.anki.dialogs.DeckSelectionDialog
import com.ichi2.anki.dialogs.DeckSelectionDialog.DeckSelectionListener
import com.ichi2.anki.dialogs.DeckSelectionDialog.SelectableDeck
import com.ichi2.anki.dialogs.DiscardChangesDialog
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.libanki.sched.DeckNode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
Expand All @@ -56,6 +58,7 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener {
private lateinit var deckPickerWidgetPreferences: WidgetPreferences
private val MAX_DECKS_ALLOWED = 5 // Maximum number of decks allowed in the widget
private var hasUnsavedChanges = false // Flag to track unsaved changes
private var dueTree: DeckNode? = null

override fun onCreate(savedInstanceState: Bundle?) {
if (showedActivityFailedScreen(savedInstanceState)) {
Expand Down Expand Up @@ -114,13 +117,28 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener {
finish()
}

// Load the due tree
loadDueTree()

findViewById<FloatingActionButton>(R.id.fabWidgetDeckPicker).setOnClickListener {
showDeckSelectionDialog()
lifecycleScope.launch {
val tree = dueTree
if (tree != null) {
// Check if the default deck is the only available deck and there are no cards
val isEmpty = isCollectionEmpty(tree, collectionIsEmpty = true)
if (isEmpty) {
showSnackbar(R.string.no_decks_available_message)
} else {
showDeckSelectionDialog()
}
}
}
}

// Load and display saved preferences
loadSavedPreferences()

// Update the visibility of the "no decks" placeholder and the widget configuration container
updateViewVisibility()

// Register broadcast receiver to handle widget deletion
Expand Down Expand Up @@ -288,6 +306,36 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener {
context?.let { deleteWidgetDataDeckPickerWidget(appWidgetId) }
}
}

/**
* Checks whether the collection is empty based on the structure of the provided deck tree and
* an external flag indicating whether the collection is empty.
*
* This function is specifically implemented to address an issue where the default deck
* (with `did` equal to `1L`) 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.
*
* @param tree The root node of the deck tree, representing the hierarchy of decks.
* @param collectionIsEmpty A flag indicating whether the collection is externally considered empty.
* @return `true` if the collection is empty based on the deck tree and the external flag;
* `false` otherwise.
*/
private fun isCollectionEmpty(tree: DeckNode, collectionIsEmpty: Boolean): Boolean {
val isEmpty = tree.children.size == 1 && tree.children[0].did == 1L && collectionIsEmpty
Timber.d("isEmpty: $isEmpty")

return isEmpty
}

/**
* Loads the due tree asynchronously.
*/
private fun loadDueTree() {
lifecycleScope.launch {
dueTree = withCol { sched.deckDueTree() }
}
}
}

/**
Expand Down
9 changes: 9 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/widget/WidgetPreferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,13 @@ class WidgetPreferences(context: Context) {
putString("deck_picker_widget_selected_decks_$appWidgetId", selectedDecks.joinToString(","))
}
}

fun getAllWidgetIds(): IntArray {
val widgetIdsString = deckPickerSharedPreferences.getString("all_widget_ids", "")
return if (!widgetIdsString.isNullOrEmpty()) {
widgetIdsString.split(",").map { it.toInt() }.toIntArray()
} else {
intArrayOf()
}
}
}
1 change: 1 addition & 0 deletions AnkiDroid/src/main/res/values/08-widget.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<string name="no_selected_deck_placeholder_title" comment="Placeholder title when no decks are selected ">Select decks to display in the widgets</string>
<string name="no_selected_deck_placeholder_description" comment="Description for starting to mark decks ">Select decks with the + icon.</string>
<string name="deck_removed_from_widget" comment="Snackbar when deck is removed from widget " >Deck Removed</string>
<string name="no_decks_available_message" comment="Snackbar when the collection is empty ">No decks available to select</string>"
<string name="keep_editing">Keep Editing</string>
<string name="discard_changes_message">Discard current input?</string>
<plurals name="deck_limit_reached">
Expand Down

0 comments on commit 242fd12

Please sign in to comment.