Skip to content

Commit ec8e779

Browse files
committed
Introduce NoteEditorActivity to host the NoteEditorFragment
1 parent a4de2fa commit ec8e779

File tree

15 files changed

+346
-161
lines changed

15 files changed

+346
-161
lines changed

AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorIntentTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class NoteEditorIntentTest : InstrumentedTest() {
4242
var runtimePermissionRule: TestRule? = GrantStoragePermission.instance
4343

4444
@get:Rule
45-
var activityRuleIntent: ActivityScenarioRule<SingleFragmentActivity>? =
45+
var activityRuleIntent: ActivityScenarioRule<NoteEditorActivity>? =
4646
ActivityScenarioRule(
4747
noteEditorTextIntent,
4848
)
@@ -57,7 +57,7 @@ class NoteEditorIntentTest : InstrumentedTest() {
5757
var currentFieldStrings: String? = null
5858
scenario.onActivity { activity ->
5959
val editor = activity.getEditor()
60-
currentFieldStrings = editor.currentFieldStrings[0]
60+
currentFieldStrings = editor!!.currentFieldStrings[0]
6161
}
6262
MatcherAssert.assertThat(currentFieldStrings!!, Matchers.equalTo("sample text"))
6363
}

AnkiDroid/src/androidTest/java/com/ichi2/anki/NoteEditorTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ abstract class NoteEditorTest protected constructor() {
4545
.takeUnless { isInvalid }
4646

4747
@get:Rule
48-
var activityRule: ActivityScenarioRule<SingleFragmentActivity>? =
49-
ActivityScenarioRule<SingleFragmentActivity>(
48+
var activityRule: ActivityScenarioRule<NoteEditorActivity>? =
49+
ActivityScenarioRule<NoteEditorActivity>(
5050
noteEditorIntent,
5151
).takeUnless { isInvalid }
5252

AnkiDroid/src/androidTest/java/com/ichi2/anki/testutil/NoteEditorFragment.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
package com.ichi2.anki.testutil
1818

1919
import androidx.test.core.app.ActivityScenario
20+
import com.ichi2.anki.NoteEditorActivity
2021
import com.ichi2.anki.NoteEditorFragment
2122
import com.ichi2.anki.R
22-
import com.ichi2.anki.SingleFragmentActivity
2323
import java.util.concurrent.atomic.AtomicReference
2424

2525
/**
@@ -28,11 +28,11 @@ import java.util.concurrent.atomic.AtomicReference
2828
* @throws Throwable if any exception is thrown during the execution of the block.
2929
*/
3030
@Throws(Throwable::class)
31-
fun ActivityScenario<SingleFragmentActivity>.onNoteEditor(block: (NoteEditorFragment) -> Unit) {
31+
fun ActivityScenario<NoteEditorActivity>.onNoteEditor(block: (NoteEditorFragment) -> Unit) {
3232
val wrapped = AtomicReference<Throwable?>(null)
33-
this.onActivity { activity: SingleFragmentActivity ->
33+
this.onActivity { activity: NoteEditorActivity ->
3434
try {
35-
val editor = activity.getEditor()
35+
val editor: NoteEditorFragment = activity.getEditor()!!
3636
activity.runOnUiThread {
3737
try {
3838
block(editor)
@@ -47,8 +47,5 @@ fun ActivityScenario<SingleFragmentActivity>.onNoteEditor(block: (NoteEditorFrag
4747
wrapped.get()?.let { throw it }
4848
}
4949

50-
/**
51-
* Extension function for SingleFragmentActivity to find the NoteEditor fragment
52-
*/
53-
fun SingleFragmentActivity.getEditor(): NoteEditorFragment =
54-
supportFragmentManager.findFragmentById(R.id.fragment_container) as NoteEditorFragment
50+
fun NoteEditorActivity.getEditor(): NoteEditorFragment? =
51+
supportFragmentManager.findFragmentById(R.id.note_editor_fragment_frame) as NoteEditorFragment

AnkiDroid/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,12 @@
452452
android:exported="false"
453453
android:configChanges="orientation|screenSize"
454454
/>
455+
<activity
456+
android:name="com.ichi2.anki.NoteEditorActivity"
457+
android:exported="false"
458+
android:configChanges="keyboardHidden|orientation|screenSize"
459+
android:windowSoftInputMode="stateAlwaysHidden|adjustResize"
460+
/>
455461
<activity
456462
android:name="com.ichi2.anki.previewer.CardViewerActivity"
457463
android:exported="false"
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright (c) 2025 Hari Srinivasan <harisrini21@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki
18+
19+
import android.content.Context
20+
import android.content.Intent
21+
import android.os.Bundle
22+
import androidx.fragment.app.Fragment
23+
import androidx.fragment.app.commit
24+
import com.ichi2.anki.android.input.ShortcutGroup
25+
import com.ichi2.anki.android.input.ShortcutGroupProvider
26+
import com.ichi2.anki.common.annotations.NeedsTest
27+
import com.ichi2.anki.dialogs.DeckSelectionDialog
28+
import com.ichi2.anki.dialogs.DeckSelectionDialog.DeckSelectionListener
29+
import com.ichi2.anki.dialogs.tags.TagsDialogListener
30+
import com.ichi2.anki.libanki.Collection
31+
import com.ichi2.anki.model.CardStateFilter
32+
import com.ichi2.anki.noteeditor.NoteEditorLauncher
33+
import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider
34+
import com.ichi2.anki.snackbar.SnackbarBuilder
35+
import com.ichi2.anki.widgets.DeckDropDownAdapter.SubtitleListener
36+
import timber.log.Timber
37+
import kotlin.reflect.KClass
38+
import kotlin.reflect.jvm.jvmName
39+
40+
/**
41+
* To find the actual note Editor, @see [NoteEditorFragment]
42+
* This activity contains the NoteEditorFragment, and, on x-large screens, the previewer fragment.
43+
* It also ensures that changes in the note are transmitted to the previewer
44+
*/
45+
46+
@NeedsTest("TODO: Add tests for NoteEditorActivity")
47+
class NoteEditorActivity :
48+
AnkiActivity(),
49+
DeckSelectionListener,
50+
SubtitleListener,
51+
TagsDialogListener,
52+
BaseSnackbarBuilderProvider,
53+
DispatchKeyEventListener,
54+
ShortcutGroupProvider {
55+
override val baseSnackbarBuilder: SnackbarBuilder = { }
56+
57+
override fun onCreate(savedInstanceState: Bundle?) {
58+
if (showedActivityFailedScreen(savedInstanceState)) {
59+
return
60+
}
61+
super.onCreate(savedInstanceState)
62+
if (!ensureStoragePermissions()) {
63+
return
64+
}
65+
66+
setContentView(R.layout.note_editor)
67+
68+
// Create and launch the NoteEditorFragment based on the launcher intent
69+
val launcher =
70+
if (intent.hasExtra(FRAGMENT_NAME_EXTRA)) {
71+
val fragmentClassName = intent.getStringExtra(FRAGMENT_NAME_EXTRA)
72+
if (fragmentClassName == NoteEditorFragment::class.java.name) {
73+
val fragmentArgs = intent.getBundleExtra(FRAGMENT_ARGS_EXTRA)
74+
if (fragmentArgs != null) {
75+
NoteEditorLauncher.PassArguments(fragmentArgs)
76+
} else {
77+
NoteEditorLauncher.AddNote()
78+
}
79+
} else {
80+
NoteEditorLauncher.AddNote()
81+
}
82+
} else {
83+
// Regular NoteEditorActivity intent handling
84+
intent.getBundleExtra(FRAGMENT_ARGS_EXTRA)?.let { fragmentArgs ->
85+
// If FRAGMENT_ARGS_EXTRA is provided, use it directly
86+
NoteEditorLauncher.PassArguments(fragmentArgs)
87+
} ?: intent.extras?.let { bundle ->
88+
// Check if the bundle contains FRAGMENT_ARGS_EXTRA (for launchers that wrap their args)
89+
bundle.getBundle(FRAGMENT_ARGS_EXTRA)?.let { wrappedFragmentArgs ->
90+
NoteEditorLauncher.PassArguments(wrappedFragmentArgs)
91+
} ?: NoteEditorLauncher.PassArguments(bundle)
92+
} ?: NoteEditorLauncher.AddNote()
93+
}
94+
95+
supportFragmentManager.commit {
96+
replace(R.id.note_editor_fragment_frame, NoteEditorFragment.newInstance(launcher), FRAGMENT_TAG)
97+
}
98+
99+
startLoadingCollection()
100+
}
101+
102+
/**
103+
* Retrieves the [NoteEditorFragment]
104+
*/
105+
val noteEditorFragment: NoteEditorFragment?
106+
get() = supportFragmentManager.findFragmentById(R.id.note_editor_fragment_frame) as? NoteEditorFragment
107+
108+
override fun onCollectionLoaded(col: Collection) {
109+
super.onCollectionLoaded(col)
110+
Timber.d("onCollectionLoaded()")
111+
registerReceiver()
112+
}
113+
114+
override fun onDeckSelected(deck: DeckSelectionDialog.SelectableDeck?) {
115+
noteEditorFragment?.onDeckSelected(deck)
116+
}
117+
118+
override val subtitleText: String
119+
get() = noteEditorFragment?.subtitleText ?: ""
120+
121+
override fun onSelectedTags(
122+
selectedTags: List<String>,
123+
indeterminateTags: List<String>,
124+
stateFilter: CardStateFilter,
125+
) {
126+
noteEditorFragment?.onSelectedTags(selectedTags, indeterminateTags, stateFilter)
127+
}
128+
129+
override fun dispatchKeyEvent(event: android.view.KeyEvent): Boolean =
130+
noteEditorFragment?.dispatchKeyEvent(event) ?: false || super.dispatchKeyEvent(event)
131+
132+
override val shortcuts: ShortcutGroup
133+
get() = noteEditorFragment?.shortcuts ?: ShortcutGroup(emptyList(), 0)
134+
135+
companion object {
136+
const val FRAGMENT_ARGS_EXTRA = "fragment_args"
137+
const val FRAGMENT_NAME_EXTRA = "fragmentName"
138+
const val FRAGMENT_TAG = "NoteEditorFragmentTag"
139+
140+
fun getIntent(
141+
context: Context,
142+
fragmentClass: KClass<out Fragment>,
143+
arguments: Bundle? = null,
144+
intentAction: String? = null,
145+
): Intent =
146+
Intent(context, NoteEditorActivity::class.java).apply {
147+
putExtra(FRAGMENT_NAME_EXTRA, fragmentClass.jvmName)
148+
putExtra(FRAGMENT_ARGS_EXTRA, arguments)
149+
action = intentAction
150+
}
151+
}
152+
}

AnkiDroid/src/main/java/com/ichi2/anki/NoteEditorFragment.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,15 @@ import androidx.core.os.BundleCompat
7070
import androidx.core.text.HtmlCompat
7171
import androidx.core.util.component1
7272
import androidx.core.util.component2
73+
import androidx.core.view.MenuHost
7374
import androidx.core.view.MenuProvider
7475
import androidx.core.view.OnReceiveContentListener
7576
import androidx.core.view.WindowInsetsControllerCompat
7677
import androidx.core.view.isVisible
7778
import androidx.draganddrop.DropHelper
7879
import androidx.fragment.app.Fragment
7980
import androidx.fragment.app.activityViewModels
81+
import androidx.lifecycle.Lifecycle
8082
import androidx.lifecycle.lifecycleScope
8183
import anki.config.ConfigKey
8284
import anki.notes.NoteFieldsCheckResponse
@@ -226,8 +228,11 @@ class NoteEditorFragment :
226228
private val getColUnsafe: Collection
227229
get() = CollectionManager.getColUnsafe()
228230

231+
private val noteEditorActivity: NoteEditorActivity
232+
get() = requireActivity() as NoteEditorActivity
233+
229234
private val mainToolbar: androidx.appcompat.widget.Toolbar
230-
get() = requireView().findViewById(R.id.toolbar)
235+
get() = noteEditorActivity.findViewById(R.id.toolbar)
231236

232237
/**
233238
* Flag which forces the calling activity to rebuild it's definition of current card from scratch
@@ -578,7 +583,13 @@ class NoteEditorFragment :
578583
requireActivity().onBackPressedDispatcher.onBackPressed()
579584
}
580585

586+
// Register this fragment as a menu provider with the activity
581587
mainToolbar.addMenuProvider(this)
588+
(requireActivity() as MenuHost).addMenuProvider(
589+
this,
590+
viewLifecycleOwner,
591+
Lifecycle.State.RESUMED,
592+
)
582593
}
583594

584595
/**

AnkiDroid/src/main/java/com/ichi2/anki/noteeditor/NoteEditorLauncher.kt

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ import android.os.Parcelable
2424
import androidx.core.os.bundleOf
2525
import com.ichi2.anim.ActivityTransitionAnimation
2626
import com.ichi2.anki.AnkiActivity
27+
import com.ichi2.anki.NoteEditorActivity
2728
import com.ichi2.anki.NoteEditorFragment
2829
import com.ichi2.anki.NoteEditorFragment.Companion.NoteEditorCaller
29-
import com.ichi2.anki.SingleFragmentActivity
3030
import com.ichi2.anki.browser.CardBrowserViewModel
3131
import com.ichi2.anki.libanki.CardId
3232
import com.ichi2.anki.libanki.DeckId
@@ -39,16 +39,19 @@ sealed interface NoteEditorLauncher : Destination {
3939
override fun toIntent(context: Context): Intent = toIntent(context, action = null)
4040

4141
/**
42-
* Generates an intent to open the NoteEditor fragment with the configured parameters.
42+
* Generates an intent to open the NoteEditor activity with the configured parameters
4343
*
4444
* @param context The context from which the intent is launched.
4545
* @param action Optional action string for the intent.
46-
* @return Intent configured to launch the NoteEditor fragment.
46+
* @return Intent configured to launch the appropriate activity.
4747
*/
4848
fun toIntent(
4949
context: Context,
5050
action: String? = null,
51-
) = SingleFragmentActivity.getIntent(context, NoteEditorFragment::class, toBundle(), action)
51+
) = Intent(context, NoteEditorActivity::class.java).apply {
52+
putExtras(toBundle())
53+
action?.let { this.action = it }
54+
}
5255

5356
/**
5457
* Converts the configuration into a Bundle to pass arguments to the NoteEditor fragment.
@@ -105,16 +108,18 @@ sealed interface NoteEditorLauncher : Destination {
105108
val inFragmentedActivity: Boolean = false,
106109
) : NoteEditorLauncher {
107110
override fun toBundle(): Bundle {
108-
val bundle =
111+
val fragmentArgs =
109112
bundleOf(
110113
NoteEditorFragment.EXTRA_CALLER to NoteEditorCaller.CARDBROWSER_ADD.value,
111114
NoteEditorFragment.EXTRA_TEXT_FROM_SEARCH_VIEW to viewModel.searchTerms,
112115
NoteEditorFragment.IN_FRAGMENTED_ACTIVITY to inFragmentedActivity,
113116
)
114117
if (viewModel.lastDeckId?.let { id -> id > 0 } == true) {
115-
bundle.putLong(NoteEditorFragment.EXTRA_DID, viewModel.lastDeckId!!)
118+
fragmentArgs.putLong(NoteEditorFragment.EXTRA_DID, viewModel.lastDeckId!!)
116119
}
117-
return bundle
120+
return bundleOf(
121+
NoteEditorActivity.FRAGMENT_ARGS_EXTRA to fragmentArgs,
122+
)
118123
}
119124
}
120125

@@ -125,17 +130,23 @@ sealed interface NoteEditorLauncher : Destination {
125130
data class AddNoteFromReviewer(
126131
val animation: ActivityTransitionAnimation.Direction? = null,
127132
) : NoteEditorLauncher {
128-
override fun toBundle(): Bundle =
129-
bundleOf(
130-
NoteEditorFragment.EXTRA_CALLER to NoteEditorCaller.REVIEWER_ADD.value,
131-
).also { bundle ->
132-
animation?.let { animation ->
133-
bundle.putParcelable(
134-
AnkiActivity.FINISH_ANIMATION_EXTRA,
135-
animation as Parcelable,
136-
)
133+
override fun toBundle(): Bundle {
134+
val fragmentArgs =
135+
bundleOf(
136+
NoteEditorFragment.EXTRA_CALLER to NoteEditorCaller.REVIEWER_ADD.value,
137+
).also { bundle ->
138+
animation?.let { animation ->
139+
bundle.putParcelable(
140+
AnkiActivity.FINISH_ANIMATION_EXTRA,
141+
animation as Parcelable,
142+
)
143+
}
137144
}
138-
}
145+
146+
return bundleOf(
147+
NoteEditorActivity.FRAGMENT_ARGS_EXTRA to fragmentArgs,
148+
)
149+
}
139150
}
140151

141152
/**
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:layout_width="match_parent"
4+
android:layout_height="match_parent">
5+
<LinearLayout
6+
android:layout_width="match_parent"
7+
android:layout_height="match_parent"
8+
android:orientation="vertical">
9+
<include layout="@layout/toolbar" />
10+
<androidx.fragment.app.FragmentContainerView
11+
android:id="@+id/note_editor_fragment_frame"
12+
android:layout_width="match_parent"
13+
android:layout_height="match_parent" />
14+
</LinearLayout>
15+
</androidx.coordinatorlayout.widget.CoordinatorLayout>

0 commit comments

Comments
 (0)