Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
cee8622
fix(shared-decks): return to the deck after an AnkiWeb login
criticalAY Oct 21, 2026
4467ae3
fix(shared-decks): update Back after the home button clears history
criticalAY Oct 26, 2026
9d3bc91
refactor(theme): move dark/black theme definitions to `:common:android`
david-allison Oct 26, 2026
554550a
refactor(theme): move light theme definition to `:common:android`
david-allison Oct 26, 2026
06b879b
refactor(theme): move plain theme definition to `:common:android`
david-allison Oct 26, 2026
e7527ad
refactor(theme): move e-ink theme definition to `:common:android`
david-allison Oct 26, 2026
cfa28e2
refactor(settings): move day/night theme enums to `:common:android`
david-allison Oct 26, 2026
c489157
fix(progress): observe on the fragment's lifecycle
criticalAY Oct 14, 2026
6ac61fa
fix(progress): share a loading dialog between screens
criticalAY Oct 15, 2026
04a4851
feat(study-options): show progress from the ViewModel
criticalAY Oct 14, 2026
5096fbd
fix(tests): keep LeakCanary off under Robolectric
criticalAY Oct 27, 2026
472b98b
refactor: move `ic_remove` to common:android
david-allison Oct 27, 2026
5f4c547
refactor: move `NonLeadingZeroInputFilter` to common:android
david-allison Oct 27, 2026
2f614ec
refactor: move padding helpers to `common:android`
david-allison Oct 27, 2026
c8c2d64
refactor: move `numberRangeTitle()` to `:common:android`
david-allison Oct 27, 2026
5068b77
refactor(compose): extract theme attributes to `:common:android`
david-allison Oct 27, 2026
af08e31
refactor(compose): extract theme/preview helpers to `:common:android`
david-allison Oct 27, 2026
4561666
refactor(note-editor): extract toolbar previews to `:note-editor`
david-allison Oct 27, 2026
ef558bc
test: null external storage without replacing ContextImpl shadow
mikehardy Oct 28, 2026
b0f7dc4
test(tags): Cover existing tags in TagsDialog.kt
Rizos-Vassili Oct 28, 2026
b4275ef
fix: use profile storage for IdsFile
david-allison Oct 5, 2026
141a131
fix: move IdsFile to `cacheDir/idsfiles`
david-allison Oct 5, 2026
aeea59b
fix(app): improve cache cleanup failure handling
david-allison Oct 23, 2026
4c0fe70
feat(app): clean up IdsFile on startup
david-allison Oct 5, 2026
81dbe2a
fix(browser): stop reporting missing IdsFile to ACRA
david-allison Oct 23, 2026
e78676b
refactor: use IdsFile.getIdsOrNull()
david-allison Oct 23, 2026
05e39c2
fix(server): validate content-length before allocating
BrayanDSO Oct 26, 2026
bbc41fa
fix(server): never report a failed request as 200 OK
BrayanDSO Oct 28, 2026
cd96e91
fix(server): reject unhandled methods with 405
BrayanDSO Oct 28, 2026
c70d926
refactor(server): send fixed-length responses
BrayanDSO Oct 28, 2026
38dc16d
fix(compose): make popup menus opaque
david-allison Oct 24, 2026
9369f3a
refactor(compose): inline `TextInputDialogCard`
david-allison Oct 24, 2026
463ca5b
fix(compose): maintain text dialog cursor and selection
david-allison Oct 24, 2026
76ded7f
feat(input-dialog): support input transformations
david-allison Oct 24, 2026
a7b3d52
feat(input-dialog): support helper text
david-allison Oct 24, 2026
4702bed
feat(input-dialog): support 2-line descriptions
david-allison Oct 24, 2026
203a8f2
feat(input-dialog): support validating text submission
david-allison Oct 24, 2026
ba8d871
feat(input-dialog): support 'enabled/disabled' state
david-allison Oct 24, 2026
17dd305
fix(input-dialog): make labels accessible
david-allison Oct 24, 2026
1fb9049
fix(input-dialog): ensure buttons are visible
david-allison Oct 24, 2026
8bc4866
fix(dialogs): remove motion in animations
david-allison Oct 25, 2026
ec88885
fix(tags): preserve casing in row equality for renames
david-allison Oct 24, 2026
b043592
fix(tags): searches no longer expand subtrees towards leaves
david-allison Oct 25, 2026
7a96e24
feat(tags): optimistic updates of 'collapsed' state
david-allison Oct 24, 2026
5ced01e
fix(tags): better handle 'collapse/expand' during search
david-allison Oct 27, 2026
47010a8
fix(browser): ANR on saved saved search previews
david-allison Oct 27, 2026
d06fb5d
build: bump backend to 0.1.74-anki26.09.3
david-allison Oct 28, 2026
2a3381f
fix(tags): workaround: report tag moves which are no-ops
david-allison Oct 24, 2026
1cd42b5
fix(browser): use simple line-break mode to fix ANR
david-allison Oct 27, 2026
9bf725b
test(tags): cover clearing the duplicate tag error
Rizos-Vassili Oct 28, 2026
d8c1115
Merge remote-tracking branch 'origin/main' into merge-upstream-2026-1…
float3 Oct 28, 2026
897cc36
Take the theme styles from :common:android in tests
float3 Oct 29, 2026
ba2bf09
Let a leftover poll read the profile in the private access test
float3 Oct 29, 2026
91ce3cc
Wrap the heatmap render test's themed context
float3 Oct 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.R
import com.ichi2.anki.dialogs.NoteTypeFieldEditorContextMenu.NoteTypeFieldEditorContextMenuAction
import com.ichi2.anki.tests.InstrumentedTest
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import com.ichi2.anki.common.android.R as CommonR

/** Tests [NoteTypeFieldEditorContextMenu] */
@RunWith(AndroidJUnit4::class)
Expand All @@ -27,7 +27,7 @@ class NoteTypeFieldEditorContextMenuTest : InstrumentedTest() {
fun showsAllOptions() {
launchFragment(
fragmentArgs = Bundle().apply { putString(NoteTypeFieldEditorContextMenu.ARG_LABEL, testDialogTitle) },
themeResId = R.style.Theme_Light,
themeResId = CommonR.style.Theme_Light,
) { NoteTypeFieldEditorContextMenu() }
onView(withText(testDialogTitle))
.inRoot(isDialog())
Expand Down
31 changes: 24 additions & 7 deletions AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.ichi2.anki.AnkiDroidApp.Companion.sharedPreferencesTestingOverride
import com.ichi2.anki.analytics.initializeAnalytics
import com.ichi2.anki.analytics.launchPreviousAnrReporting
import com.ichi2.anki.ankiquest.Ankiquest
import com.ichi2.anki.browser.IdsFile
import com.ichi2.anki.browser.SharedPreferencesLastDeckIdRepository
import com.ichi2.anki.common.android.AdaptionUtil
import com.ichi2.anki.common.android.Animations
Expand All @@ -41,6 +42,7 @@ import com.ichi2.anki.common.preferences.sharedPrefs
import com.ichi2.anki.common.storage.CollectionHelper
import com.ichi2.anki.common.storage.StorageDecision
import com.ichi2.anki.common.utils.android.SdCard
import com.ichi2.anki.common.utils.android.isRobolectric
import com.ichi2.anki.common.utils.android.showThemedToast
import com.ichi2.anki.common.utils.annotation.KotlinCleanup
import com.ichi2.anki.compat.CompatHelper
Expand All @@ -56,6 +58,7 @@ import com.ichi2.anki.logging.RobolectricDebugTree
import com.ichi2.anki.logging.logActivityCreation
import com.ichi2.anki.model.FieldFilters.NoSuggestFilter
import com.ichi2.anki.multimedia.MultimediaArgsStorage
import com.ichi2.anki.multiprofile.ProfileContextWrapper
import com.ichi2.anki.multiprofile.ProfileManager
import com.ichi2.anki.multiprofile.isPhoenixProcess
import com.ichi2.anki.navigation.initializeNavigator
Expand Down Expand Up @@ -137,7 +140,7 @@ open class AnkiDroidApp :
@set:VisibleForTesting
var profileManager: ProfileManager? = null

private val profileContext: Context?
private val profileContext: ProfileContextWrapper?
get() = profileManager?.activeProfileContext

/**
Expand Down Expand Up @@ -206,7 +209,7 @@ open class AnkiDroidApp :
LogType.PRODUCTION -> Timber.plant(ProductionCrashReportingTree())
}
Timber.plant(ReminderLogTree(this))
if (BuildConfig.ENABLE_LEAK_CANARY) {
if (BuildConfig.ENABLE_LEAK_CANARY && !isRobolectric) {
LeakCanaryConfiguration.setInitialConfigFor(this)
} else {
LeakCanaryConfiguration.disable()
Expand Down Expand Up @@ -280,17 +283,31 @@ open class AnkiDroidApp :
super.onTerminate()
}

/** Schedules cache maintenance and logs its duration without delaying startup. */
private fun launchCacheCleanup() =
/** Schedules temporary-file maintenance and logs its duration without delaying startup. */
@VisibleForTesting
internal fun launchCacheCleanup() =
applicationScope.launch(Dispatchers.IO) {
runCatching {
measureTime("cacheCleanup") {
measureTime("cacheCleanup") {
runCacheCleanup("multimediaArgsCleanup") {
MultimediaArgsStorage.create(this@AnkiDroidApp).removeExpiredFiles()
}
runCacheCleanup("voiceRecordingsCleanup") {
VoiceRecorder.removeExpiredRecordings(VoiceRecorder.recordingsDirectory(this@AnkiDroidApp))
}
}.onFailure { Timber.w(it, "Unable to clean up expired cache files") }
runCacheCleanup("idsFileCleanup") {
IdsFile.deleteStaleFiles(this@AnkiDroidApp)
}
}
}

private inline fun runCacheCleanup(
name: String,
block: () -> Unit,
) {
runCatching { measureTime(name, block) }
.onFailure { Timber.w(it, "Unable to run %s", name) }
}

/**
* @param debugTraceSqlCalls Log all SQL statements executed by the backend.
* **Warning** The log may be delayed by 100ms, so you should not assume than a given SQL
Expand Down
2 changes: 1 addition & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ open class CardBrowser :
viewModelStore,
CardBrowserViewModel.factory(
lastDeckIdRepository = AnkiDroidApp.instance.sharedPrefsLastDeckIdRepository,
idsFileDirectory = IdsFile.getDirectory(this),
idsFileDirectory = IdsFile.getDirectory(),
options = launchOptions,
isFragmented = fragmented,
initialDeckId = intent?.getLongExtra(CardBrowserViewModel.EXTRA_DECK_ID),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ private fun ensureBrowserViewModel() {
deckPicker.viewModelStore,
CardBrowserViewModel.factory(
lastDeckIdRepository = AnkiDroidApp.instance.sharedPrefsLastDeckIdRepository,
idsFileDirectory = IdsFile.getDirectory(deckPicker),
idsFileDirectory = IdsFile.getDirectory(),
options = null,
isFragmented = false,
initialDeckId = null,
Expand Down
23 changes: 13 additions & 10 deletions AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.widget.ScrollView
import android.widget.TextView
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.Group
import androidx.core.text.HtmlCompat
Expand Down Expand Up @@ -48,6 +49,7 @@ import com.ichi2.anki.filtered.FilteredDeckOptionsFragment
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.libanki.Decks
import com.ichi2.anki.observability.ChangeManager
import com.ichi2.anki.progress.observeProgress
import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.ui.internationalization.sentenceCase
import com.ichi2.anki.utils.doOnApplyWindowInsets
Expand Down Expand Up @@ -114,6 +116,7 @@ class StudyOptionsFragment :
displayedDeckId = savedInstanceState?.getLong(STATE_DISPLAYED_DECK_ID)?.takeIf { it != 0L }
// Subscribe once per fragment: returning from another panel can recreate its view.
ChangeManager.subscribe(this)
observeProgress(viewModel) { progress -> getString(progress.messageRes) }
}

override fun onCreateView(
Expand Down Expand Up @@ -289,20 +292,12 @@ class StudyOptionsFragment :
}
R.id.action_rebuild -> {
Timber.i("StudyOptionsFragment:: rebuild cram deck button pressed")
launchCatchingTask {
withProgress(CommonString.rebuild_filtered_deck) {
viewModel.rebuildCram()
}
}
launchCatchingTask { viewModel.rebuildCram() }
return true
}
R.id.action_empty -> {
Timber.i("StudyOptionsFragment:: empty cram deck button pressed")
launchCatchingTask {
withProgress(CommonString.empty_filtered_deck) {
viewModel.emptyCram()
}
}
launchCatchingTask { viewModel.emptyCram() }
return true
}
else -> return false
Expand Down Expand Up @@ -571,3 +566,11 @@ class StudyOptionsFragment :
}
}
}

@get:StringRes
private val StudyOptionsProgress.messageRes: Int
get() =
when (this) {
StudyOptionsProgress.REBUILDING_FILTERED_DECK -> CommonString.rebuild_filtered_deck
StudyOptionsProgress.EMPTYING_FILTERED_DECK -> CommonString.empty_filtered_deck
}
25 changes: 20 additions & 5 deletions AnkiDroid/src/main/java/com/ichi2/anki/StudyOptionsViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import com.ichi2.anki.CollectionManager.withOpenColOrNull
import com.ichi2.anki.libanki.Collection
import com.ichi2.anki.libanki.DeckId
import com.ichi2.anki.observability.undoableOp
import com.ichi2.anki.progress.HasProgress
import com.ichi2.anki.progress.ProgressManager
import com.ichi2.anki.study.DeckKind
import com.ichi2.anki.study.StudyAvailability
import com.ichi2.anki.study.currentDeckStudyState
Expand All @@ -20,7 +22,11 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File

class StudyOptionsViewModel : ViewModel() {
class StudyOptionsViewModel :
ViewModel(),
HasProgress<StudyOptionsProgress> {
override val progressManager = ProgressManager<StudyOptionsProgress>()

val flowOfState: StateFlow<StudyOptionsState>
field = MutableStateFlow<StudyOptionsState>(StudyOptionsState.Loading)
val state: StudyOptionsState get() = flowOfState.value
Expand Down Expand Up @@ -52,14 +58,18 @@ class StudyOptionsViewModel : ViewModel() {

suspend fun rebuildCram() {
Timber.d("doInBackground - RebuildCram")
undoableOp { sched.rebuildFilteredDeck(decks.selected()) }
withCol { updateStateFromCollection() }
progressManager.withProgress(message = StudyOptionsProgress.REBUILDING_FILTERED_DECK) {
undoableOp { sched.rebuildFilteredDeck(decks.selected()) }
withCol { updateStateFromCollection() }
}
}

suspend fun emptyCram() {
Timber.d("doInBackgroundEmptyCram")
undoableOp { sched.emptyFilteredDeck(decks.selected()) }
withCol { updateStateFromCollection() }
progressManager.withProgress(message = StudyOptionsProgress.EMPTYING_FILTERED_DECK) {
undoableOp { sched.emptyFilteredDeck(decks.selected()) }
withCol { updateStateFromCollection() }
}
}

fun unbury(): Job =
Expand Down Expand Up @@ -138,6 +148,11 @@ class StudyOptionsViewModel : ViewModel() {
}
}

enum class StudyOptionsProgress {
REBUILDING_FILTERED_DECK,
EMPTYING_FILTERED_DECK,
}

class DeckStudyData(
val newCardsToday: Int,
val lrnCardsToday: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class BrowserColumnSelectionAdapter(
binding.buttonToggleColumn.apply {
// NICE_TO_HAVE: animate between + and -
val isExclude = absoluteAdapterPosition < positionOfAvailableHeading
setImageResource(if (isExclude) R.drawable.ic_remove else CommonR.drawable.ic_add)
setImageResource(if (isExclude) CommonR.drawable.ic_remove else CommonR.drawable.ic_add)

val label = context.getString(if (isExclude) CommonString.exclude_column else CommonString.include_column)
contentDescription = label
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,7 @@ class CardBrowserFragment :
val (type, selectedIds) = activityViewModel.querySelectionExportData() ?: return
ExportDialogFragment
.newInstance(
IdsFile.getDirectory(requireContext()),
IdsFile.getDirectory(),
type,
selectedIds,
).show(parentFragmentManager, "exportDialog")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,13 +556,12 @@ class CardBrowserViewModel(
multiselectStateFile = idsFile
pendingSelectionRestore =
try {
idsFile?.getIds()?.map { CardOrNoteId(it) }
idsFile?.getIdsOrNull()?.map { CardOrNoteId(it) }
} catch (e: Exception) {
// #19572: I suspect we have a startup bug here, so continue reporting the exception
Timber.w(e, "failed to read STATE_MULTISELECT_VALUES")
CrashReportService.sendExceptionReport(
e = e,
origin = "19572: STATE_MULTISELECT_VALUES",
origin = "CardBrowserViewModel",
onlyIfSilent = true,
)
// fallback to no selections, but still in multiselect mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,8 @@ class FindAndReplaceDialogFragment : AnalyticsDialogFragment() {
(dialog as? AlertDialog)?.positiveButton?.isEnabled = false
binding.contentViewsGroup.isVisible = false
binding.loadingViewsGroup.isVisible = true
val fetchNoteIdsResult = runCatching { idsFile.getIds() }
val noteIds = fetchNoteIdsResult.getOrNull()
if (fetchNoteIdsResult.isFailure || noteIds == null) {
val noteIds = runCatching { idsFile.getIdsOrNull() }.getOrNull()
if (noteIds == null) {
requireActivity().showSnackbar(CommonString.something_wrong)
dismiss()
return@launch
Expand Down Expand Up @@ -190,7 +189,7 @@ class FindAndReplaceDialogFragment : AnalyticsDialogFragment() {
context: Context,
noteIds: List<NoteId>,
): FindAndReplaceDialogFragment {
val file = IdsFile(IdsFile.getDirectory(context), noteIds, IdsFile.Purpose.FIND_AND_REPLACE)
val file = IdsFile(IdsFile.getDirectory(), noteIds, IdsFile.Purpose.FIND_AND_REPLACE)
return FindAndReplaceDialogFragment().apply {
arguments =
Bundle().apply {
Expand Down
Loading
Loading