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 !
- Loading branch information
Showing
16 changed files
with
1,082 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
208 changes: 208 additions & 0 deletions
208
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,208 @@ | ||
/* | ||
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 DeckPickerWidgetData( | ||
val deckId: Long, | ||
val name: String, | ||
val reviewCount: Int, | ||
val learnCount: Int, | ||
val newCount: Int | ||
) | ||
|
||
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 | ||
fun updateWidget( | ||
context: Context, | ||
appWidgetManager: AppWidgetManager, | ||
widgetId: IntArray, | ||
deckIds: LongArray | ||
) { | ||
val remoteViews = RemoteViews(context.packageName, R.layout.widget_deck_picker_large) | ||
|
||
// Launch a coroutine to fetch deck stats | ||
applicationScope.launch(Dispatchers.Main) { | ||
val deckData = getDeckNameAndStats(deckIds.toList()) | ||
|
||
// Clear previous deck views | ||
remoteViews.removeAllViews(R.id.deckCollection) | ||
|
||
// Inflate deck views dynamically based on deckData | ||
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 | ||
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 | ||
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 | ||
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 | ||
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. | ||
*/ | ||
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] } | ||
} |
81 changes: 81 additions & 0 deletions
81
AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidgetAdapter.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,81 @@ | ||
/* | ||
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.view.LayoutInflater | ||
import android.view.View | ||
import android.view.ViewGroup | ||
import android.widget.ImageButton | ||
import android.widget.TextView | ||
import androidx.recyclerview.widget.RecyclerView | ||
import com.ichi2.anki.R | ||
import com.ichi2.anki.dialogs.DeckSelectionDialog.SelectableDeck | ||
|
||
class DeckPickerWidgetAdapter( | ||
val decks: MutableList<SelectableDeck> | ||
) : RecyclerView.Adapter<DeckPickerWidgetAdapter.DeckViewHolder>() { | ||
|
||
class DeckViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { | ||
val deckNameTextView: TextView = itemView.findViewById(R.id.deck_name) | ||
val removeButton: ImageButton = itemView.findViewById(R.id.action_button_remove_deck) | ||
} | ||
|
||
// Creates and inflates the view for each item in the RecyclerView | ||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeckViewHolder { | ||
val view = LayoutInflater.from(parent.context) | ||
.inflate(R.layout.widget_item_deck_config, parent, false) | ||
return DeckViewHolder(view) | ||
} | ||
|
||
// Binds data to each view holder | ||
override fun onBindViewHolder(holder: DeckViewHolder, position: Int) { | ||
val deck = decks[position] | ||
holder.deckNameTextView.text = deck.displayName | ||
holder.removeButton.setOnClickListener { | ||
removeDeck(position) | ||
} | ||
} | ||
|
||
// Returns the total number of items in the RecyclerView | ||
override fun getItemCount(): Int = decks.size | ||
|
||
// Adds a new deck to the list and notifies the adapter | ||
fun addDeck(deck: SelectableDeck) { | ||
decks.add(deck) | ||
notifyItemInserted(decks.size - 1) | ||
} | ||
|
||
// Removes a deck from the list and notifies the adapter | ||
private fun removeDeck(position: Int) { | ||
decks.removeAt(position) | ||
notifyItemRemoved(position) | ||
} | ||
|
||
// Moves a deck within the list and notifies the adapter | ||
fun moveDeck(fromPosition: Int, toPosition: Int) { | ||
val deck = decks.removeAt(fromPosition) | ||
decks.add(toPosition, deck) | ||
notifyItemMoved(fromPosition, toPosition) | ||
} | ||
|
||
// Sets the entire list of decks and notifies the adapter | ||
fun setDecks(newDecks: List<SelectableDeck>) { | ||
decks.clear() | ||
decks.addAll(newDecks) | ||
notifyDataSetChanged() | ||
} | ||
} |
Oops, something went wrong.