Skip to content

Commit e37933a

Browse files
committed
refactor(message-list): extract LegacyMessageListFragment and introduce Factory pattern; allows for future runtime toggling between the legacy fragment and the new MessageListFragment using feature flags
1 parent 66ba2a3 commit e37933a

4 files changed

Lines changed: 117 additions & 47 deletions

File tree

legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ import com.fsck.k9.ui.BuildConfig
4242
import com.fsck.k9.ui.R
4343
import com.fsck.k9.ui.base.BaseActivity
4444
import com.fsck.k9.ui.managefolders.ManageFoldersActivity
45-
import com.fsck.k9.ui.messagelist.DefaultFolderProvider
4645
import com.fsck.k9.ui.messagelist.AbstractMessageListFragment
4746
import com.fsck.k9.ui.messagelist.AbstractMessageListFragment.MessageListFragmentListener
47+
import com.fsck.k9.ui.messagelist.DefaultFolderProvider
4848
import com.fsck.k9.ui.messageview.MessageViewContainerFragment
4949
import com.fsck.k9.ui.messageview.MessageViewContainerFragment.MessageViewContainerListener
5050
import com.fsck.k9.ui.messageview.MessageViewFragment.MessageViewFragmentListener
@@ -53,7 +53,6 @@ import com.fsck.k9.ui.settings.SettingsActivity
5353
import com.fsck.k9.view.ViewSwitcher
5454
import com.fsck.k9.view.ViewSwitcher.OnSwitchCompleteListener
5555
import com.google.android.material.textview.MaterialTextView
56-
import kotlin.getValue
5756
import net.thunderbird.core.android.account.LegacyAccount
5857
import net.thunderbird.core.android.account.LegacyAccountDto
5958
import net.thunderbird.core.android.account.LegacyAccountDtoManager
@@ -121,6 +120,7 @@ open class MainActivity :
121120
private var openFolderTransaction: FragmentTransaction? = null
122121
private var progressBar: ProgressBar? = null
123122
private var messageViewPlaceHolder: PlaceholderFragment? = null
123+
private val messageListFragmentFactory: AbstractMessageListFragment.Factory by inject()
124124
private var messageListFragment: AbstractMessageListFragment? = null
125125
private var messageViewContainerFragment: MessageViewContainerFragment? = null
126126
private var account: LegacyAccountDto? = null
@@ -266,7 +266,9 @@ open class MainActivity :
266266

267267
private fun findFragments() {
268268
val fragmentManager = supportFragmentManager
269-
messageListFragment = fragmentManager.findFragmentById(R.id.message_list_container) as? AbstractMessageListFragment
269+
messageListFragment = fragmentManager.findFragmentById(
270+
R.id.message_list_container,
271+
) as? AbstractMessageListFragment
270272
messageViewContainerFragment =
271273
fragmentManager.findFragmentByTag(FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER) as? MessageViewContainerFragment
272274

@@ -283,10 +285,10 @@ open class MainActivity :
283285
val hasMessageListFragment = messageListFragment != null
284286
if (!hasMessageListFragment) {
285287
val fragmentTransaction = fragmentManager.beginTransaction()
286-
val messageListFragment = AbstractMessageListFragment.newInstance(
287-
search!!,
288-
false,
289-
generalSettingsManager.getConfig()
288+
val messageListFragment = messageListFragmentFactory.newInstance(
289+
search = search!!,
290+
isThreadDisplay = false,
291+
threadedList = generalSettingsManager.getConfig()
290292
.display
291293
.inboxSettings
292294
.isThreadedViewEnabled &&
@@ -741,10 +743,10 @@ open class MainActivity :
741743
}
742744

743745
val openFolderTransaction = fragmentManager.beginTransaction()
744-
val messageListFragment = AbstractMessageListFragment.newInstance(
745-
search,
746-
false,
747-
generalSettingsManager.getConfig().display.inboxSettings.isThreadedViewEnabled,
746+
val messageListFragment = messageListFragmentFactory.newInstance(
747+
search = search,
748+
isThreadDisplay = false,
749+
threadedList = generalSettingsManager.getConfig().display.inboxSettings.isThreadedViewEnabled,
748750
)
749751
openFolderTransaction.replace(R.id.message_list_container, messageListFragment)
750752

@@ -1229,7 +1231,11 @@ open class MainActivity :
12291231

12301232
initializeFromLocalSearch(tmpSearch)
12311233

1232-
val fragment = AbstractMessageListFragment.newInstance(tmpSearch, true, false)
1234+
val fragment = messageListFragmentFactory.newInstance(
1235+
search = tmpSearch,
1236+
isThreadDisplay = true,
1237+
threadedList = false,
1238+
)
12331239
addMessageListFragment(fragment)
12341240
}
12351241

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/KoinModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import app.k9mail.legacy.message.controller.MessagingControllerMailChecker
55
import com.fsck.k9.controller.MessagingController
66
import com.fsck.k9.ui.helper.DisplayHtmlUiFactory
77
import com.fsck.k9.ui.helper.SizeFormatter
8+
import com.fsck.k9.ui.messagelist.AbstractMessageListFragment
9+
import com.fsck.k9.ui.messagelist.LegacyMessageListFragment
810
import com.fsck.k9.ui.messageview.LinkTextHandler
911
import com.fsck.k9.ui.share.ShareIntentBuilder
1012
import net.thunderbird.core.common.inject.getList
@@ -25,4 +27,8 @@ val uiModule = module {
2527
factory { (context: Context) -> SizeFormatter(context.resources) }
2628
factory { ShareIntentBuilder(resourceProvider = get(), textPartFinder = get(), quoteDateFormatter = get()) }
2729
factory { LinkTextHandler(context = get(), clipboardManager = get()) }
30+
factory<AbstractMessageListFragment.Factory> {
31+
// TODO(9497): verify if EnableMessageListNewState is enabled. If so, use the new MessageListFragment instead.
32+
LegacyMessageListFragment.Factory
33+
}
2834
}

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import android.view.View
1212
import android.view.ViewGroup
1313
import android.widget.Toast
1414
import androidx.activity.result.ActivityResultLauncher
15+
import androidx.annotation.Discouraged
1516
import androidx.annotation.StringRes
1617
import androidx.appcompat.view.ActionMode
1718
import androidx.compose.animation.animateContentSize
1819
import androidx.compose.ui.Modifier
1920
import androidx.compose.ui.layout.onSizeChanged
2021
import androidx.compose.ui.platform.ComposeView
2122
import androidx.coordinatorlayout.widget.CoordinatorLayout
22-
import androidx.core.os.bundleOf
2323
import androidx.core.view.ViewCompat
2424
import androidx.core.view.WindowInsetsCompat
2525
import androidx.core.view.WindowInsetsCompat.Type.systemBars
@@ -94,14 +94,14 @@ import net.thunderbird.core.featureflag.FeatureFlagKey
9494
import net.thunderbird.core.featureflag.FeatureFlagProvider
9595
import net.thunderbird.core.featureflag.FeatureFlagResult
9696
import net.thunderbird.core.logging.Logger
97-
import net.thunderbird.core.logging.legacy.Log
9897
import net.thunderbird.core.outcome.Outcome
9998
import net.thunderbird.core.preference.GeneralSettingsManager
10099
import net.thunderbird.core.preference.display.visualSettings.message.list.DisplayMessageListSettings
101100
import net.thunderbird.core.preference.interaction.InteractionSettings
102101
import net.thunderbird.core.ui.theme.api.FeatureThemeProvider
103102
import net.thunderbird.feature.account.avatar.AvatarMonogramCreator
104103
import net.thunderbird.feature.mail.folder.api.OutboxFolderManager
104+
import net.thunderbird.feature.mail.message.list.MessageListFeatureFlags
105105
import net.thunderbird.feature.mail.message.list.domain.DomainContract
106106
import net.thunderbird.feature.mail.message.list.ui.dialog.SetupArchiveFolderDialogFragmentFactory
107107
import net.thunderbird.feature.notification.api.content.InAppNotification
@@ -124,15 +124,29 @@ private const val MAXIMUM_MESSAGE_SORT_OVERRIDES = 3
124124
private const val MINIMUM_CLICK_INTERVAL = 200L
125125
private const val RECENT_CHANGES_SNACKBAR_DURATION = 10 * 1000
126126

127-
private const val TAG = "MessageListFragment"
128-
129-
@Suppress("LargeClass", "TooManyFunctions")
130-
class AbstractMessageListFragment :
127+
@Suppress(
128+
"LargeClass",
129+
"TooManyFunctions",
130+
"CyclomaticComplexMethod",
131+
"TooGenericExceptionCaught",
132+
"TooGenericExceptionThrown",
133+
"SwallowedException",
134+
"ReturnCount",
135+
"ForbiddenComment",
136+
)
137+
@Discouraged(
138+
message = "This class is in maintenance mode. DO NOT introduce any new features in this class. " +
139+
"Only bugfixes are allowed. New features must be introduced in the new MessageListFragment, " +
140+
"following the MVI principle.",
141+
)
142+
abstract class AbstractMessageListFragment :
131143
Fragment(),
132144
ConfirmationDialogFragmentListener,
133145
MessageListItemActionListener,
134146
ErrorNotificationsDialogFragmentActionListener {
135147

148+
abstract val logTag: String
149+
136150
val viewModel: MessageListViewModel by viewModel()
137151
private val recentChangesViewModel: RecentChangesViewModel by viewModel()
138152

@@ -383,10 +397,10 @@ class AbstractMessageListFragment :
383397
setFragmentResultListener(
384398
SetupArchiveFolderDialogFragmentFactory.RESULT_CODE_DISMISS_REQUEST_KEY,
385399
) { key, bundle ->
386-
Log.d(
400+
logger.debug(logTag) {
387401
"SetupArchiveFolderDialogFragment fragment listener triggered with " +
388-
"key: $key and bundle: $bundle",
389-
)
402+
"key: $key and bundle: $bundle"
403+
}
390404
loadMessageList(forceUpdate = true)
391405
}
392406
}
@@ -1618,7 +1632,7 @@ class AbstractMessageListFragment :
16181632
for ((folderId, messagesInFolder) in folderMap) {
16191633
val account = accountManager.getAccount(messagesInFolder.first().accountUuid)
16201634
if (account == null) {
1621-
logger.debug(TAG) {
1635+
logger.debug(logTag) {
16221636
"Account for message ${messagesInFolder.first()} not found, skipping copy/move operation"
16231637
}
16241638
continue
@@ -1743,12 +1757,12 @@ class AbstractMessageListFragment :
17431757
// If we represent a remote search, then kill that before going back.
17441758
if (isRemoteSearch && remoteSearchFuture != null) {
17451759
try {
1746-
Log.i("Remote search in progress, attempting to abort...")
1760+
logger.info(logTag) { "Remote search in progress, attempting to abort..." }
17471761

17481762
// Canceling the future stops any message fetches in progress.
17491763
val cancelSuccess = remoteSearchFuture!!.cancel(true) // mayInterruptIfRunning = true
17501764
if (!cancelSuccess) {
1751-
Log.e("Could not cancel remote search future.")
1765+
logger.error(logTag) { "Could not cancel remote search future." }
17521766
}
17531767

17541768
// Closing the folder will kill off the connection if we're mid-search.
@@ -1763,7 +1777,7 @@ class AbstractMessageListFragment :
17631777
)
17641778
} catch (e: Exception) {
17651779
// Since the user is going back, log and squash any exceptions.
1766-
Log.e(e, "Could not abort remote search before going back")
1780+
logger.error(logTag, e) { "Could not abort remote search before going back" }
17671781
}
17681782
}
17691783

@@ -2596,31 +2610,42 @@ class AbstractMessageListFragment :
25962610
}
25972611
}
25982612

2599-
companion object Companion {
2600-
2601-
private const val ARG_SEARCH = "searchObject"
2602-
private const val ARG_THREADED_LIST = "showingThreadedList"
2603-
private const val ARG_IS_THREAD_DISPLAY = "isThreadedDisplay"
2604-
2605-
private const val STATE_SELECTED_MESSAGES = "selectedMessages"
2606-
private const val STATE_ACTIVE_MESSAGES = "activeMessages"
2607-
private const val STATE_ACTIVE_MESSAGE = "activeMessage"
2608-
private const val STATE_REMOTE_SEARCH_PERFORMED = "remoteSearchPerformed"
2609-
2613+
/**
2614+
* A factory for creating instances of [AbstractMessageListFragment].
2615+
*
2616+
* This interface is a temporary solution to toggle between different fragment implementations
2617+
* based on a feature flag. It allows for the creation of either a modern [MessageListFragment] or a
2618+
* [LegacyMessageListFragment] depending on the state of [MessageListFeatureFlags.EnableMessageListNewState].
2619+
*/
2620+
interface Factory {
2621+
/**
2622+
* Creates a new instance of a class that inherits from [AbstractMessageListFragment].
2623+
*
2624+
* The specific implementation returned ([MessageListFragment] or [LegacyMessageListFragment]) is determined
2625+
* by the [MessageListFeatureFlags.EnableMessageListNewState] feature flag.
2626+
*
2627+
* @param search The search query that defines which messages to display.
2628+
* @param isThreadDisplay `true` if the fragment is used to display a single thread, `false` otherwise.
2629+
* @param threadedList `true` to display the message list in a threaded conversation view, `false` otherwise.
2630+
*
2631+
* @return An instance of [MessageListFragment] if the new state feature flag is enabled;
2632+
* otherwise, an instance of [LegacyMessageListFragment].
2633+
*/
26102634
fun newInstance(
26112635
search: LocalMessageSearch,
26122636
isThreadDisplay: Boolean,
26132637
threadedList: Boolean,
2614-
): AbstractMessageListFragment {
2615-
val searchBytes = LocalMessageSearchSerializer.serialize(search)
2616-
2617-
return AbstractMessageListFragment().apply {
2618-
arguments = bundleOf(
2619-
ARG_SEARCH to searchBytes,
2620-
ARG_IS_THREAD_DISPLAY to isThreadDisplay,
2621-
ARG_THREADED_LIST to threadedList,
2622-
)
2623-
}
2624-
}
2638+
): AbstractMessageListFragment
2639+
}
2640+
2641+
companion object {
2642+
protected const val ARG_SEARCH = "searchObject"
2643+
protected const val ARG_THREADED_LIST = "showingThreadedList"
2644+
protected const val ARG_IS_THREAD_DISPLAY = "isThreadedDisplay"
2645+
2646+
protected const val STATE_SELECTED_MESSAGES = "selectedMessages"
2647+
protected const val STATE_ACTIVE_MESSAGES = "activeMessages"
2648+
protected const val STATE_ACTIVE_MESSAGE = "activeMessage"
2649+
protected const val STATE_REMOTE_SEARCH_PERFORMED = "remoteSearchPerformed"
26252650
}
26262651
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.fsck.k9.ui.messagelist
2+
3+
import androidx.core.os.bundleOf
4+
import net.thunderbird.feature.search.legacy.LocalMessageSearch
5+
import net.thunderbird.feature.search.legacy.serialization.LocalMessageSearchSerializer
6+
7+
private const val TAG = "LegacyMessageListFragment"
8+
9+
@Deprecated(
10+
message = "DO NOT introduce any new features in this class. " +
11+
"This will be replaced by the new MessageListFragment and deleted in the future.",
12+
)
13+
class LegacyMessageListFragment : AbstractMessageListFragment() {
14+
override val logTag: String = TAG
15+
16+
companion object Factory : AbstractMessageListFragment.Factory {
17+
override fun newInstance(
18+
search: LocalMessageSearch,
19+
isThreadDisplay: Boolean,
20+
threadedList: Boolean,
21+
): LegacyMessageListFragment {
22+
val searchBytes = LocalMessageSearchSerializer.serialize(search)
23+
24+
return LegacyMessageListFragment().apply {
25+
arguments = bundleOf(
26+
ARG_SEARCH to searchBytes,
27+
ARG_IS_THREAD_DISPLAY to isThreadDisplay,
28+
ARG_THREADED_LIST to threadedList,
29+
)
30+
}
31+
}
32+
}
33+
}

0 commit comments

Comments
 (0)