diff --git a/AnkiDroid/src/main/AndroidManifest.xml b/AnkiDroid/src/main/AndroidManifest.xml index 01ca68f56f85..cdb7a6d26769 100644 --- a/AnkiDroid/src/main/AndroidManifest.xml +++ b/AnkiDroid/src/main/AndroidManifest.xml @@ -526,6 +526,7 @@ 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"--> diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/snackbar/Snackbars.kt b/AnkiDroid/src/main/java/com/ichi2/anki/snackbar/Snackbars.kt index e991d463856a..a3daa829b90a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/snackbar/Snackbars.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/snackbar/Snackbars.kt @@ -70,10 +70,11 @@ interface BaseSnackbarBuilderProvider { fun Activity.showSnackbar( @StringRes textResource: Int, duration: Int = Snackbar.LENGTH_LONG, + anchorView: View? = null, snackbarBuilder: SnackbarBuilder? = null ) { val text = getText(textResource) - showSnackbar(text, duration, snackbarBuilder) + showSnackbar(text, duration, anchorView, snackbarBuilder) } /** @@ -104,13 +105,14 @@ fun Activity.showSnackbar( fun Activity.showSnackbar( text: CharSequence, duration: Int = Snackbar.LENGTH_LONG, + anchorView: View? = null, snackbarBuilder: SnackbarBuilder? = null ) { val view: View? = findViewById(R.id.root_layout) as? CoordinatorLayout if (view != null) { val baseSnackbarBuilder = (this as? BaseSnackbarBuilderProvider)?.baseSnackbarBuilder - view.showSnackbar(text, duration) { + view.showSnackbar(text, duration, anchorView = anchorView) { baseSnackbarBuilder?.invoke(this) snackbarBuilder?.invoke(this) Timber.d("displayed snackbar: '%s'", text) @@ -157,10 +159,11 @@ fun Activity.showSnackbar( fun View.showSnackbar( @StringRes textResource: Int, duration: Int = Snackbar.LENGTH_LONG, + anchorView: View? = null, snackbarBuilder: SnackbarBuilder? = null ) { val text = resources.getText(textResource) - showSnackbar(text, duration, snackbarBuilder) + showSnackbar(text, duration, anchorView, snackbarBuilder) } /** @@ -192,19 +195,24 @@ fun View.showSnackbar( fun View.showSnackbar( text: CharSequence, duration: Int = Snackbar.LENGTH_LONG, + anchorView: View? = null, snackbarBuilder: SnackbarBuilder? = null ) { - val snackbar = Snackbar.make(this, text, duration) - snackbar.setMaxLines(4) - snackbar.behavior = SwipeDismissBehaviorFix() + Snackbar.make(this, text, duration).apply { + this.anchorView = anchorView + setMaxLines(4) + behavior = SwipeDismissBehaviorFix() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - snackbar.fixMarginsWhenInsetsChange() - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + fixMarginsWhenInsetsChange() + } - if (snackbarBuilder != null) { snackbar.snackbarBuilder() } + if (snackbarBuilder != null) { + snackbarBuilder() + } - snackbar.show() + show() + } } /** @@ -236,10 +244,11 @@ fun View.showSnackbar( fun Fragment.showSnackbar( text: CharSequence, duration: Int = Snackbar.LENGTH_LONG, + anchorView: View? = null, snackbarBuilder: SnackbarBuilder? = null ) { val baseSnackbarBuilder = (this as? BaseSnackbarBuilderProvider)?.baseSnackbarBuilder - requireActivity().showSnackbar(text, duration) { + requireActivity().showSnackbar(text, duration, anchorView = anchorView) { baseSnackbarBuilder?.invoke(this) snackbarBuilder?.invoke(this) Timber.d("displayed snackbar: '%s'", text) @@ -275,10 +284,11 @@ fun Fragment.showSnackbar( fun Fragment.showSnackbar( @StringRes textResource: Int, duration: Int = Snackbar.LENGTH_LONG, + anchorView: View? = null, snackbarBuilder: SnackbarBuilder? = null ) { val text = resources.getText(textResource) - showSnackbar(text, duration, snackbarBuilder) + showSnackbar(text, duration, anchorView, snackbarBuilder) } /* ********************************************************************************************** */ diff --git a/AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt b/AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt index 084f3cabfdda..4e5fe63968d2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt +++ b/AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidget.kt @@ -97,11 +97,8 @@ class DeckPickerWidget : AnalyticsWidgetProvider(), ChangeManager.Subscriber { * Each ID corresponds to a specific deck, and the view will * contain exactly the decks whose IDs are in this list. * - * TODO: Implement the following enhancements: - * 1. Instead of doing nothing when a deck has no cards to review, open the Study Options screen - * so the user can choose to do a custom study. - * 2. If the deck is completely empty (no cards at all), display a Snackbar or Toast message - * saying "The deck is empty" instead of opening any activity. + * TODO: If the deck is completely empty (no cards at all), display a Snackbar or Toast message + * saying "The deck is empty" instead of opening any activity. * */ fun updateWidget( @@ -322,13 +319,17 @@ class DeckPickerWidget : AnalyticsWidgetProvider(), ChangeManager.Subscriber { } } AppWidgetManager.ACTION_APPWIDGET_DELETED -> { + Timber.d("ACTION_APPWIDGET_DELETED received") val appWidgetId = intent.getIntExtra( AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID ) if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + Timber.d("Deleting widget with ID: $appWidgetId") cancelRecurringAlarm(context, appWidgetId) widgetPreferences.deleteDeckPickerWidgetData(appWidgetId) + } else { + Timber.e("Invalid widget ID received in ACTION_APPWIDGET_DELETED") } } AppWidgetManager.ACTION_APPWIDGET_ENABLED -> { diff --git a/AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidgetConfig.kt b/AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidgetConfig.kt index 2f762175a03c..a59fe8203615 100644 --- a/AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidgetConfig.kt +++ b/AnkiDroid/src/main/java/com/ichi2/widget/DeckPickerWidgetConfig.kt @@ -59,8 +59,12 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener { private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID lateinit var deckAdapter: WidgetConfigScreenAdapter 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 + + /** + * Maximum number of decks allowed in the widget. + */ + private val MAX_DECKS_ALLOWED = 5 + private var hasUnsavedChanges = false private var dueTree: DeckNode? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -69,6 +73,11 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener { } super.onCreate(savedInstanceState) + + if (!ensureStoragePermissions()) { + return + } + setContentView(R.layout.widget_deck_picker_config) deckPickerWidgetPreferences = WidgetPreferences(this) @@ -84,7 +93,7 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener { return } - // Check if the collection is empty before proceeding + // Check if the collection is empty before proceeding and if the collection is empty, show a toast instead of the configuration view. lifecycleScope.launch { val tree = withCol { sched.deckDueTree() } if (isCollectionEmpty(tree)) { @@ -103,15 +112,17 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener { } @SuppressLint("DirectSnackbarMakeUsage") + fun showSnackbar(message: CharSequence) { + val v: View = findViewById(R.id.widgetConfigContainer) + v.showSnackbar( + message, + Snackbar.LENGTH_LONG, + findViewById(R.id.fabWidgetDeckPicker) + ) + } + fun showSnackbar(messageResId: Int) { - val snackbar = Snackbar.make( - findViewById(R.id.widgetConfigContainer), - getString(messageResId), - Snackbar.LENGTH_LONG - ).apply { - anchorView = findViewById(R.id.fabWidgetDeckPicker) - } - snackbar.show() + showSnackbar(getString(messageResId)) } // Method to initialize UI components @@ -139,18 +150,7 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener { // TODO: Implement multi-select functionality so that user can select desired decks in once. findViewById(R.id.fabWidgetDeckPicker).setOnClickListener { - 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) - if (isEmpty) { - showSnackbar(R.string.no_decks_available_message) - } else { - showDeckSelectionDialog() - } - } - } + showDeckSelectionDialog() } // Load and display saved preferences @@ -341,8 +341,7 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener { if (isDeckAlreadySelected) { // Show snackbar if the deck is already selected // TODO: Eventually, ensure that the user can't select a deck that is already selected. - val message = getString(R.string.deck_already_selected_message) - showSnackbar(message) + showSnackbar(getString(R.string.deck_already_selected_message)) return } @@ -439,11 +438,6 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener { sendBroadcast(updateIntent) } - /** Deletes the widget data associated with the given app widget ID. */ - private fun deleteWidgetDataDeckPickerWidget(appWidgetId: Int) { - deckPickerWidgetPreferences.deleteDeckPickerWidgetData(appWidgetId) - } - /** BroadcastReceiver to handle widget removal. */ private val widgetRemovedReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -456,7 +450,7 @@ class DeckPickerWidgetConfig : AnkiActivity(), DeckSelectionListener { return } - context?.let { deleteWidgetDataDeckPickerWidget(appWidgetId) } + context?.let { deckPickerWidgetPreferences.deleteDeckPickerWidgetData(appWidgetId) } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/widget/WidgetPreferences.kt b/AnkiDroid/src/main/java/com/ichi2/widget/WidgetPreferences.kt index 2a12962ac893..a5aaefef61f8 100644 --- a/AnkiDroid/src/main/java/com/ichi2/widget/WidgetPreferences.kt +++ b/AnkiDroid/src/main/java/com/ichi2/widget/WidgetPreferences.kt @@ -26,16 +26,29 @@ import androidx.core.content.edit */ class WidgetPreferences(context: Context) { + /** + * Prefix for the SharedPreferences key used to store the selected decks for the DeckPickerWidget. + * The full key is constructed by appending the appWidgetId to this prefix, ensuring that each + * widget instance has a unique key. This approach helps prevent typos and ensures consistency + * across the codebase when accessing or modifying the stored deck selections. + */ + private val DECK_PICKER_WIDGET_KEY = "deck_picker_widget_selected_decks_" + private val deckPickerSharedPreferences = context.getSharedPreferences("DeckPickerWidgetPrefs", Context.MODE_PRIVATE) - // Deletes the stored data for a specific widget for DeckPickerWidget + /** + * Deletes the selected deck IDs from the shared preferences for the given widget ID. + */ fun deleteDeckPickerWidgetData(appWidgetId: Int) { deckPickerSharedPreferences.edit { remove("deck_picker_widget_selected_decks_$appWidgetId") } } - // Get selected deck IDs from shared preferences for DeckPickerWidget + /** + * Retrieves the selected deck IDs from the shared preferences for the given widget ID. + * Note: There's no guarantee that these IDs still represent decks that exist at the time of execution. + */ fun getSelectedDeckIdsFromPreferencesDeckPickerWidget(appWidgetId: Int): LongArray { val selectedDecksString = deckPickerSharedPreferences.getString("deck_picker_widget_selected_decks_$appWidgetId", "") return if (!selectedDecksString.isNullOrEmpty()) { @@ -45,19 +58,19 @@ class WidgetPreferences(context: Context) { } } - // Save selected deck IDs to shared preferences for DeckPickerWidget - fun saveSelectedDecks(appWidgetId: Int, selectedDecks: List) { - deckPickerSharedPreferences.edit { - putString("deck_picker_widget_selected_decks_$appWidgetId", selectedDecks.joinToString(",")) - } + /** + * Generates the key for the shared preferences for the given widget ID. + */ + private fun getDeckPickerWidgetKey(appWidgetId: Int): String { + return "$DECK_PICKER_WIDGET_KEY$appWidgetId" } - fun getAllWidgetIds(): IntArray { - val widgetIdsString = deckPickerSharedPreferences.getString("all_widget_ids", "") - return if (!widgetIdsString.isNullOrEmpty()) { - widgetIdsString.split(",").map { it.toInt() }.toIntArray() - } else { - intArrayOf() + /** + * Saves the selected deck IDs to the shared preferences for the given widget ID. + */ + fun saveSelectedDecks(appWidgetId: Int, selectedDecks: List) { + deckPickerSharedPreferences.edit { + putString(getDeckPickerWidgetKey(appWidgetId), selectedDecks.joinToString(",")) } } } diff --git a/AnkiDroid/src/main/res/layout/widget_deck_picker_config.xml b/AnkiDroid/src/main/res/layout/widget_deck_picker_config.xml index 320baa4faf4f..b0bf52dc2303 100644 --- a/AnkiDroid/src/main/res/layout/widget_deck_picker_config.xml +++ b/AnkiDroid/src/main/res/layout/widget_deck_picker_config.xml @@ -23,16 +23,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" + android:layout_marginStart="7dp" android:gravity="center" + android:padding="30dp" android:text="@string/no_selected_deck_placeholder_title" /> - + - + \ No newline at end of file diff --git a/AnkiDroid/src/main/res/layout/widget_item_deck_main.xml b/AnkiDroid/src/main/res/layout/widget_item_deck_main.xml index 93b50c0c890a..a5ab7f6921dc 100644 --- a/AnkiDroid/src/main/res/layout/widget_item_deck_main.xml +++ b/AnkiDroid/src/main/res/layout/widget_item_deck_main.xml @@ -35,7 +35,7 @@ tools:text="50" /> Select decks - Select decks to display in the widget - Select decks with the + icon. - Deck Removed - No decks available to select" + Select decks to display in the widget. Select decks with the + icon. + Deck Removed This deck is already selected You can select up to %d deck. diff --git a/AnkiDroid/src/main/res/xml/widget_provider_deck_picker.xml b/AnkiDroid/src/main/res/xml/widget_provider_deck_picker.xml index 91d780f4665d..f328be52ab53 100644 --- a/AnkiDroid/src/main/res/xml/widget_provider_deck_picker.xml +++ b/AnkiDroid/src/main/res/xml/widget_provider_deck_picker.xml @@ -8,7 +8,6 @@ while same for height but just adjusted for desired widget layout in most of cases.-->