diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt index 6c634d7d203..1e4f38d94f7 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MainActivity.kt @@ -42,9 +42,9 @@ import com.fsck.k9.ui.BuildConfig import com.fsck.k9.ui.R import com.fsck.k9.ui.base.BaseActivity import com.fsck.k9.ui.managefolders.ManageFoldersActivity +import com.fsck.k9.ui.messagelist.AbstractMessageListFragment +import com.fsck.k9.ui.messagelist.AbstractMessageListFragment.MessageListFragmentListener import com.fsck.k9.ui.messagelist.DefaultFolderProvider -import com.fsck.k9.ui.messagelist.MessageListFragment -import com.fsck.k9.ui.messagelist.MessageListFragment.MessageListFragmentListener import com.fsck.k9.ui.messageview.MessageViewContainerFragment import com.fsck.k9.ui.messageview.MessageViewContainerFragment.MessageViewContainerListener import com.fsck.k9.ui.messageview.MessageViewFragment.MessageViewFragmentListener @@ -53,7 +53,6 @@ import com.fsck.k9.ui.settings.SettingsActivity import com.fsck.k9.view.ViewSwitcher import com.fsck.k9.view.ViewSwitcher.OnSwitchCompleteListener import com.google.android.material.textview.MaterialTextView -import kotlin.getValue import net.thunderbird.core.android.account.LegacyAccount import net.thunderbird.core.android.account.LegacyAccountDto import net.thunderbird.core.android.account.LegacyAccountDtoManager @@ -86,7 +85,7 @@ private const val TAG = "MainActivity" * "View Message" notification. * * `MainActivity` manages the overall layout, including the navigation drawer and the main content area, - * which currently displays either a [MessageListFragment] or a [MessageViewContainerFragment]. It orchestrates + * which currently displays either a [AbstractMessageListFragment] or a [MessageViewContainerFragment]. It orchestrates * the interactions between these fragments and handles the back stack. The responsibilities for managing the * action bar, search functionality, and single-pane/split-view layout logic are currently handled here but * are intended to be refactored into more dedicated components over time. @@ -121,7 +120,8 @@ open class MainActivity : private var openFolderTransaction: FragmentTransaction? = null private var progressBar: ProgressBar? = null private var messageViewPlaceHolder: PlaceholderFragment? = null - private var messageListFragment: MessageListFragment? = null + private val messageListFragmentFactory: AbstractMessageListFragment.Factory by inject() + private var messageListFragment: AbstractMessageListFragment? = null private var messageViewContainerFragment: MessageViewContainerFragment? = null private var account: LegacyAccountDto? = null private var search: LocalMessageSearch? = null @@ -266,7 +266,9 @@ open class MainActivity : private fun findFragments() { val fragmentManager = supportFragmentManager - messageListFragment = fragmentManager.findFragmentById(R.id.message_list_container) as? MessageListFragment + messageListFragment = fragmentManager.findFragmentById( + R.id.message_list_container, + ) as? AbstractMessageListFragment messageViewContainerFragment = fragmentManager.findFragmentByTag(FRAGMENT_TAG_MESSAGE_VIEW_CONTAINER) as? MessageViewContainerFragment @@ -283,10 +285,10 @@ open class MainActivity : val hasMessageListFragment = messageListFragment != null if (!hasMessageListFragment) { val fragmentTransaction = fragmentManager.beginTransaction() - val messageListFragment = MessageListFragment.newInstance( - search!!, - false, - generalSettingsManager.getConfig() + val messageListFragment = messageListFragmentFactory.newInstance( + search = search!!, + isThreadDisplay = false, + threadedList = generalSettingsManager.getConfig() .display .inboxSettings .isThreadedViewEnabled && @@ -741,10 +743,10 @@ open class MainActivity : } val openFolderTransaction = fragmentManager.beginTransaction() - val messageListFragment = MessageListFragment.newInstance( - search, - false, - generalSettingsManager.getConfig().display.inboxSettings.isThreadedViewEnabled, + val messageListFragment = messageListFragmentFactory.newInstance( + search = search, + isThreadDisplay = false, + threadedList = generalSettingsManager.getConfig().display.inboxSettings.isThreadedViewEnabled, ) openFolderTransaction.replace(R.id.message_list_container, messageListFragment) @@ -1159,7 +1161,7 @@ open class MainActivity : } } - private fun addMessageListFragment(fragment: MessageListFragment) { + private fun addMessageListFragment(fragment: AbstractMessageListFragment) { messageListFragment?.isActive = false supportFragmentManager.commit { @@ -1229,7 +1231,11 @@ open class MainActivity : initializeFromLocalSearch(tmpSearch) - val fragment = MessageListFragment.newInstance(tmpSearch, true, false) + val fragment = messageListFragmentFactory.newInstance( + search = tmpSearch, + isThreadDisplay = true, + threadedList = false, + ) addMessageListFragment(fragment) } @@ -1459,7 +1465,7 @@ open class MainActivity : } } - private fun MessageListFragment.setFullyActive() { + private fun AbstractMessageListFragment.setFullyActive() { isActive = true onFullyActive() } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/KoinModule.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/KoinModule.kt index 113258af5ba..7184308507d 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/KoinModule.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/KoinModule.kt @@ -5,6 +5,8 @@ import app.k9mail.legacy.message.controller.MessagingControllerMailChecker import com.fsck.k9.controller.MessagingController import com.fsck.k9.ui.helper.DisplayHtmlUiFactory import com.fsck.k9.ui.helper.SizeFormatter +import com.fsck.k9.ui.messagelist.AbstractMessageListFragment +import com.fsck.k9.ui.messagelist.LegacyMessageListFragment import com.fsck.k9.ui.messageview.LinkTextHandler import com.fsck.k9.ui.share.ShareIntentBuilder import net.thunderbird.core.common.inject.getList @@ -25,4 +27,8 @@ val uiModule = module { factory { (context: Context) -> SizeFormatter(context.resources) } factory { ShareIntentBuilder(resourceProvider = get(), textPartFinder = get(), quoteDateFormatter = get()) } factory { LinkTextHandler(context = get(), clipboardManager = get()) } + factory { + // TODO(9497): verify if EnableMessageListNewState is enabled. If so, use the new MessageListFragment instead. + LegacyMessageListFragment.Factory + } } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt similarity index 96% rename from legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt rename to legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt index 812511ffedf..3bb8dcf5c0f 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/AbstractMessageListFragment.kt @@ -12,6 +12,7 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.ActivityResultLauncher +import androidx.annotation.Discouraged import androidx.annotation.StringRes import androidx.appcompat.view.ActionMode import androidx.compose.animation.animateContentSize @@ -19,7 +20,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.ComposeView import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.os.bundleOf import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat.Type.systemBars @@ -62,7 +62,7 @@ import com.fsck.k9.ui.changelog.RecentChangesViewModel import com.fsck.k9.ui.choosefolder.ChooseFolderActivity import com.fsck.k9.ui.choosefolder.ChooseFolderResultContract import com.fsck.k9.ui.helper.RelativeDateTimeFormatter -import com.fsck.k9.ui.messagelist.MessageListFragment.MessageListFragmentListener.Companion.MAX_PROGRESS +import com.fsck.k9.ui.messagelist.AbstractMessageListFragment.MessageListFragmentListener.Companion.MAX_PROGRESS import com.fsck.k9.ui.messagelist.debug.AuthDebugActions import com.fsck.k9.ui.messagelist.item.MessageViewHolder import com.google.android.material.floatingactionbutton.FloatingActionButton @@ -94,7 +94,6 @@ import net.thunderbird.core.featureflag.FeatureFlagKey import net.thunderbird.core.featureflag.FeatureFlagProvider import net.thunderbird.core.featureflag.FeatureFlagResult import net.thunderbird.core.logging.Logger -import net.thunderbird.core.logging.legacy.Log import net.thunderbird.core.outcome.Outcome import net.thunderbird.core.preference.GeneralSettingsManager import net.thunderbird.core.preference.display.visualSettings.message.list.DisplayMessageListSettings @@ -102,6 +101,7 @@ import net.thunderbird.core.preference.interaction.InteractionSettings import net.thunderbird.core.ui.theme.api.FeatureThemeProvider import net.thunderbird.feature.account.avatar.AvatarMonogramCreator import net.thunderbird.feature.mail.folder.api.OutboxFolderManager +import net.thunderbird.feature.mail.message.list.MessageListFeatureFlags import net.thunderbird.feature.mail.message.list.domain.DomainContract import net.thunderbird.feature.mail.message.list.ui.dialog.SetupArchiveFolderDialogFragmentFactory import net.thunderbird.feature.notification.api.content.InAppNotification @@ -124,15 +124,29 @@ private const val MAXIMUM_MESSAGE_SORT_OVERRIDES = 3 private const val MINIMUM_CLICK_INTERVAL = 200L private const val RECENT_CHANGES_SNACKBAR_DURATION = 10 * 1000 -private const val TAG = "MessageListFragment" - -@Suppress("LargeClass", "TooManyFunctions") -class MessageListFragment : +@Suppress( + "LargeClass", + "TooManyFunctions", + "CyclomaticComplexMethod", + "TooGenericExceptionCaught", + "TooGenericExceptionThrown", + "SwallowedException", + "ReturnCount", + "ForbiddenComment", +) +@Discouraged( + message = "This class is in maintenance mode. DO NOT introduce any new features in this class. " + + "Only bugfixes are allowed. New features must be introduced in the new MessageListFragment, " + + "following the MVI principle.", +) +abstract class AbstractMessageListFragment : Fragment(), ConfirmationDialogFragmentListener, MessageListItemActionListener, ErrorNotificationsDialogFragmentActionListener { + abstract val logTag: String + val viewModel: MessageListViewModel by viewModel() private val recentChangesViewModel: RecentChangesViewModel by viewModel() @@ -373,7 +387,7 @@ class MessageListFragment : contactRepository = contactRepository, avatarMonogramCreator = avatarMonogramCreator, ).apply { - activeMessage = this@MessageListFragment.activeMessage + activeMessage = this@AbstractMessageListFragment.activeMessage } } @@ -383,10 +397,10 @@ class MessageListFragment : setFragmentResultListener( SetupArchiveFolderDialogFragmentFactory.RESULT_CODE_DISMISS_REQUEST_KEY, ) { key, bundle -> - Log.d( + logger.debug(logTag) { "SetupArchiveFolderDialogFragment fragment listener triggered with " + - "key: $key and bundle: $bundle", - ) + "key: $key and bundle: $bundle" + } loadMessageList(forceUpdate = true) } } @@ -901,7 +915,7 @@ class MessageListFragment : ) lifecycleScope.launch(Dispatchers.IO) { accountManager.saveAccount(updatedAccount) - this@MessageListFragment.account = updatedAccount + this@AbstractMessageListFragment.account = updatedAccount } } else { K9.sortType = this.sortType @@ -1618,7 +1632,7 @@ class MessageListFragment : for ((folderId, messagesInFolder) in folderMap) { val account = accountManager.getAccount(messagesInFolder.first().accountUuid) if (account == null) { - logger.debug(TAG) { + logger.debug(logTag) { "Account for message ${messagesInFolder.first()} not found, skipping copy/move operation" } continue @@ -1743,12 +1757,12 @@ class MessageListFragment : // If we represent a remote search, then kill that before going back. if (isRemoteSearch && remoteSearchFuture != null) { try { - Log.i("Remote search in progress, attempting to abort...") + logger.info(logTag) { "Remote search in progress, attempting to abort..." } // Canceling the future stops any message fetches in progress. val cancelSuccess = remoteSearchFuture!!.cancel(true) // mayInterruptIfRunning = true if (!cancelSuccess) { - Log.e("Could not cancel remote search future.") + logger.error(logTag) { "Could not cancel remote search future." } } // Closing the folder will kill off the connection if we're mid-search. @@ -1763,7 +1777,7 @@ class MessageListFragment : ) } catch (e: Exception) { // Since the user is going back, log and squash any exceptions. - Log.e(e, "Could not abort remote search before going back") + logger.error(logTag, e) { "Could not abort remote search before going back" } } } @@ -2591,36 +2605,47 @@ class MessageListFragment : fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? fun goBack() - companion object { + companion object Companion { const val MAX_PROGRESS = 10000 } } - companion object { - - private const val ARG_SEARCH = "searchObject" - private const val ARG_THREADED_LIST = "showingThreadedList" - private const val ARG_IS_THREAD_DISPLAY = "isThreadedDisplay" - - private const val STATE_SELECTED_MESSAGES = "selectedMessages" - private const val STATE_ACTIVE_MESSAGES = "activeMessages" - private const val STATE_ACTIVE_MESSAGE = "activeMessage" - private const val STATE_REMOTE_SEARCH_PERFORMED = "remoteSearchPerformed" - + /** + * A factory for creating instances of [AbstractMessageListFragment]. + * + * This interface is a temporary solution to toggle between different fragment implementations + * based on a feature flag. It allows for the creation of either a modern [MessageListFragment] or a + * [LegacyMessageListFragment] depending on the state of [MessageListFeatureFlags.EnableMessageListNewState]. + */ + interface Factory { + /** + * Creates a new instance of a class that inherits from [AbstractMessageListFragment]. + * + * The specific implementation returned ([MessageListFragment] or [LegacyMessageListFragment]) is determined + * by the [MessageListFeatureFlags.EnableMessageListNewState] feature flag. + * + * @param search The search query that defines which messages to display. + * @param isThreadDisplay `true` if the fragment is used to display a single thread, `false` otherwise. + * @param threadedList `true` to display the message list in a threaded conversation view, `false` otherwise. + * + * @return An instance of [MessageListFragment] if the new state feature flag is enabled; + * otherwise, an instance of [LegacyMessageListFragment]. + */ fun newInstance( search: LocalMessageSearch, isThreadDisplay: Boolean, threadedList: Boolean, - ): MessageListFragment { - val searchBytes = LocalMessageSearchSerializer.serialize(search) - - return MessageListFragment().apply { - arguments = bundleOf( - ARG_SEARCH to searchBytes, - ARG_IS_THREAD_DISPLAY to isThreadDisplay, - ARG_THREADED_LIST to threadedList, - ) - } - } + ): AbstractMessageListFragment + } + + companion object { + protected const val ARG_SEARCH = "searchObject" + protected const val ARG_THREADED_LIST = "showingThreadedList" + protected const val ARG_IS_THREAD_DISPLAY = "isThreadedDisplay" + + protected const val STATE_SELECTED_MESSAGES = "selectedMessages" + protected const val STATE_ACTIVE_MESSAGES = "activeMessages" + protected const val STATE_ACTIVE_MESSAGE = "activeMessage" + protected const val STATE_REMOTE_SEARCH_PERFORMED = "remoteSearchPerformed" } } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/LegacyMessageListFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/LegacyMessageListFragment.kt new file mode 100644 index 00000000000..f20382d82b5 --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/LegacyMessageListFragment.kt @@ -0,0 +1,33 @@ +package com.fsck.k9.ui.messagelist + +import androidx.core.os.bundleOf +import net.thunderbird.feature.search.legacy.LocalMessageSearch +import net.thunderbird.feature.search.legacy.serialization.LocalMessageSearchSerializer + +private const val TAG = "LegacyMessageListFragment" + +@Deprecated( + message = "DO NOT introduce any new features in this class. " + + "This will be replaced by the new MessageListFragment and deleted in the future.", +) +class LegacyMessageListFragment : AbstractMessageListFragment() { + override val logTag: String = TAG + + companion object Factory : AbstractMessageListFragment.Factory { + override fun newInstance( + search: LocalMessageSearch, + isThreadDisplay: Boolean, + threadedList: Boolean, + ): LegacyMessageListFragment { + val searchBytes = LocalMessageSearchSerializer.serialize(search) + + return LegacyMessageListFragment().apply { + arguments = bundleOf( + ARG_SEARCH to searchBytes, + ARG_IS_THREAD_DISPLAY to isThreadDisplay, + ARG_THREADED_LIST to threadedList, + ) + } + } + } +} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListHandler.java b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListHandler.java index c3cd533ae84..724761c25ee 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListHandler.java +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListHandler.java @@ -21,9 +21,9 @@ public class MessageListHandler extends Handler { private static final int ACTION_PROGRESS = 3; private static final int ACTION_REMOTE_SEARCH_FINISHED = 4; - private WeakReference mFragment; + private WeakReference mFragment; - public MessageListHandler(MessageListFragment fragment) { + public MessageListHandler(AbstractMessageListFragment fragment) { mFragment = new WeakReference<>(fragment); } public void folderLoading(long folderId, boolean loading) { @@ -52,7 +52,7 @@ public void updateFooter(final String message) { post(new Runnable() { @Override public void run() { - MessageListFragment fragment = mFragment.get(); + AbstractMessageListFragment fragment = mFragment.get(); if (fragment != null) { fragment.updateFooterText(message); } @@ -62,7 +62,7 @@ public void run() { @Override public void handleMessage(android.os.Message msg) { - MessageListFragment fragment = mFragment.get(); + AbstractMessageListFragment fragment = mFragment.get(); if (fragment == null) { return; } diff --git a/legacy/ui/legacy/src/main/res/layout/message_list_fragment.xml b/legacy/ui/legacy/src/main/res/layout/message_list_fragment.xml index 8b4d730bbbb..919742a683a 100644 --- a/legacy/ui/legacy/src/main/res/layout/message_list_fragment.xml +++ b/legacy/ui/legacy/src/main/res/layout/message_list_fragment.xml @@ -6,7 +6,7 @@ android:id="@+id/message_list_coordinator" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".messagelist.MessageListFragment" + tools:context=".messagelist.AbstractMessageListFragment" >