Skip to content

Commit

Permalink
Implemention of Deck Picker Widget !
Browse files Browse the repository at this point in the history
  • Loading branch information
xenonnn4w committed Jul 27, 2024
1 parent 73dde8a commit 7c203b2
Show file tree
Hide file tree
Showing 16 changed files with 1,082 additions and 0 deletions.
21 changes: 21 additions & 0 deletions AnkiDroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,27 @@
/>
</receiver>

<receiver
android:name="com.ichi2.widget.DeckPickerWidget"
android:exported="false"
android:enabled="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_deck_picker" />
</receiver>

<activity
android:name="com.ichi2.widget.DeckPickerWidgetConfig"
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
208 changes: 208 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt
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] }
}
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()
}
}
Loading

0 comments on commit 7c203b2

Please sign in to comment.