diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index 8a88907d265d..f9dd11d86d38 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -2624,12 +2624,11 @@ abstract class AbstractFlashcardViewer : get() = displayAnswer internal fun showTagsDialog() { - val tags = ArrayList(getColUnsafe.tags.all()) - val selTags = ArrayList(currentCard!!.note(getColUnsafe).tags) + val noteId = currentCard!!.note(getColUnsafe).id val dialog = tagsDialogFactory!! .newTagsDialog() - .withArguments(this, TagsDialog.DialogType.EDIT_TAGS, selTags, tags) + .withArguments(this, TagsDialog.DialogType.EDIT_TAGS, noteIds = listOf(noteId)) showDialogFragment(dialog) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt index e4903a4e4301..482c65ac521b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt @@ -68,7 +68,7 @@ import com.ichi2.anki.browser.CardBrowserViewModel.SearchState.Searching import com.ichi2.anki.browser.CardOrNoteId import com.ichi2.anki.browser.ColumnHeading import com.ichi2.anki.browser.FindAndReplaceDialogFragment -import com.ichi2.anki.browser.PreviewerIdsFile +import com.ichi2.anki.browser.IdsFile import com.ichi2.anki.browser.RepositionCardFragment import com.ichi2.anki.browser.RepositionCardFragment.Companion.REQUEST_REPOSITION_NEW_CARDS import com.ichi2.anki.browser.RepositionCardsRequest.ContainsNonNewCardsError @@ -120,7 +120,6 @@ import com.ichi2.libanki.ChangeManager import com.ichi2.libanki.Collection import com.ichi2.libanki.DeckId import com.ichi2.libanki.DeckNameId -import com.ichi2.libanki.NoteId import com.ichi2.libanki.SortOrder import com.ichi2.libanki.undoableOp import com.ichi2.ui.CardBrowserSearchView @@ -130,10 +129,8 @@ import com.ichi2.utils.dp import com.ichi2.utils.increaseHorizontalPaddingOfOverflowMenuIcons import com.ichi2.utils.updatePaddingRelative import com.ichi2.widget.WidgetStatus.updateInBackground -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import net.ankiweb.rsdroid.RustCleanup import net.ankiweb.rsdroid.Translations import timber.log.Timber @@ -1437,14 +1434,14 @@ open class CardBrowser : private fun onPreview() { launchCatchingTask { val intentData = viewModel.queryPreviewIntentData() - onPreviewCardsActivityResult.launch(getPreviewIntent(intentData.currentIndex, intentData.previewerIdsFile)) + onPreviewCardsActivityResult.launch(getPreviewIntent(intentData.currentIndex, intentData.idsFile)) } } private fun getPreviewIntent( index: Int, - previewerIdsFile: PreviewerIdsFile, - ): Intent = PreviewerDestination(index, previewerIdsFile).toIntent(this) + idsFile: IdsFile, + ): Intent = PreviewerDestination(index, idsFile).toIntent(this) private fun rescheduleSelectedCards() { if (!viewModel.hasSelectedAnyRows()) { @@ -1503,84 +1500,16 @@ open class CardBrowser : if (!viewModel.hasSelectedAnyRows()) { Timber.d("showEditTagsDialog: called with empty selection") } - - var progressMax: Int? = null // this can be made null to blank the dialog - var progress = 0 - - fun onProgress(progressContext: ProgressContext) { - val max = progressMax - if (max == null) { - progressContext.amount = null - progressContext.text = getString(R.string.dialog_processing) - } else { - progressContext.amount = Pair(progress, max) - } - } - launchCatchingTask { - withProgress(extractProgress = ::onProgress) { - val allTags = withCol { tags.all() } - val selectedNoteIds = viewModel.queryAllSelectedNoteIds() - - progressMax = selectedNoteIds.size * 2 - // TODO!! This is terribly slow on AnKing - val checkedTags = - withCol { - selectedNoteIds - .asSequence() // reduce memory pressure - .flatMap { nid -> - progress++ - getNote(nid).tags // requires withCol - }.distinct() - .toList() - } - - if (selectedNoteIds.size == 1) { - Timber.d("showEditTagsDialog: edit tags for one note") - tagsDialogListenerAction = TagsDialogListenerAction.EDIT_TAGS - val dialog = - tagsDialogFactory.newTagsDialog().withArguments( - this@CardBrowser, - type = TagsDialog.DialogType.EDIT_TAGS, - checkedTags = checkedTags, - allTags = allTags, - ) - showDialogFragment(dialog) - return@withProgress - } - // TODO!! This is terribly slow on AnKing - // PERF: This MUST be combined with the above sequence - this becomes O(2n) on a - // database operation performed over 30k times - val uncheckedTags = - withCol { - selectedNoteIds - .asSequence() // reduce memory pressure - .flatMap { nid: NoteId -> - progress++ - val note = getNote(nid) // requires withCol - val noteTags = note.tags.toSet() - allTags.filter { t: String? -> !noteTags.contains(t) } - }.distinct() - .toList() - } - - progressMax = null - - Timber.d("showEditTagsDialog: edit tags for multiple note") - tagsDialogListenerAction = TagsDialogListenerAction.EDIT_TAGS - - // withArguments performs IO, can be 18 seconds - val dialog = - withContext(Dispatchers.IO) { - tagsDialogFactory.newTagsDialog().withArguments( - context = this@CardBrowser, - type = TagsDialog.DialogType.EDIT_TAGS, - checkedTags = checkedTags, - uncheckedTags = uncheckedTags, - allTags = allTags, - ) - } - showDialogFragment(dialog) - } + tagsDialogListenerAction = TagsDialogListenerAction.EDIT_TAGS + lifecycleScope.launch { + val noteIds = viewModel.queryAllSelectedNoteIds() + val dialog = + tagsDialogFactory.newTagsDialog().withArguments( + this@CardBrowser, + type = TagsDialog.DialogType.EDIT_TAGS, + noteIds = noteIds, + ) + showDialogFragment(dialog) } } @@ -1591,8 +1520,7 @@ open class CardBrowser : tagsDialogFactory.newTagsDialog().withArguments( context = this@CardBrowser, type = TagsDialog.DialogType.FILTER_BY_TAG, - checkedTags = ArrayList(0), - allTags = withCol { tags.all() }, + noteIds = emptyList(), ) showDialogFragment(dialog) } @@ -1950,8 +1878,8 @@ suspend fun searchForRows( class PreviewerDestination( val currentIndex: Int, - val previewerIdsFile: PreviewerIdsFile, + val idsFile: IdsFile, ) @CheckResult -fun PreviewerDestination.toIntent(context: Context) = PreviewerFragment.getIntent(context, previewerIdsFile, currentIndex) +fun PreviewerDestination.toIntent(context: Context) = PreviewerFragment.getIntent(context, idsFile, currentIndex) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt index 08ec268c0dfb..f22d856c333d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt @@ -1605,18 +1605,13 @@ class NoteEditor : } private fun showTagsDialog() { - if (selectedTags == null) { - selectedTags = ArrayList(0) - } - val tags = ArrayList(getColUnsafe.tags.all()) - val selTags = ArrayList(selectedTags!!) + val selTags = selectedTags?.let { ArrayList(it) } ?: arrayListOf() val dialog = with(requireContext()) { tagsDialogFactory!!.newTagsDialog().withArguments( context = this, type = TagsDialog.DialogType.EDIT_TAGS, checkedTags = selTags, - allTags = tags, ) } showDialogFragment(dialog) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt index 19f626226101..a656e6dffcf4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserViewModel.kt @@ -881,11 +881,11 @@ class CardBrowserViewModel( suspend fun queryPreviewIntentData(): PreviewerDestination { // If in NOTES mode, we show one Card per Note, as this matches Anki Desktop return if (selectedRowCount() > 1) { - PreviewerDestination(currentIndex = 0, PreviewerIdsFile(cacheDir, queryAllSelectedCardIds())) + PreviewerDestination(currentIndex = 0, IdsFile(cacheDir, queryAllSelectedCardIds())) } else { // Preview all cards, starting from the one that is currently selected val startIndex = indexOfFirstCheckedCard() ?: 0 - PreviewerDestination(startIndex, PreviewerIdsFile(cacheDir, queryOneCardIdPerNote())) + PreviewerDestination(startIndex, IdsFile(cacheDir, queryOneCardIdPerNote())) } } @@ -1135,26 +1135,28 @@ enum class SaveSearchResult { } /** - * Temporary file containing the IDs of the cards to be displayed at the previewer + * Temporary file containing cards or note IDs to be passed in a Bundle. + * + * It avoids [android.os.TransactionTooLargeException] when passing a big amount of data. */ -class PreviewerIdsFile( +class IdsFile( path: String, ) : File(path), Parcelable { /** * @param directory parent directory of the file. Generally it should be the cache directory - * @param cardIds ids of the cards to be displayed + * @param ids ids to store */ - constructor(directory: File, cardIds: List) : this(createTempFile("previewerIds", ".tmp", directory).path) { + constructor(directory: File, ids: List) : this(createTempFile("ids", ".tmp", directory).path) { DataOutputStream(FileOutputStream(this)).use { outputStream -> - outputStream.writeInt(cardIds.size) - for (id in cardIds) { + outputStream.writeInt(ids.size) + for (id in ids) { outputStream.writeLong(id) } } } - fun getCardIds(): List = + fun getIds(): List = DataInputStream(FileInputStream(this)).use { inputStream -> val size = inputStream.readInt() List(size) { inputStream.readLong() } @@ -1173,10 +1175,10 @@ class PreviewerIdsFile( @JvmField @Suppress("unused") val CREATOR = - object : Parcelable.Creator { - override fun createFromParcel(source: Parcel?): PreviewerIdsFile = PreviewerIdsFile(source!!.readString()!!) + object : Parcelable.Creator { + override fun createFromParcel(source: Parcel?): IdsFile = IdsFile(source!!.readString()!!) - override fun newArray(size: Int): Array = arrayOf() + override fun newArray(size: Int): Array = arrayOf() } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsArrayAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsArrayAdapter.kt index 80e705791bc7..f9814588507d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsArrayAdapter.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsArrayAdapter.kt @@ -15,7 +15,6 @@ */ package com.ichi2.anki.dialogs.tags -import android.content.res.Resources import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -45,7 +44,6 @@ import java.util.TreeSet */ class TagsArrayAdapter( private val tags: TagsList, - private val resources: Resources, ) : RecyclerView.Adapter(), Filterable { class ViewHolder( @@ -302,7 +300,7 @@ class TagsArrayAdapter( // do not add padding if there is no visible nested tag holder.expandButton.visibility = View.GONE } - holder.expandButton.contentDescription = resources.getString(R.string.expand_tag, holder.node.tag.replace("::", " ")) + holder.expandButton.contentDescription = holder.itemView.context.getString(R.string.expand_tag, holder.node.tag.replace("::", " ")) holder.textView.text = TagsUtil.getTagParts(holder.node.tag).last() diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsDialog.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsDialog.kt index 9ea5e4df4dcf..f8bb87247506 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsDialog.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsDialog.kt @@ -1,16 +1,14 @@ //noinspection MissingCopyrightHeader #8659 package com.ichi2.anki.dialogs.tags -import android.annotation.SuppressLint import android.app.Dialog import android.content.Context +import android.content.DialogInterface import android.os.Bundle -import android.os.Parcel import android.os.Parcelable import android.text.InputFilter import android.text.InputType import android.text.Spanned -import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.WindowManager @@ -18,7 +16,6 @@ import android.widget.EditText import android.widget.RadioGroup import android.widget.TextView import androidx.annotation.VisibleForTesting -import androidx.annotation.WorkerThread import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar @@ -26,14 +23,22 @@ import androidx.core.content.ContextCompat import androidx.core.os.BundleCompat import androidx.core.os.bundleOf import androidx.core.view.isVisible +import androidx.fragment.app.viewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.ichi2.anki.OnContextAndLongClickListener import com.ichi2.anki.R import com.ichi2.anki.analytics.AnalyticsDialogFragment +import com.ichi2.anki.browser.IdsFile +import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.model.CardStateFilter import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.annotations.NeedsTest +import com.ichi2.libanki.NoteId import com.ichi2.ui.AccessibleSearchView import com.ichi2.utils.DisplayUtils.resizeWhenSoftInputShown import com.ichi2.utils.TagsUtil @@ -44,18 +49,21 @@ import com.ichi2.utils.negativeButton import com.ichi2.utils.positiveButton import com.ichi2.utils.show import com.ichi2.utils.title -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import org.apache.commons.io.FileUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize import timber.log.Timber -import java.io.File -import java.nio.charset.Charset class TagsDialog : AnalyticsDialogFragment { /** * Enum that define all possible types of TagsDialog */ - enum class DialogType { + @Parcelize + enum class DialogType : Parcelable { /** * Edit tags of note(s) */ @@ -68,19 +76,30 @@ class TagsDialog : AnalyticsDialogFragment { } private var type: DialogType? = null - private var tags: TagsList? = null - private var positiveText: String? = null - private var dialogTitle: String? = null private var tagsArrayAdapter: TagsArrayAdapter? = null private var toolbarSearchView: AccessibleSearchView? = null private var toolbarSearchItem: MenuItem? = null private var noTagsTextView: TextView? = null - private var tagsListRecyclerView: RecyclerView? = null - private var dialog: AlertDialog? = null private val listener: TagsDialogListener? private lateinit var selectedOption: CardStateFilter + @VisibleForTesting + val viewModel: TagsDialogViewModel by viewModels { + val idsFile = + requireNotNull( + BundleCompat.getParcelable(requireArguments(), ARG_TAGS_FILE, IdsFile::class.java), + ) { + "$ARG_TAGS_FILE is required" + } + val noteIds = idsFile.getIds() + val checkedTags = + requireNotNull(requireArguments().getStringArrayList(ARG_CHECKED_TAGS)) { + "$ARG_CHECKED_TAGS is required" + } + viewModelFactory { initializer { TagsDialogViewModel(noteIds = noteIds, checkedTags = checkedTags) } } + } + /** * Constructs a new [TagsDialog] that will communicate the results using the provided listener. */ @@ -97,39 +116,23 @@ class TagsDialog : AnalyticsDialogFragment { listener = null } - /** - * @param type the type of dialog @see [DialogType] - * @param checkedTags tags of the note - * @param allTags all possible tags in the collection - * @return Initialized instance of [TagsDialog] - */ - fun withArguments( - context: Context, - type: DialogType, - checkedTags: List, - allTags: List, - ): TagsDialog = withArguments(context, type, checkedTags, null, allTags) - /** * Construct a tags dialog for a collection of notes * * @param type the type of dialog @see [DialogType] - * @param checkedTags sum of all checked tags - * @param uncheckedTags sum of all unchecked tags - * @param allTags all possible tags in the collection * @return Initialized instance of [TagsDialog] */ fun withArguments( context: Context, type: DialogType, - checkedTags: List, - uncheckedTags: List?, - allTags: List, + noteIds: List = emptyList(), + checkedTags: ArrayList = arrayListOf(), ): TagsDialog { - val data = TagsFile.TagsData(type, checkedTags, uncheckedTags, allTags) - val file = TagsFile(context.cacheDir, data) + val file = IdsFile(context.cacheDir, noteIds) arguments = this.arguments ?: bundleOf( ARG_TAGS_FILE to file, + ARG_DIALOG_TYPE to type, + ARG_CHECKED_TAGS to checkedTags, ) return this } @@ -138,21 +141,12 @@ class TagsDialog : AnalyticsDialogFragment { super.onCreate(savedInstanceState) resizeWhenSoftInputShown(requireActivity().window) - val tagsFile = + type = requireNotNull( - BundleCompat.getParcelable(requireArguments(), ARG_TAGS_FILE, TagsFile::class.java), + BundleCompat.getParcelable(requireArguments(), ARG_DIALOG_TYPE, DialogType::class.java), ) { - "$ARG_TAGS_FILE is required" + "$ARG_DIALOG_TYPE is required" } - - val data = tagsFile.getData() - type = data.type - tags = - TagsList( - allTags = data.allTags, - checkedTags = data.checkedTags, - uncheckedTags = data.uncheckedTags, - ) isCancelable = true } @@ -166,59 +160,109 @@ class TagsDialog : AnalyticsDialogFragment { "filled as prefix properly. In other dialog types, long-clicking a tag behaves like a short click.", ) override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - @SuppressLint("InflateParams") - val tagsDialogView = LayoutInflater.from(activity).inflate(R.layout.tags_dialog, null, false) - tagsListRecyclerView = tagsDialogView.findViewById(R.id.tags_dialog_tags_list) - val tagsListRecyclerView: RecyclerView? = tagsListRecyclerView - tagsListRecyclerView?.requestFocus() - val tagsListLayout: RecyclerView.LayoutManager = LinearLayoutManager(activity) - tagsListRecyclerView?.layoutManager = tagsListLayout - tagsArrayAdapter = TagsArrayAdapter(tags!!, resources) - tagsListRecyclerView?.adapter = tagsArrayAdapter - noTagsTextView = tagsDialogView.findViewById(R.id.tags_dialog_no_tags_textview) - val noTagsTextView: TextView? = noTagsTextView - if (tags!!.isEmpty) { - noTagsTextView?.visibility = View.VISIBLE - } - val optionsGroup = tagsDialogView.findViewById(R.id.tags_dialog_options_radiogroup) - for (i in 0 until optionsGroup.childCount) { - optionsGroup.getChildAt(i).id = i - } - optionsGroup.check(0) - selectedOption = radioButtonIdToCardState(optionsGroup.checkedRadioButtonId) - optionsGroup.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> selectedOption = radioButtonIdToCardState(checkedId) } - if (type == DialogType.EDIT_TAGS) { - dialogTitle = resources.getString(R.string.card_details_tags) - optionsGroup.visibility = View.GONE - positiveText = getString(R.string.dialog_ok) - tagsArrayAdapter!!.tagContextAndLongClickListener = - OnContextAndLongClickListener { v -> - createAddTagDialog(v.tag as String) - true + val view = layoutInflater.inflate(R.layout.tags_dialog, null) + + val positiveText = + if (type == DialogType.EDIT_TAGS) { + getString(R.string.dialog_ok) + } else { + getString(R.string.select) + } + + val tagsListLayout: RecyclerView.LayoutManager = LinearLayoutManager(requireContext()) + val tagsListRecyclerView = + view.findViewById(R.id.tags_dialog_tags_list).apply { + requestFocus() + layoutManager = tagsListLayout + } + val optionsGroup = + view.findViewById(R.id.tags_dialog_options_radiogroup).apply { + isVisible = type != DialogType.EDIT_TAGS + for (i in 0 until childCount) { + getChildAt(i).id = i } - } else { - dialogTitle = resources.getString(R.string.studyoptions_limit_select_tags) - positiveText = getString(R.string.select) - tagsArrayAdapter!!.tagContextAndLongClickListener = OnContextAndLongClickListener { false } + check(0) + } + selectedOption = radioButtonIdToCardState(optionsGroup.checkedRadioButtonId) + optionsGroup.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> + selectedOption = radioButtonIdToCardState(checkedId) } - adjustToolbar(tagsDialogView) - dialog = + + adjustToolbar(view) + + val dialog = AlertDialog .Builder(requireActivity()) - .positiveButton(text = positiveText!!) { - tagsDialogListener.onSelectedTags( - tags!!.copyOfCheckedTagList(), - tags!!.copyOfIndeterminateTagList(), - selectedOption, - ) - }.negativeButton(R.string.dialog_cancel) - .customView(view = tagsDialogView) + .positiveButton(text = positiveText) { onPositiveButton() } + .negativeButton(R.string.dialog_cancel) + .customView(view = view) .create() - val dialog: AlertDialog? = dialog - resizeWhenSoftInputShown(dialog?.window!!) + + lifecycleScope.launch { + val loadingContainer = view.findViewById(R.id.loading_container) + val progressTextView = view.findViewById(R.id.progress_text) + val showProgressJob = + launch { + delay(600) + withContext(Dispatchers.Main) { + loadingContainer.visibility = View.VISIBLE + viewModel.initProgress + .flowWithLifecycle(lifecycle) + .onEach { progress -> + progressTextView.text = + when (progress) { + TagsDialogViewModel.InitProgress.Processing -> + getString(R.string.dialog_processing) + is TagsDialogViewModel.InitProgress.FetchingNoteTags -> + "${progress.noteNumber}/${progress.noteCount}" + TagsDialogViewModel.InitProgress.Finished -> null + } + }.launchIn(lifecycleScope) + } + } + val positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE) + positiveButton?.isEnabled = false + + val tags = viewModel.tags.await() + + tagsArrayAdapter = TagsArrayAdapter(tags) + tagsListRecyclerView.adapter = tagsArrayAdapter + noTagsTextView = view.findViewById(R.id.tags_dialog_no_tags_textview) + if (tags.isEmpty) { + noTagsTextView?.visibility = View.VISIBLE + } + tagsArrayAdapter?.tagContextAndLongClickListener = + if (type == DialogType.EDIT_TAGS) { + OnContextAndLongClickListener { v -> + createAddTagDialog(v.tag as String) + true + } + } else { + OnContextAndLongClickListener { false } + } + showProgressJob.cancel() + loadingContainer.isVisible = false + positiveButton?.isEnabled = true + } + + dialog.window?.let { + resizeWhenSoftInputShown(it) + } + return dialog } + private fun onPositiveButton() { + lifecycleScope.launch { + val tags = viewModel.tags.await() + tagsDialogListener.onSelectedTags( + tags.copyOfCheckedTagList(), + tags.copyOfIndeterminateTagList(), + selectedOption, + ) + } + } + override fun onResume() { super.onResume() dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) @@ -237,8 +281,8 @@ class TagsDialog : AnalyticsDialogFragment { private fun adjustToolbar(tagsDialogView: View) { val toolbar: Toolbar = tagsDialogView.findViewById(R.id.tags_dialog_toolbar) - toolbar.title = dialogTitle - toolbar.inflateMenu(R.menu.tags_dialog_menu) + val titleRes = if (type == DialogType.EDIT_TAGS) R.string.card_details_tags else R.string.studyoptions_limit_select_tags + toolbar.setTitle(titleRes) val toolbarAddItem = toolbar.menu.findItem(R.id.tags_dialog_action_add) val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_add_white) @@ -269,17 +313,19 @@ class TagsDialog : AnalyticsDialogFragment { } override fun onQueryTextChange(newText: String): Boolean { - val adapter = tagsListRecyclerView!!.adapter as TagsArrayAdapter? - adapter!!.filter.filter(newText) + tagsArrayAdapter?.filter?.filter(newText) return true } }, ) val checkAllItem = toolbar.menu.findItem(R.id.tags_dialog_action_select_all) checkAllItem.setOnMenuItemClickListener { - val didChange = tags!!.toggleAllCheckedStatuses() - if (didChange) { - tagsArrayAdapter!!.notifyDataSetChanged() + launchCatchingTask { + val tags = viewModel.tags.await() + val didChange = tags.toggleAllCheckedStatuses() + if (didChange) { + tagsArrayAdapter?.notifyDataSetChanged() + } } true } @@ -325,29 +371,32 @@ class TagsDialog : AnalyticsDialogFragment { @VisibleForTesting fun addTag(rawTag: String?) { - if (!rawTag.isNullOrEmpty()) { + if (rawTag.isNullOrEmpty()) return + lifecycleScope.launch { + val tags = viewModel.tags.await() val tag = TagsUtil.getUniformedTag(rawTag) val feedbackText: String - if (tags!!.add(tag)) { + if (tags.add(tag)) { if (noTagsTextView!!.isVisible) { noTagsTextView!!.visibility = View.GONE } - tags!!.add(tag) + tags.add(tag) + val positiveText = (dialog as? AlertDialog)?.positiveButton?.text ?: getString(R.string.dialog_ok) feedbackText = getString(R.string.tag_editor_add_feedback, tag, positiveText) } else { feedbackText = getString(R.string.tag_editor_add_feedback_existing, tag) } - tags!!.check(tag) - tagsArrayAdapter!!.sortData() - tagsArrayAdapter!!.notifyDataSetChanged() + tags.check(tag) + tagsArrayAdapter?.sortData() + tagsArrayAdapter?.notifyDataSetChanged() // Expand to reveal the newly added tag. - tagsArrayAdapter!!.filter.apply { + tagsArrayAdapter?.filter?.apply { setExpandTarget(tag) refresh() } // Show a snackbar to let the user know the tag was added successfully - dialog!!.findViewById(R.id.tags_dialog_snackbar)?.showSnackbar(feedbackText) + dialog?.findViewById(R.id.tags_dialog_snackbar)?.showSnackbar(feedbackText) } } @@ -355,7 +404,9 @@ class TagsDialog : AnalyticsDialogFragment { internal fun getSearchView(): AccessibleSearchView? = toolbarSearchView companion object { - private const val ARG_TAGS_FILE = "tagsFile" + const val ARG_TAGS_FILE = "tagsFile" + private const val ARG_DIALOG_TYPE = "dialogType" + private const val ARG_CHECKED_TAGS = "checkedTags" /** * The filter that constrains the inputted tag. @@ -402,61 +453,3 @@ class TagsDialog : AnalyticsDialogFragment { } } } - -/** - * Temporary file containing the arguments [TagsDialog] uses - * - * to avoid [android.os.TransactionTooLargeException] - * - */ -@WorkerThread -class TagsFile( - path: String, -) : File(path), - Parcelable { - /** - * @param directory parent directory of the file. Generally it should be the cache directory - * @param data data for the dialog to display. Typically [Context.getCacheDir] - */ - constructor(directory: File, data: TagsData) : this(createTempFile("tagsDialog", ".tmp", directory).path) { - // PERF: Use an alternate format - // https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/formats.md - val jsonEncoded = Json.encodeToString(data) - Timber.d("persisting tags to disk, length: %d", jsonEncoded.length) - FileUtils.writeStringToFile(this, jsonEncoded, Charset.forName("UTF-8")) - } - - fun getData(): TagsData { - // PERF!!: This takes ~2 seconds with AnKing - val jsonEncoded = FileUtils.readFileToString(this, Charset.forName("UTF-8")) - return Json.decodeFromString(jsonEncoded) - } - - override fun describeContents(): Int = 0 - - override fun writeToParcel( - dest: Parcel, - flags: Int, - ) { - dest.writeString(path) - } - - companion object { - @JvmField - @Suppress("unused") - val CREATOR = - object : Parcelable.Creator { - override fun createFromParcel(source: Parcel?): TagsFile = TagsFile(source!!.readString()!!) - - override fun newArray(size: Int): Array = arrayOf() - } - } - - @Serializable - data class TagsData( - val type: TagsDialog.DialogType, - val checkedTags: List, - val uncheckedTags: List?, - val allTags: List, - ) -} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsDialogViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsDialogViewModel.kt new file mode 100644 index 000000000000..cc0da3169594 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsDialogViewModel.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 Brayan Oliveira <69634269+brayandso@users.noreply.github.con> + * + * 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 . + */ +package com.ichi2.anki.dialogs.tags + +import androidx.lifecycle.ViewModel +import com.ichi2.anki.CollectionManager.withCol +import com.ichi2.anki.asyncIO +import com.ichi2.libanki.NoteId +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * @param noteIds IDs of notes whose tags should bfe retrieved and marked as "checked" + * @param checkedTags additional list of checked tags. + * They are joined with the tags retrieved from noteIds + */ +class TagsDialogViewModel( + noteIds: Collection = emptyList(), + checkedTags: Collection = emptyList(), +) : ViewModel() { + val tags: Deferred + + private val _initProgress = MutableStateFlow(InitProgress.Processing) + val initProgress = _initProgress.asStateFlow() + + init { + tags = + asyncIO { + val allTags = withCol { tags.all() }.toSet() + val allCheckedTags = + noteIds + .flatMapIndexedTo(mutableSetOf()) { index, nid -> + _initProgress.emit(InitProgress.FetchingNoteTags(index + 1, noteIds.size)) + withCol { getNote(nid) }.tags + }.apply { + addAll(checkedTags) + } + _initProgress.emit(InitProgress.Processing) + val uncheckedTags = allTags - allCheckedTags + TagsList( + allTags = allTags, + checkedTags = allCheckedTags, + uncheckedTags = uncheckedTags, + ).also { + _initProgress.emit(InitProgress.Finished) + } + } + } + + sealed interface InitProgress { + data object Processing : InitProgress + + class FetchingNoteTags( + val noteNumber: Int, + val noteCount: Int, + ) : InitProgress + + data object Finished : InitProgress + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsList.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsList.kt index 5443c198f876..020dbca1cc57 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsList.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/tags/TagsList.kt @@ -35,14 +35,14 @@ import java.util.TreeSet * @param uncheckedTags a list containing the currently unselected tags. Any duplicates will be ignored. */ class TagsList( - allTags: List, - checkedTags: List, - uncheckedTags: List? = null, + allTags: Collection, + checkedTags: Collection, + uncheckedTags: Collection? = null, ) : Iterable { /** * A Set containing the currently selected tags */ - private val checkedTags: MutableSet + private val checkedTags: MutableSet = TreeSet(java.lang.String.CASE_INSENSITIVE_ORDER) /** * A Set containing the tags with indeterminate state @@ -55,7 +55,6 @@ class TagsList( private val allTags: UniqueArrayList init { - this.checkedTags = TreeSet(java.lang.String.CASE_INSENSITIVE_ORDER) this.checkedTags.addAll(checkedTags) this.allTags = from(allTags, java.lang.String.CASE_INSENSITIVE_ORDER) this.allTags.addAll(this.checkedTags) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt index 7b863b095bc0..6f2e74bf7bd7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerFragment.kt @@ -39,7 +39,7 @@ import com.google.android.material.textview.MaterialTextView import com.ichi2.anki.DispatchKeyEventListener import com.ichi2.anki.Flag import com.ichi2.anki.R -import com.ichi2.anki.browser.PreviewerIdsFile +import com.ichi2.anki.browser.IdsFile import com.ichi2.anki.cardviewer.CardMediaPlayer import com.ichi2.anki.reviewer.BindingProcessor import com.ichi2.anki.reviewer.MappableBinding @@ -59,12 +59,12 @@ class PreviewerFragment : DispatchKeyEventListener, BindingProcessor { override val viewModel: PreviewerViewModel by viewModels { - val previewerIdsFile = - requireNotNull(BundleCompat.getParcelable(requireArguments(), CARD_IDS_FILE_ARG, PreviewerIdsFile::class.java)) { + val idsFile = + requireNotNull(BundleCompat.getParcelable(requireArguments(), CARD_IDS_FILE_ARG, IdsFile::class.java)) { "$CARD_IDS_FILE_ARG is required" } val currentIndex = requireArguments().getInt(CURRENT_INDEX_ARG, 0) - PreviewerViewModel.factory(previewerIdsFile, currentIndex, CardMediaPlayer()) + PreviewerViewModel.factory(idsFile, currentIndex, CardMediaPlayer()) } override val webView: WebView get() = requireView().findViewById(R.id.webview) @@ -282,18 +282,18 @@ class PreviewerFragment : /** Index of the card to be first displayed among the IDs provided by [CARD_IDS_FILE_ARG] */ const val CURRENT_INDEX_ARG = "currentIndex" - /** Argument key to a [PreviewerIdsFile] with the IDs of the cards to be displayed */ + /** Argument key to a [IdsFile] with the IDs of the cards to be displayed */ const val CARD_IDS_FILE_ARG = "cardIdsFile" fun getIntent( context: Context, - previewerIdsFile: PreviewerIdsFile, + idsFile: IdsFile, currentIndex: Int, ): Intent { val arguments = bundleOf( CURRENT_INDEX_ARG to currentIndex, - CARD_IDS_FILE_ARG to previewerIdsFile, + CARD_IDS_FILE_ARG to idsFile, ) return CardViewerActivity.getIntent(context, PreviewerFragment::class, arguments) } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt index 881f1cd4f96f..c5b3f0ed1324 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerViewModel.kt @@ -24,7 +24,7 @@ import com.ichi2.anki.Flag import com.ichi2.anki.NoteEditor import com.ichi2.anki.OnErrorListener import com.ichi2.anki.asyncIO -import com.ichi2.anki.browser.PreviewerIdsFile +import com.ichi2.anki.browser.IdsFile import com.ichi2.anki.cardviewer.CardMediaPlayer import com.ichi2.anki.cardviewer.SingleCardSide import com.ichi2.anki.launchCatchingIO @@ -46,7 +46,7 @@ import kotlinx.coroutines.flow.update import timber.log.Timber class PreviewerViewModel( - previewerIdsFile: PreviewerIdsFile, + idsFile: IdsFile, firstIndex: Int, cardMediaPlayer: CardMediaPlayer, ) : CardViewerViewModel(cardMediaPlayer), @@ -55,7 +55,7 @@ class PreviewerViewModel( val backSideOnly = MutableStateFlow(false) val isMarked = MutableStateFlow(false) val flag: MutableStateFlow = MutableStateFlow(Flag.NONE) - private val selectedCardIds: List = previewerIdsFile.getCardIds() + private val selectedCardIds: List = idsFile.getIds() val isBackButtonEnabled = combine(currentIndex, showingAnswer, backSideOnly) { index, showingAnswer, isBackSideOnly -> index != 0 || (showingAnswer && !isBackSideOnly) @@ -249,13 +249,13 @@ class PreviewerViewModel( companion object { fun factory( - previewerIdsFile: PreviewerIdsFile, + idsFile: IdsFile, currentIndex: Int, cardMediaPlayer: CardMediaPlayer, ): ViewModelProvider.Factory = viewModelFactory { initializer { - PreviewerViewModel(previewerIdsFile, currentIndex, cardMediaPlayer) + PreviewerViewModel(idsFile, currentIndex, cardMediaPlayer) } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/UniqueArrayList.kt b/AnkiDroid/src/main/java/com/ichi2/utils/UniqueArrayList.kt index fbfc4f4769b2..bb9ce8f07373 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/UniqueArrayList.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/UniqueArrayList.kt @@ -150,7 +150,7 @@ class UniqueArrayList /** * @param comparator used to judge uniqueness */ fun from( - source: List, + source: Collection, comparator: Comparator? = null, ): UniqueArrayList { val set: Set = diff --git a/AnkiDroid/src/main/res/layout/tags_dialog.xml b/AnkiDroid/src/main/res/layout/tags_dialog.xml index af27c3e46b76..8cc91277643a 100644 --- a/AnkiDroid/src/main/res/layout/tags_dialog.xml +++ b/AnkiDroid/src/main/res/layout/tags_dialog.xml @@ -37,13 +37,40 @@ android:text="@string/tags_dialog_option_due_cards"/> + + + + + + + + android:scrollbars="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_above="@id/tags_dialog_options_radiogroup" + android:layout_below="@id/tags_dialog_toolbar" + tools:listitem="@layout/tags_item_list_dialog" /> + app:popupTheme="@style/ActionBar.Popup" + app:menu="@menu/tags_dialog_menu"> diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt index 4f9a816871c0..45d91a017090 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/CardBrowserTest.kt @@ -525,7 +525,7 @@ class CardBrowserTest : RobolectricTest() { assertThat("before: index", previewIntent.currentIndex, equalTo(0)) assertThat( "before: cards", - previewIntent.previewerIdsFile.getCardIds(), + previewIntent.idsFile.getIds(), equalTo(listOf(cid1, cid2)), ) @@ -537,7 +537,7 @@ class CardBrowserTest : RobolectricTest() { assertThat("after: index", intentAfterReverse.currentIndex, equalTo(0)) assertThat( "after: cards", - intentAfterReverse.previewerIdsFile.getCardIds(), + intentAfterReverse.idsFile.getIds(), equalTo(listOf(cid2, cid1)), ) } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt index 86d3008e71f9..274a51605632 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/browser/CardBrowserViewModelTest.kt @@ -769,7 +769,7 @@ class CardBrowserViewModelTest : JvmTest() { val data = queryPreviewIntentData() assertThat(data.currentIndex, equalTo(0)) - data.previewerIdsFile.getCardIds().also { actualCardIds -> + data.idsFile.getIds().also { actualCardIds -> assertThat("previewing a note previews cards", actualCardIds, hasSize(5)) val firstCardIds = @@ -793,7 +793,7 @@ class CardBrowserViewModelTest : JvmTest() { runViewModelTest(notes = 2) { val data = queryPreviewIntentData() assertThat(data.currentIndex, equalTo(0)) - assertThat(data.previewerIdsFile.getCardIds(), hasSize(2)) + assertThat(data.idsFile.getIds(), hasSize(2)) } @Test @@ -802,7 +802,7 @@ class CardBrowserViewModelTest : JvmTest() { selectRowsWithPositions(0).also { val data = queryPreviewIntentData() assertThat(data.currentIndex, equalTo(0)) - assertThat(data.previewerIdsFile.getCardIds(), hasSize(2)) + assertThat(data.idsFile.getIds(), hasSize(2)) } selectNone() @@ -811,7 +811,7 @@ class CardBrowserViewModelTest : JvmTest() { selectRowsWithPositions(1).also { val data = queryPreviewIntentData() assertThat(data.currentIndex, equalTo(1)) - assertThat(data.previewerIdsFile.getCardIds(), hasSize(2)) + assertThat(data.idsFile.getIds(), hasSize(2)) } } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/tags/TagsDialogTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/tags/TagsDialogTest.kt index cd0cc79a3e12..2f54bbb71f13 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/tags/TagsDialogTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/dialogs/tags/TagsDialogTest.kt @@ -18,15 +18,12 @@ package com.ichi2.anki.dialogs.tags import android.os.Bundle import android.widget.EditText import androidx.appcompat.app.AlertDialog -import androidx.core.os.BundleCompat import androidx.fragment.app.testing.FragmentScenario import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.RecyclerView import androidx.test.ext.junit.runners.AndroidJUnit4 import com.ichi2.anki.R import com.ichi2.anki.RobolectricTest -import com.ichi2.anki.dialogs.utils.AnKingTags -import com.ichi2.testutils.HamcrestUtils.containsInAnyOrder import com.ichi2.testutils.ParametersUtils import com.ichi2.testutils.RecyclerViewUtils import com.ichi2.ui.CheckBoxTriStates @@ -47,7 +44,7 @@ class TagsDialogTest : RobolectricTest() { fun test_AddNewTag_shouldBeVisibleInRecyclerView_andSortedCorrectly() { val type = TagsDialog.DialogType.EDIT_TAGS val allTags = listOf("a", "b", "d", "e") - val checkedTags = listOf("a", "b") + val checkedTags = arrayListOf("a", "b") val args = TagsDialog(ParametersUtils.whatever()) .withTestArguments(type, checkedTags, allTags) @@ -81,7 +78,7 @@ class TagsDialogTest : RobolectricTest() { fun test_AddNewTag_existingTag_shouldBeSelectedAndSorted() { val type = TagsDialog.DialogType.EDIT_TAGS val allTags = listOf("a", "b", "d", "e") - val checkedTags = listOf("a", "b") + val checkedTags = arrayListOf("a", "b") val args = TagsDialog(ParametersUtils.whatever()) .withTestArguments(type, checkedTags, allTags) @@ -114,14 +111,13 @@ class TagsDialogTest : RobolectricTest() { fun test_checked_unchecked_indeterminate() { val type = TagsDialog.DialogType.EDIT_TAGS val expectedAllTags = listOf("a", "b", "d", "e") - val checkedTags = listOf("a", "b") - val uncheckedTags = listOf("b", "d") - val expectedCheckedTags = listOf("a") + val checkedTags = arrayListOf("a", "b") + val expectedCheckedTags = listOf("a", "b") val expectedUncheckedTags = listOf("d", "e") - val expectedIndeterminate = listOf("b") + val expectedIndeterminate = emptyList() val args = TagsDialog(ParametersUtils.whatever()) - .withTestArguments(type, checkedTags, uncheckedTags, expectedAllTags) + .withTestArguments(type, checkedTags, expectedAllTags) .requireArguments() val mockListener = Mockito.mock(TagsDialogListener::class.java) val factory = TagsDialogFactory(mockListener) @@ -170,7 +166,7 @@ class TagsDialogTest : RobolectricTest() { "book", ) val checkedTags = - listOf( + arrayListOf( "fruit::pear::big", "sport::tennis", ) @@ -218,7 +214,7 @@ class TagsDialogTest : RobolectricTest() { fun test_AddNewTag_newHierarchicalTag_pathToItShouldBeExpanded() { val type = TagsDialog.DialogType.EDIT_TAGS val allTags = listOf("common::speak", "common::speak::daily", "common::sport::tennis", "common::sport::football") - val checkedTags = listOf("common::speak::daily", "common::sport::tennis") + val checkedTags = arrayListOf("common::speak::daily", "common::sport::tennis") val args = TagsDialog(ParametersUtils.whatever()) .withTestArguments(type, checkedTags, allTags) @@ -271,7 +267,7 @@ class TagsDialogTest : RobolectricTest() { fun test_AddNewTag_newHierarchicalTag_willUniformHierarchicalTag() { val type = TagsDialog.DialogType.EDIT_TAGS val allTags = listOf("common") - val checkedTags = listOf("common") + val checkedTags = arrayListOf("common") val args = TagsDialog(ParametersUtils.whatever()) .withTestArguments(type, checkedTags, allTags) @@ -316,7 +312,7 @@ class TagsDialogTest : RobolectricTest() { "common::sport::football::small", ) val checkedTags = - listOf( + arrayListOf( "common::speak::tennis", "common::sport::tennis", "common::sport::football::small", @@ -360,7 +356,7 @@ class TagsDialogTest : RobolectricTest() { fun test_SearchTag_willInheritExpandState() { val type = TagsDialog.DialogType.FILTER_BY_TAG val allTags = listOf("common::speak", "common::sport::tennis") - val checkedTags = emptyList() + val checkedTags = arrayListOf() val args = TagsDialog(ParametersUtils.whatever()) .withTestArguments(type, checkedTags, allTags) @@ -407,7 +403,7 @@ class TagsDialogTest : RobolectricTest() { "common::sport::football", "common::sport::football::small", ) - val checkedTags = emptyList() + val checkedTags = arrayListOf() val args = TagsDialog(ParametersUtils.whatever()) .withTestArguments(type, checkedTags, allTags) @@ -524,7 +520,7 @@ class TagsDialogTest : RobolectricTest() { fun test_SearchTag_spaceWillBeFilteredCorrectly() { val type = TagsDialog.DialogType.FILTER_BY_TAG val allTags = listOf("hello::world") - val checkedTags = emptyList() + val checkedTags = arrayListOf() val args = TagsDialog(ParametersUtils.whatever()) .withTestArguments(type, checkedTags, allTags) @@ -568,7 +564,7 @@ class TagsDialogTest : RobolectricTest() { val args = TagsDialog(ParametersUtils.whatever()) - .withTestArguments(type, emptyList(), allTags) + .withTestArguments(type, arrayListOf(), allTags) .arguments val mockListener = Mockito.mock(TagsDialogListener::class.java) val factory = TagsDialogFactory(mockListener) @@ -578,61 +574,22 @@ class TagsDialogTest : RobolectricTest() { } } - @Test - fun `huge number of tags`() { - val type = TagsDialog.DialogType.FILTER_BY_TAG - val allTags = AnKingTags.value - - val args = - TagsDialog(ParametersUtils.whatever()) - .withTestArguments(type, emptyList(), allTags) - .arguments - val mockListener = Mockito.mock(TagsDialogListener::class.java) - val factory = TagsDialogFactory(mockListener) - FragmentScenario.launch(TagsDialog::class.java, args, R.style.Theme_Light, factory).use { scenario -> - scenario.moveToState(Lifecycle.State.STARTED) - scenario.onFragment { f -> - val tagsFile = - requireNotNull( - BundleCompat.getParcelable( - f.requireArguments(), - "tagsFile", - TagsFile::class.java, - ), - ) - - val dataFromArguments = tagsFile.getData() - - assertThat(dataFromArguments.allTags, containsInAnyOrder(allTags)) - } - } - } - // these are called 'withTestArguments' due to "extension is shadowed by a member" warnings // this is needed so we can pass in 'targetContext' for context.cacheDir private fun TagsDialog.withTestArguments( type: TagsDialog.DialogType, - checkedTags: List, - allTags: List, - ) = withArguments( - context = targetContext, - type = type, - checkedTags = checkedTags, - allTags = allTags, - ) - - private fun TagsDialog.withTestArguments( - type: TagsDialog.DialogType, - checkedTags: List, - uncheckedTags: List?, - allTags: List, - ) = withArguments( - context = targetContext, - type = type, - checkedTags = checkedTags, - uncheckedTags = uncheckedTags, - allTags = allTags, - ) + checkedTags: ArrayList, + allTags: Collection, + ): TagsDialog { + val note = col.newNote() + col.tags.bulkAdd(listOf(note.id), allTags.joinToString(separator = " ")) + col.addNote(note, 0L) + return withArguments( + context = targetContext, + type = type, + checkedTags = checkedTags, + ) + } private fun runTagsDialogScenario( args: Bundle, diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/previewer/PreviewerFragmentTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/previewer/PreviewerFragmentTest.kt index 65f93ea14ac0..d212e9632dbd 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/previewer/PreviewerFragmentTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/previewer/PreviewerFragmentTest.kt @@ -21,7 +21,7 @@ import androidx.test.core.app.ActivityScenario import androidx.test.espresso.Espresso import androidx.test.ext.junit.runners.AndroidJUnit4 import com.ichi2.anki.RobolectricTest -import com.ichi2.anki.browser.PreviewerIdsFile +import com.ichi2.anki.browser.IdsFile import com.ichi2.testutils.createTransientDirectory import org.hamcrest.MatcherAssert.assertThat import org.junit.Test @@ -36,7 +36,7 @@ class PreviewerFragmentTest : RobolectricTest() { val intent = PreviewerFragment.getIntent( targetContext, - previewerIdsFile = PreviewerIdsFile(createTransientDirectory(), note.cardIds(col)), + idsFile = IdsFile(createTransientDirectory(), note.cardIds(col)), currentIndex = 0, )