Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -24,7 +24,7 @@ import com.ichi2.anki.NoteEditorFragment.Companion.intentLaunchedWithImage
import com.ichi2.anki.noteeditor.NoteEditorLauncher
import com.ichi2.anki.tests.InstrumentedTest
import com.ichi2.anki.testutil.GrantStoragePermission
import com.ichi2.anki.testutil.getEditor
import com.ichi2.anki.testutil.getNoteEditorFragment
import com.ichi2.testutils.common.Flaky
import com.ichi2.testutils.common.OS
import com.ichi2.utils.AssetHelper.TEXT_PLAIN
Expand All @@ -42,7 +42,7 @@ class NoteEditorIntentTest : InstrumentedTest() {
var runtimePermissionRule: TestRule? = GrantStoragePermission.instance

@get:Rule
var activityRuleIntent: ActivityScenarioRule<SingleFragmentActivity>? =
var activityRuleIntent: ActivityScenarioRule<NoteEditorActivity>? =
ActivityScenarioRule(
noteEditorTextIntent,
)
Expand All @@ -56,7 +56,7 @@ class NoteEditorIntentTest : InstrumentedTest() {

var currentFieldStrings: String? = null
scenario.onActivity { activity ->
val editor = activity.getEditor()
val editor = activity.getNoteEditorFragment()
currentFieldStrings = editor.currentFieldStrings[0]
}
MatcherAssert.assertThat(currentFieldStrings!!, Matchers.equalTo("sample text"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ abstract class NoteEditorTest protected constructor() {
.takeUnless { isInvalid }

@get:Rule
var activityRule: ActivityScenarioRule<SingleFragmentActivity>? =
ActivityScenarioRule<SingleFragmentActivity>(
var activityRule: ActivityScenarioRule<NoteEditorActivity>? =
ActivityScenarioRule<NoteEditorActivity>(
noteEditorIntent,
).takeUnless { isInvalid }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
package com.ichi2.anki.testutil

import androidx.test.core.app.ActivityScenario
import com.ichi2.anki.NoteEditorActivity
import com.ichi2.anki.NoteEditorFragment
import com.ichi2.anki.R
import com.ichi2.anki.SingleFragmentActivity
import java.util.concurrent.atomic.AtomicReference

/**
Expand All @@ -28,11 +28,11 @@ import java.util.concurrent.atomic.AtomicReference
* @throws Throwable if any exception is thrown during the execution of the block.
*/
@Throws(Throwable::class)
fun ActivityScenario<SingleFragmentActivity>.onNoteEditor(block: (NoteEditorFragment) -> Unit) {
fun ActivityScenario<NoteEditorActivity>.onNoteEditor(block: (NoteEditorFragment) -> Unit) {
val wrapped = AtomicReference<Throwable?>(null)
this.onActivity { activity: SingleFragmentActivity ->
this.onActivity { activity: NoteEditorActivity ->
try {
val editor = activity.getEditor()
val editor: NoteEditorFragment = activity.getNoteEditorFragment()
activity.runOnUiThread {
try {
block(editor)
Expand All @@ -47,8 +47,5 @@ fun ActivityScenario<SingleFragmentActivity>.onNoteEditor(block: (NoteEditorFrag
wrapped.get()?.let { throw it }
}

/**
* Extension function for SingleFragmentActivity to find the NoteEditor fragment
*/
fun SingleFragmentActivity.getEditor(): NoteEditorFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NoteEditorFragment
fun NoteEditorActivity.getNoteEditorFragment(): NoteEditorFragment =
supportFragmentManager.findFragmentById(R.id.note_editor_fragment_frame) as NoteEditorFragment
6 changes: 6 additions & 0 deletions AnkiDroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,12 @@
android:exported="false"
android:configChanges="orientation|screenSize"
/>
<activity
android:name="com.ichi2.anki.NoteEditorActivity"
android:exported="false"
android:configChanges="keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize"
/>
<activity
android:name="com.ichi2.anki.previewer.CardViewerActivity"
android:exported="false"
Expand Down
162 changes: 162 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright (c) 2025 Hari Srinivasan <harisrini21@gmail.com>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.anki

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import com.ichi2.anki.android.input.ShortcutGroup
import com.ichi2.anki.android.input.ShortcutGroupProvider
import com.ichi2.anki.libanki.Collection
import com.ichi2.anki.noteeditor.NoteEditorLauncher
import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider
import com.ichi2.anki.snackbar.SnackbarBuilder
import timber.log.Timber
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName

/**
* To find the actual note Editor, @see [NoteEditorFragment]
* This activity contains the NoteEditorFragment, and, on x-large screens, the previewer fragment.
* It also ensures that changes in the note are transmitted to the previewer
*/

// TODO: Move intent handling to [NoteEditorActivity] from [NoteEditorFragment]
class NoteEditorActivity :
AnkiActivity(),
BaseSnackbarBuilderProvider,
DispatchKeyEventListener,
ShortcutGroupProvider {
override val baseSnackbarBuilder: SnackbarBuilder = { }

lateinit var noteEditorFragment: NoteEditorFragment

override fun onCreate(savedInstanceState: Bundle?) {
if (showedActivityFailedScreen(savedInstanceState)) {
return
}
super.onCreate(savedInstanceState)
if (!ensureStoragePermissions()) {
return
}

setContentView(R.layout.note_editor)

/**
* The [NoteEditorActivity] activity supports multiple note editing workflows using fragments.
* It dynamically chooses the appropriate fragment to load and the arguments to pass to it,
* based on intent extras provided at launch time.
*
* - [FRAGMENT_NAME_EXTRA]: Fully qualified name of the fragment class to instantiate.
* If set to [NoteEditorFragment], the activity initializes it with the arguments in
* [FRAGMENT_ARGS_EXTRA].
*
* - [FRAGMENT_ARGS_EXTRA]: Bundle containing parameters for the fragment (e.g. note ID,
* deck ID, etc.). Used to populate fields or determine editor behavior.
*
* This logic is encapsulated in the [launcher] assignment, which selects the correct
* fragment mode (e.g. add note, edit note) based on intent contents.
*/
val launcher =
if (intent.hasExtra(FRAGMENT_NAME_EXTRA)) {
val fragmentClassName = intent.getStringExtra(FRAGMENT_NAME_EXTRA)
if (fragmentClassName == NoteEditorFragment::class.java.name) {
val fragmentArgs = intent.getBundleExtra(FRAGMENT_ARGS_EXTRA)
if (fragmentArgs != null) {
NoteEditorLauncher.PassArguments(fragmentArgs)
} else {
NoteEditorLauncher.AddNote()
}
} else {
NoteEditorLauncher.AddNote()
}
} else {
// Regular NoteEditor intent handling
intent.getBundleExtra(FRAGMENT_ARGS_EXTRA)?.let { fragmentArgs ->
// If FRAGMENT_ARGS_EXTRA is provided, use it directly
NoteEditorLauncher.PassArguments(fragmentArgs)
} ?: intent.extras?.let { bundle ->
// Check if the bundle contains FRAGMENT_ARGS_EXTRA (for launchers that wrap their args)
bundle.getBundle(FRAGMENT_ARGS_EXTRA)?.let { wrappedFragmentArgs ->
NoteEditorLauncher.PassArguments(wrappedFragmentArgs)
} ?: NoteEditorLauncher.PassArguments(bundle)
} ?: NoteEditorLauncher.AddNote()
}

val existingFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG)

if (existingFragment == null) {
supportFragmentManager.commit {
replace(R.id.note_editor_fragment_frame, NoteEditorFragment.newInstance(launcher), FRAGMENT_TAG)
setReorderingAllowed(true)
/**
* Initializes the noteEditorFragment reference only after the transaction is committed.
* This ensures the fragment is fully created and available in the activity before
* any code attempts to interact with it, preventing potential null reference issues.
*/
runOnCommit {
noteEditorFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as NoteEditorFragment
}
}
} else {
noteEditorFragment = existingFragment as NoteEditorFragment
}

startLoadingCollection()
}

override fun onCollectionLoaded(col: Collection) {
super.onCollectionLoaded(col)
Timber.d("onCollectionLoaded()")
registerReceiver()
}

override fun dispatchKeyEvent(event: android.view.KeyEvent): Boolean =
noteEditorFragment.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)

override val shortcuts: ShortcutGroup
get() = noteEditorFragment.shortcuts

companion object {
const val FRAGMENT_ARGS_EXTRA = "fragmentArgs"
const val FRAGMENT_NAME_EXTRA = "fragmentName"
const val FRAGMENT_TAG = "NoteEditorFragmentTag"

/**
* Creates an Intent to launch the NoteEditor activity with a specific fragment class and arguments.
*
* @param context The context from which the intent will be launched
* @param fragmentClass The Kotlin class of the Fragment to instantiate
* @param arguments Optional bundle of arguments to pass to the fragment
* @param intentAction Optional action to set on the intent
* @return An Intent configured to launch NoteEditor with the specified fragment
*/
fun getIntent(
context: Context,
fragmentClass: KClass<out Fragment>,
arguments: Bundle? = null,
intentAction: String? = null,
): Intent =
Intent(context, NoteEditorActivity::class.java).apply {
putExtra(FRAGMENT_NAME_EXTRA, fragmentClass.jvmName)
putExtra(FRAGMENT_ARGS_EXTRA, arguments)
action = intentAction
}
}
}
15 changes: 9 additions & 6 deletions AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,15 @@ import androidx.core.os.BundleCompat
import androidx.core.text.HtmlCompat
import androidx.core.util.component1
import androidx.core.util.component2
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.core.view.OnReceiveContentListener
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.draganddrop.DropHelper
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import anki.config.ConfigKey
import anki.notes.NoteFieldsCheckResponse
Expand Down Expand Up @@ -152,7 +154,6 @@ import com.ichi2.anki.ui.setupNoteTypeSpinner
import com.ichi2.anki.utils.ext.sharedPrefs
import com.ichi2.anki.utils.ext.showDialogFragment
import com.ichi2.anki.utils.ext.window
import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener
import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat
import com.ichi2.compat.setTooltipTextCompat
import com.ichi2.imagecropper.ImageCropper
Expand Down Expand Up @@ -210,7 +211,6 @@ const val CALLER_KEY = "caller"
class NoteEditorFragment :
Fragment(R.layout.note_editor_fragment),
DeckSelectionListener,
SubtitleListener,
TagsDialogListener,
BaseSnackbarBuilderProvider,
DispatchKeyEventListener,
Expand All @@ -227,7 +227,7 @@ class NoteEditorFragment :
get() = CollectionManager.getColUnsafe()

private val mainToolbar: androidx.appcompat.widget.Toolbar
get() = requireView().findViewById(R.id.toolbar)
get() = requireAnkiActivity().findViewById(R.id.toolbar)

/**
* Flag which forces the calling activity to rebuild it's definition of current card from scratch
Expand Down Expand Up @@ -441,9 +441,6 @@ class NoteEditorFragment :
}
}

override val subtitleText: String
get() = ""

private enum class AddClozeType {
SAME_NUMBER,
INCREMENT_NUMBER,
Expand Down Expand Up @@ -579,7 +576,13 @@ class NoteEditorFragment :
requireActivity().onBackPressedDispatcher.onBackPressed()
}

// Register this fragment as a menu provider with the activity
mainToolbar.addMenuProvider(this)
(requireActivity() as MenuHost).addMenuProvider(
this,
viewLifecycleOwner,
Lifecycle.State.RESUMED,
)
}

/**
Expand Down
Loading