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 8ceebcc commit e81e843
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 51 deletions.
2 changes: 1 addition & 1 deletion AnkiDroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@
/>
</receiver>

<!-- This widget displays a few deck and deck information on the Android home screen.
<!-- A widget that displays a few decks's 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.DeckPickerWidget"
Expand Down
116 changes: 82 additions & 34 deletions AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ data class DeckPickerWidgetData(
* 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.
* It allows user to open the reviewer directly by clicking on the deck same as deckpicker.
* There is only one way to configure the widget i.e. while adding it on home screen,
*/
class DeckPickerWidget : AnalyticsWidgetProvider(), ChangeManager.Subscriber {
Expand All @@ -77,6 +77,11 @@ class DeckPickerWidget : AnalyticsWidgetProvider(), ChangeManager.Subscriber {
*/
const val ACTION_UPDATE_WIDGET = "com.ichi2.widget.ACTION_UPDATE_WIDGET"

/**
* Key used for passing the selected deck IDs in the intent extras.
*/
const val EXTRA_SELECTED_DECK_IDS = "deck_picker_widget_selected_deck_ids"

/**
* Updates the widget with the deck data.
*
Expand Down Expand Up @@ -132,46 +137,77 @@ class DeckPickerWidget : AnalyticsWidgetProvider(), ChangeManager.Subscriber {
}

/**
* Sets a recurring alarm to update the widget every minute, if necessary.
*
* If the alarm is already set for the widget, this method does nothing.
* This ensures that multiple alarms are not created for the same widget,
* preventing potential performance issues or unexpected behavior.
* Retrieves or creates a PendingIntent for the widget.
*
* @param context the context of the application
* @param appWidgetId the ID of the widget
* @param create whether to create a new PendingIntent or just retrieve it
* @return the PendingIntent for the widget
*/
private fun setRecurringAlarm(context: Context, appWidgetId: AppWidgetId) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
private fun getPendingIntent(
context: Context,
appWidgetId: AppWidgetId,
create: Boolean
): PendingIntent? {
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)
return PendingIntent.getBroadcast(
context,
appWidgetId,
intent,
if (create) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE
}
)
}

/**
* When onUpdate is called, the code checks if an existing alarm PendingIntent
* is already set for the widget. If an alarm already exists, PendingIntent.getBroadcast
* returns the existing PendingIntent, and pendingIntent is not null.
* In this case, the method returns early and no new alarm is set.
*/
/**
* Provides the AlarmManager instance.
*
* @param context the context of the application
* @return the AlarmManager instance
*/
private fun alarmManager(context: Context): AlarmManager {
return context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
}

/**
* Ensure a recurring alarm is set to update the widget every minute.
*
* If the alarm is already set for the widget, this method does nothing.
* This ensures that multiple alarms are not created for the same widget,
* preventing potential performance issues or unexpected behavior.
*
* @param context the context of the application
* @param appWidgetId the ID of the widget
*/
private fun setRecurringAlarm(context: Context, appWidgetId: AppWidgetId) {
val pendingIntent = getPendingIntent(context, appWidgetId, create = false)

if (pendingIntent != null) {
Timber.v("Recurring alarm PendingIntent already exists for widget ID: $appWidgetId")
return
}

Timber.v("Creating a new recurring alarm PendingIntent for widget ID: $appWidgetId")
val newPendingIntent = PendingIntent.getBroadcast(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)

val alarmManager = alarmManager(context)
val newPendingIntent = getPendingIntent(context, appWidgetId, create = true)

// Set alarm to trigger every minute
val ONE_MINUTE_MILLIS = 60.seconds.inWholeMilliseconds
alarmManager.setRepeating(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + ONE_MINUTE_MILLIS,
ONE_MINUTE_MILLIS,
newPendingIntent
)
if (newPendingIntent != null) {
alarmManager.setRepeating(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + ONE_MINUTE_MILLIS,
ONE_MINUTE_MILLIS,
newPendingIntent
)
}
}

/**
Expand All @@ -181,14 +217,12 @@ class DeckPickerWidget : AnalyticsWidgetProvider(), ChangeManager.Subscriber {
* @param appWidgetId the ID of the widget
*/
private fun cancelRecurringAlarm(context: Context, appWidgetId: AppWidgetId) {
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)
val pendingIntent = getPendingIntent(context, appWidgetId, create = true)
val alarmManager = alarmManager(context)
Timber.d("Canceling recurring alarm for widget ID: $appWidgetId")
alarmManager.cancel(pendingIntent)
if (pendingIntent != null) {
alarmManager.cancel(pendingIntent)
}
}
}

Expand Down Expand Up @@ -234,13 +268,27 @@ class DeckPickerWidget : AnalyticsWidgetProvider(), ChangeManager.Subscriber {
// It typically happens when the widget is added, resized, or updated from an external trigger.
ACTION_APPWIDGET_UPDATE -> {
val appWidgetManager = AppWidgetManager.getInstance(context)

/**
* Array of widget IDs that need to be updated.
*
* When the `ACTION_APPWIDGET_UPDATE` intent is broadcast, it may include multiple widget IDs
* if there are multiple instances of the widget on the user's home screen. This array
* (`appWidgetIds`) represents those widget instances that need to be updated.
*
* Handling multiple widget IDs is necessary because users can add multiple instances of the
* same widget, and the system updates them together. The loop through this array ensures
* that each widget is updated individually.
*/
val appWidgetIds = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
val selectedDeckIds = intent.getLongArrayExtra("deck_picker_widget_selected_deck_ids")
val selectedDeckIds = intent.getLongArrayExtra(EXTRA_SELECTED_DECK_IDS)

if (appWidgetIds != null && selectedDeckIds != null) {
for (appWidgetId in appWidgetIds) {
updateWidget(context, appWidgetManager, appWidgetId, selectedDeckIds)
}
if (appWidgetIds == null || selectedDeckIds == null) {
return
}

for (appWidgetId in appWidgetIds) {
updateWidget(context, appWidgetManager, appWidgetId, selectedDeckIds)
}
}
// This custom action is received to update a specific widget.
Expand Down
54 changes: 46 additions & 8 deletions AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidgetConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import timber.log.Timber
* Activity for configuring the Deck Picker Widget.
* This activity allows the user to select decks from deck selection dialog to be displayed in the widget.
* User can Select up to 5 decks.
* User Can remove, reorder decks and reconfigure by holding the widget .
* User Can remove, reorder decks and reconfigure by holding the widget.
*/
class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener {

Expand Down Expand Up @@ -85,6 +85,7 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener {
deckAdapter.removeDeck(position)
showSnackbar(R.string.deck_removed_from_widget)
updateViewVisibility()
updateFabVisibility()
hasUnsavedChanges = true // Set flag when deck is removed
}

Expand Down Expand Up @@ -172,6 +173,16 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener {
unregisterReceiverSilently(widgetRemovedReceiver)
}

/** Updates the visibility of the FloatingActionButton based on the number of selected decks */
private fun updateFabVisibility() {
val fab = findViewById<FloatingActionButton>(R.id.fabWidgetDeckPicker)
fab.visibility = if (deckAdapter.itemCount >= MAX_DECKS_ALLOWED) {
View.GONE
} else {
View.VISIBLE
}
}

/** Loads saved preferences and updates the RecyclerView */
fun loadSavedPreferences() {
val selectedDeckIds = deckPickerWidgetPreferences.getSelectedDeckIdsFromPreferencesDeckPickerWidget(appWidgetId)
Expand All @@ -181,6 +192,7 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener {
val selectedDecks = decks.filter { it.deckId in selectedDeckIds }
selectedDecks.forEach { deckAdapter.addDeck(it) }
updateViewVisibility()
updateFabVisibility()
}
}
}
Expand Down Expand Up @@ -217,13 +229,36 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener {
return
}

if (deckAdapter.itemCount >= MAX_DECKS_ALLOWED) {
val message = resources.getQuantityString(R.plurals.deck_limit_reached, MAX_DECKS_ALLOWED, MAX_DECKS_ALLOWED)
// Check if the deck is already in the list
val isDeckAlreadySelected = deckAdapter.deckIds.contains(deck.deckId)

if (isDeckAlreadySelected) {
// Show snackbar if the deck is already selected
val message = getString(R.string.deck_already_selected_message)
showSnackbar(message)
return
}

// Check if the deck is being added to a fully occupied selection
if (deckAdapter.itemCount >= MAX_DECKS_ALLOWED) {
// Snackbar will only be shown when adding the 5th deck
if (deckAdapter.itemCount == MAX_DECKS_ALLOWED) {
val message = resources.getQuantityString(R.plurals.deck_limit_reached, MAX_DECKS_ALLOWED, MAX_DECKS_ALLOWED)
showSnackbar(message)
}
// The FAB visibility should be handled in updateFabVisibility()
} else {
// Add the deck and update views
deckAdapter.addDeck(deck)
updateViewVisibility()
updateFabVisibility() // Ensure FAB visibility is updated when deck is added
hasUnsavedChanges = true // Set flag when deck is added

// Show snackbar if the deck is the 5th deck
if (deckAdapter.itemCount == MAX_DECKS_ALLOWED) {
val message = resources.getQuantityString(R.plurals.deck_limit_reached, MAX_DECKS_ALLOWED, MAX_DECKS_ALLOWED)
showSnackbar(message)
}
}
}

Expand Down Expand Up @@ -271,15 +306,18 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener {
* It then sends a broadcast to update the widget with the new deck selection.
*/
fun saveSelectedDecksToPreferencesDeckPickerWidget() {
// Get the list of selected deck IDs as strings
val selectedDecks = deckAdapter.deckIds.map { it.toString() }
deckPickerWidgetPreferences.saveSelectedDecks(appWidgetId, selectedDecks)
// Get the list of selected deck IDs as Long
val selectedDecks = deckAdapter.deckIds.map { it }

// Save the selected deck IDs as a comma-separated string
deckPickerWidgetPreferences.saveSelectedDecks(appWidgetId, selectedDecks.map { it.toString() })

val updateIntent = Intent(this, DeckPickerWidget::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))

// Pass the selected deck IDs as a long array in the intent
putExtra("deck_picker_widget_selected_deck_ids", selectedDecks.map { it.toLong() }.toLongArray())
// Pass the selected deck IDs as a List<Long> in the intent
putExtra("deck_picker_widget_selected_deck_ids", selectedDecks.toList().toLongArray())
}

// Send the broadcast to update the widget
Expand Down
17 changes: 9 additions & 8 deletions AnkiDroid/src/main/res/values/08-widget.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,21 @@

<!-- Strings to explain usage in Deck Picker Widget Configuration screen -->
<string name="select_deck_title" comment="Title for Deck Selection Dialog">Select decks</string>
<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="no_selected_deck_placeholder_title" comment="Placeholder title when no decks are selected">Select decks to display in the widget</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>
<string name="deck_already_selected_message" comment="Snackbar when user try to select the same deck again">This deck is already selected</string>
<plurals name="deck_limit_reached">
<item quantity="one">You can select up to %d decks.</item>
<item quantity="one">You can select up to %d deck.</item>
<item quantity="other">You can select up to %d decks.</item>
</plurals>

<!-- Sample deck names and properties for Deck Picker Widget layout in widget picker screen -->
<string name="deck1Name_deck_picker_widget" comment="Sample deck name for Deck 1 in Deck Picker Widget layout in widget picker">English</string>
<string name="deck2Name_deck_picker_widget" comment="Sample deck name for Deck 2 in Deck Picker Widget layout in widget picker">Español</string>
<string name="deck3Name_deck_picker_widget" comment="Sample deck name for Deck 3 in Deck Picker Widget layout in widget picker">日本語</string>
<string name="deck1Name_deck_picker_widget" comment="Example of a Deck name. Android's widget provider display it as an example of what the Deck Picker widget may look like">English</string>
<string name="deck2Name_deck_picker_widget" comment="Similar to the first deck, but in Spanish">Español</string>
<string name="deck3Name_deck_picker_widget" comment="Similar to the first deck, but in Japanese">日本語</string>

</resources>

0 comments on commit e81e843

Please sign in to comment.