forked from ankidroid/Anki-Android
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemention of Deck Picker Widget !Add Deck Picker Widget to display…
… deck statistics This commit introduces the Deck Picker Widget, which displays a list of decks along with the number of cards that are new, in learning, and due for review. It is a display-only widget. Features: - Displays deck names and statistics (new, learning, and review counts). - Updates every minute using a recurring alarm. - Retrieves selected decks from shared preferences. - Handles widget updates and deletions appropriately. This widget provides users with a quick overview of their decks without needing to open the app.
- Loading branch information
Showing
16 changed files
with
1,140 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
251 changes: 251 additions & 0 deletions
251
AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
/* | ||
* 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.widget | ||
|
||
import android.app.AlarmManager | ||
import android.app.PendingIntent | ||
import android.appwidget.AppWidgetManager | ||
import android.appwidget.AppWidgetProvider | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.os.SystemClock | ||
import android.widget.RemoteViews | ||
import com.ichi2.anki.AnkiDroidApp.Companion.applicationScope | ||
import com.ichi2.anki.CollectionManager.withCol | ||
import com.ichi2.anki.R | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.launch | ||
import timber.log.Timber | ||
|
||
/** | ||
* Data class representing the data for a deck displayed in the widget. | ||
* | ||
* @property deckId The ID of the deck. | ||
* @property name The name of the deck. | ||
* @property reviewCount The number of cards due for review. | ||
* @property learnCount The number of cards in the learning phase. | ||
* @property newCount The number of new cards. | ||
*/ | ||
data class DeckPickerWidgetData( | ||
val deckId: Long, | ||
val name: String, | ||
val reviewCount: Int, | ||
val learnCount: Int, | ||
val newCount: Int | ||
) | ||
|
||
/** | ||
* AppWidgetProvider for the Deck Picker Widget. | ||
* This widget displays a list of decks with their respective new, learning, and review card counts. | ||
* It updates every minute . | ||
* It can be resized vertically & horizontally. | ||
* No user actions can be performed from this widget as of now; it is for display purposes only. | ||
*/ | ||
class DeckPickerWidget : AppWidgetProvider() { | ||
|
||
companion object { | ||
const val ACTION_APPWIDGET_UPDATE = AppWidgetManager.ACTION_APPWIDGET_UPDATE | ||
const val ACTION_UPDATE_WIDGET = "com.ichi2.widget.ACTION_UPDATE_WIDGET" | ||
|
||
/** | ||
* Updates the widget with the deck data. | ||
* | ||
* @param context the context of the application | ||
* @param appWidgetManager the app widget manager | ||
* @param widgetId the array of widget IDs | ||
* @param deckIds the array of deck IDs | ||
*/ | ||
fun updateWidget( | ||
context: Context, | ||
appWidgetManager: AppWidgetManager, | ||
widgetId: IntArray, | ||
deckIds: LongArray | ||
) { | ||
val remoteViews = RemoteViews(context.packageName, R.layout.widget_deck_picker_large) | ||
|
||
applicationScope.launch(Dispatchers.Main) { | ||
val deckData = getDeckNameAndStats(deckIds.toList()) | ||
|
||
remoteViews.removeAllViews(R.id.deckCollection) | ||
|
||
for (deck in deckData) { | ||
val deckView = RemoteViews(context.packageName, R.layout.widget_item_deck_main) | ||
|
||
deckView.setTextViewText(R.id.deckName, deck.name) | ||
deckView.setTextViewText(R.id.deckNew, deck.newCount.toString()) | ||
deckView.setTextViewText(R.id.deckDue, deck.reviewCount.toString()) | ||
deckView.setTextViewText(R.id.deckLearn, deck.learnCount.toString()) | ||
|
||
remoteViews.addView(R.id.deckCollection, deckView) | ||
} | ||
appWidgetManager.updateAppWidget(widgetId, remoteViews) | ||
} | ||
} | ||
|
||
/** | ||
* Retrieves the selected deck IDs from shared preferences for the widget. | ||
* | ||
* @param context the context of the application | ||
* @param appWidgetId the ID of the widget | ||
* @return the array of selected deck IDs | ||
*/ | ||
private fun getSelectedDeckIdsFromPreferencesDeckPickerWidget(context: Context, appWidgetId: Int): LongArray { | ||
val sharedPreferences = context.getSharedPreferences("DeckPickerWidgetPrefs", Context.MODE_PRIVATE) | ||
val selectedDecksString = sharedPreferences.getString("deck_picker_widget_selected_decks_$appWidgetId", "") | ||
return if (!selectedDecksString.isNullOrEmpty()) { | ||
selectedDecksString.split(",").map { it.toLong() }.toLongArray() | ||
} else { | ||
longArrayOf() | ||
} | ||
} | ||
|
||
/** | ||
* Sets a recurring alarm to update the widget every minute. | ||
* | ||
* @param context the context of the application | ||
* @param appWidgetId the ID of the widget | ||
*/ | ||
private fun setRecurringAlarmDeckPickerWidget(context: Context, appWidgetId: Int) { | ||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager | ||
val intent = Intent(context, DeckPickerWidget::class.java).apply { | ||
action = ACTION_UPDATE_WIDGET | ||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) | ||
} | ||
val pendingIntent = PendingIntent.getBroadcast(context, appWidgetId, intent, PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE) | ||
|
||
if (pendingIntent == null) { | ||
val newPendingIntent = PendingIntent.getBroadcast(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) | ||
|
||
// Set alarm to trigger every minute | ||
val ONE_MINUTE_MILLIS = 60_000L | ||
alarmManager.setRepeating( | ||
AlarmManager.ELAPSED_REALTIME, | ||
SystemClock.elapsedRealtime() + ONE_MINUTE_MILLIS, | ||
ONE_MINUTE_MILLIS, | ||
newPendingIntent | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* Cancels the recurring alarm for the widget. | ||
* | ||
* @param context the context of the application | ||
* @param appWidgetId the ID of the widget | ||
*/ | ||
private fun cancelRecurringAlarmDeckPickerWidget(context: Context, appWidgetId: Int) { | ||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager | ||
val intent = Intent(context, DeckPickerWidget::class.java).apply { | ||
action = ACTION_UPDATE_WIDGET | ||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) | ||
} | ||
val pendingIntent = PendingIntent.getBroadcast(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) | ||
alarmManager.cancel(pendingIntent) | ||
} | ||
} | ||
|
||
override fun onUpdate( | ||
context: Context, | ||
appWidgetManager: AppWidgetManager, | ||
appWidgetIds: IntArray | ||
) { | ||
super.onUpdate(context, appWidgetManager, appWidgetIds) | ||
Timber.d("onUpdate") | ||
|
||
for (widgetId in appWidgetIds) { | ||
val selectedDeckIds = getSelectedDeckIdsFromPreferencesDeckPickerWidget(context, widgetId) | ||
if (selectedDeckIds.isNotEmpty()) { | ||
updateWidget(context, appWidgetManager, intArrayOf(widgetId), selectedDeckIds) | ||
} | ||
setRecurringAlarmDeckPickerWidget(context, widgetId) | ||
} | ||
} | ||
|
||
override fun onReceive(context: Context?, intent: Intent?) { | ||
if (context == null || intent == null) { | ||
Timber.e("Context or intent is null in onReceive") | ||
return | ||
} | ||
super.onReceive(context, intent) | ||
|
||
when (intent.action) { | ||
ACTION_APPWIDGET_UPDATE -> { | ||
val appWidgetManager = AppWidgetManager.getInstance(context) | ||
val appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS) | ||
val selectedDeckIds = intent.getLongArrayExtra("deck_picker_widget_selected_deck_ids") | ||
|
||
if (appWidgetIds != null && selectedDeckIds != null) { | ||
updateWidget(context, appWidgetManager, appWidgetIds, selectedDeckIds) | ||
} | ||
} | ||
ACTION_UPDATE_WIDGET -> { | ||
val appWidgetManager = AppWidgetManager.getInstance(context) | ||
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) | ||
if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { | ||
val selectedDeckIds = getSelectedDeckIdsFromPreferencesDeckPickerWidget(context, appWidgetId) | ||
if (selectedDeckIds.isNotEmpty()) { | ||
updateWidget(context, appWidgetManager, intArrayOf(appWidgetId), selectedDeckIds) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Triggers the cancel recurring alarm when the widget is deleted. | ||
* | ||
* @param context the context of the application | ||
* @param appWidgetIds the array of widget IDs being deleted | ||
*/ | ||
override fun onDeleted(context: Context?, appWidgetIds: IntArray?) { | ||
super.onDeleted(context, appWidgetIds) | ||
appWidgetIds?.forEach { widgetId -> | ||
cancelRecurringAlarmDeckPickerWidget(context!!, widgetId) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* This function takes a list of deck IDs and returns a list of data objects containing | ||
* the deck name and statistics (review, learn, and new counts) for each specified deck. | ||
* It fetches the entire deck tree, filters it to include only the specified deck IDs, | ||
* and maps the relevant data to the output list, ensuring the order matches the input list. | ||
* Currently used in DeckPickerWidget and CardAnalysisExtraWidget. | ||
* | ||
* @param deckIds the list of deck IDs to retrieve data for | ||
* @return a list of DeckPickerWidgetData objects containing deck names and statistics | ||
*/ | ||
suspend fun getDeckNameAndStats(deckIds: List<Long>): List<DeckPickerWidgetData> { | ||
val result = mutableListOf<DeckPickerWidgetData>() | ||
|
||
val deckTree = withCol { sched.deckDueTree() } | ||
|
||
deckTree.forEach { node -> | ||
if (node.did !in deckIds) return@forEach | ||
result.add( | ||
DeckPickerWidgetData( | ||
deckId = node.did, | ||
name = node.lastDeckNameComponent, | ||
reviewCount = node.revCount, | ||
learnCount = node.lrnCount, | ||
newCount = node.newCount | ||
) | ||
) | ||
} | ||
|
||
val deckIdToData = result.associateBy { it.deckId } | ||
return deckIds.mapNotNull { deckIdToData[it] } | ||
} |
Oops, something went wrong.