From 30932e3c4816aae2a95bf1cabf99251100d1d2c8 Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Wed, 31 Dec 2025 13:21:47 +0600 Subject: [PATCH 01/13] refactor(AboutFragment): convert about_title from MaterialTextView to ComposeView --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 30 +++++++++++++++++-- .../src/main/res/layout/fragment_about.xml | 11 ++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index fa9c48cd276..3a3efa0ac22 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -12,6 +12,8 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -23,10 +25,13 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleSmall +import app.k9mail.core.ui.compose.theme2.MainTheme import com.fsck.k9.ui.R import com.google.android.material.textview.MaterialTextView import net.thunderbird.core.common.provider.AppNameProvider import net.thunderbird.core.logging.legacy.Log +import net.thunderbird.core.ui.theme.api.FeatureThemeProvider import org.koin.android.ext.android.inject import app.k9mail.core.ui.legacy.designsystem.R as DesignSystemR import app.k9mail.core.ui.legacy.theme2.common.R as Theme2CommonR @@ -34,6 +39,7 @@ import app.k9mail.core.ui.legacy.theme2.common.R as Theme2CommonR @Suppress("TooManyFunctions") class AboutFragment : Fragment() { private val appNameProvider: AppNameProvider by inject() + private val themeProvider: FeatureThemeProvider by inject() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_about, container, false) @@ -56,8 +62,7 @@ class AboutFragment : Fragment() { setForumImage(view) - val titleTextView = view.findViewById(R.id.about_title) - titleTextView.text = getString(R.string.about_title, appNameProvider.appName) + setAboutTitle(view) val versionTextView = view.findViewById(R.id.version) versionTextView.text = getVersionNumber() ?: "?" @@ -180,6 +185,27 @@ class AboutFragment : Fragment() { } } + private fun setAboutTitle(view: View) { + val aboutTitle = view.findViewById(R.id.about_title) + aboutTitle.setContent { + themeProvider.WithTheme { + val context = LocalContext.current + TextTitleSmall( + text = context.getString(R.string.about_title,appNameProvider.appName), + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 16.dp, + top = 16.dp, + bottom = 8.dp, + ), + color = MainTheme.colors.secondary + ) + } + } + } + private fun displayChangeLog() { findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) } diff --git a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml index ee4da0d9749..5e6ffe60c1f 100644 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml @@ -25,17 +25,10 @@ tools:src="@drawable/ic_inbox" /> - + android:layout_height="wrap_content" /> Date: Wed, 31 Dec 2025 13:47:26 +0600 Subject: [PATCH 02/13] refactor(AboutFragment): convert version related MaterialTextViews to ComposeView --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 41 ++++++++++++++++++- .../src/main/res/layout/fragment_about.xml | 9 ++-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index 3a3efa0ac22..73b58fed42c 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -15,16 +15,21 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium +import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleSmall import app.k9mail.core.ui.compose.theme2.MainTheme import com.fsck.k9.ui.R @@ -64,8 +69,9 @@ class AboutFragment : Fragment() { setAboutTitle(view) - val versionTextView = view.findViewById(R.id.version) - versionTextView.text = getVersionNumber() ?: "?" + setVersionLabel(view) + + setVersionText(view) val versionLayout = view.findViewById(R.id.versionLayout) versionLayout.setOnClickListener { displayChangeLog() } @@ -206,6 +212,37 @@ class AboutFragment : Fragment() { } } + private fun setVersionLabel(view: View) { + val versionLabel = view.findViewById(R.id.versionLabel) + versionLabel.setContent { + themeProvider.WithTheme { + val context = LocalContext.current + TextTitleMedium( + text = context.getString(R.string.version), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + + private fun setVersionText(view: View) { + val versionLabel = view.findViewById(R.id.version) + versionLabel.setContent { + themeProvider.WithTheme { + TextBodyMedium( + text = getVersionNumber() ?: "?", + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + private fun displayChangeLog() { findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) } diff --git a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml index 5e6ffe60c1f..ca7fe04258a 100644 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml @@ -61,19 +61,16 @@ android:paddingEnd="0dp" > - - From 06e814d577165707668694beb9cc433f75770819 Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Wed, 31 Dec 2025 14:00:49 +0600 Subject: [PATCH 03/13] refactor(AboutFragment): convert authors related MaterialTextViews to ComposeView --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 56 ++++++++++++++++++- .../src/main/res/layout/fragment_about.xml | 15 ++--- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index 73b58fed42c..29620f1da28 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -71,7 +71,13 @@ class AboutFragment : Fragment() { setVersionLabel(view) - setVersionText(view) + setVersionText(view) + + setAuthorsText(view) + + setAuthorOne(view) + + setAuthorTwo(view) val versionLayout = view.findViewById(R.id.versionLayout) versionLayout.setOnClickListener { displayChangeLog() } @@ -243,6 +249,54 @@ class AboutFragment : Fragment() { } } + private fun setAuthorsText(view: View) { + val authorsLabel = view.findViewById(R.id.authorsLabel) + authorsLabel.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextTitleMedium( + text = context.getString(R.string.authors), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + + private fun setAuthorOne(view: View) { + val authorOne = view.findViewById(R.id.authorOne) + authorOne.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextBodyMedium( + text = context.getString(R.string.about_app_authors_k9), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + + private fun setAuthorTwo(view: View) { + val authorTwo = view.findViewById(R.id.authorTwo) + authorTwo.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextBodyMedium( + text = context.getString(R.string.about_app_authors_thunderbird), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + private fun displayChangeLog() { findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) } diff --git a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml index ca7fe04258a..05392e2db00 100644 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml @@ -106,25 +106,22 @@ android:paddingEnd="0dp" > - - - From ba50fdedc6bd4b4ae9177d2fe5e722d815909855 Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Wed, 31 Dec 2025 14:35:15 +0600 Subject: [PATCH 04/13] refactor(AboutFragment): convert licence related MaterialTextViews to ComposeView --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 36 +++++++++++++++++++ .../src/main/res/layout/fragment_about.xml | 10 +++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index 29620f1da28..737024604bb 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -79,6 +79,10 @@ class AboutFragment : Fragment() { setAuthorTwo(view) + setLicenseText(view) + + setLicense(view) + val versionLayout = view.findViewById(R.id.versionLayout) versionLayout.setOnClickListener { displayChangeLog() } @@ -297,6 +301,38 @@ class AboutFragment : Fragment() { } } + private fun setLicenseText(view: View) { + val licenseLabel = view.findViewById(R.id.licenseLabel) + licenseLabel.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextTitleMedium( + text = context.getString(R.string.license), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + + private fun setLicense(view: View) { + val license = view.findViewById(R.id.license) + license.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextBodyMedium( + text = context.getString(R.string.app_license), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + private fun displayChangeLog() { findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) } diff --git a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml index 05392e2db00..876993e2868 100644 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml @@ -204,18 +204,16 @@ android:paddingEnd="0dp" > - - From bd4cb92658a97c148b526feca4e1340c4efc53cf Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Wed, 31 Dec 2025 15:00:32 +0600 Subject: [PATCH 05/13] refactor(AboutFragment): convert aboutProjectTitle from MaterialTextViews to ComposeView --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 19 +++++++++++++++++++ .../src/main/res/layout/fragment_about.xml | 9 ++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index 737024604bb..451ccf7ca62 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -83,6 +83,8 @@ class AboutFragment : Fragment() { setLicense(view) + setAboutProjectTitle(view) + val versionLayout = view.findViewById(R.id.versionLayout) versionLayout.setOnClickListener { displayChangeLog() } @@ -333,6 +335,23 @@ class AboutFragment : Fragment() { } } + private fun setAboutProjectTitle(view: View) { + val aboutProjectTitle = view.findViewById(R.id.aboutProjectTitle) + aboutProjectTitle.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextTitleSmall( + text = context.getString(R.string.about_project_title), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 16.dp, bottom = 8.dp, start = 16.dp, end = 16.dp), + color = MainTheme.colors.secondary, + ) + } + } + } + private fun displayChangeLog() { findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) } diff --git a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml index 876993e2868..ffd6415f5ea 100644 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml @@ -218,15 +218,10 @@ - Date: Wed, 31 Dec 2025 15:18:39 +0600 Subject: [PATCH 06/13] refactor(AboutFragment): convert website related MaterialTextViews to ComposeView --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 36 +++++++++++++++++++ .../src/main/res/layout/fragment_about.xml | 10 +++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index 451ccf7ca62..cb94bd54095 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -85,6 +85,10 @@ class AboutFragment : Fragment() { setAboutProjectTitle(view) + setWebsiteText(view) + + setWebsite(view) + val versionLayout = view.findViewById(R.id.versionLayout) versionLayout.setOnClickListener { displayChangeLog() } @@ -352,6 +356,38 @@ class AboutFragment : Fragment() { } } + private fun setWebsiteText(view: View) { + val websiteLabel = view.findViewById(R.id.websiteLabel) + websiteLabel.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextTitleMedium( + text = context.getString(R.string.about_website_title), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + + private fun setWebsite(view: View) { + val website = view.findViewById(R.id.website) + website.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextBodyMedium( + text = context.getString(R.string.app_webpage_url), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + private fun displayChangeLog() { findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) } diff --git a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml index ffd6415f5ea..c3cf846283c 100644 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml @@ -255,18 +255,16 @@ android:paddingEnd="0dp" > - - From d54eca59498b0420c11c7b4bb9a7aeace4088dc2 Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Wed, 31 Dec 2025 15:34:57 +0600 Subject: [PATCH 07/13] refactor(AboutFragment): convert librariesTitle MaterialTextView and forum related MaterialTextViews to ComposeView --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 79 ++++++++++++++++--- .../src/main/res/layout/fragment_about.xml | 20 ++--- 2 files changed, 72 insertions(+), 27 deletions(-) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index cb94bd54095..5984d8da759 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -16,13 +16,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController @@ -73,21 +71,27 @@ class AboutFragment : Fragment() { setVersionText(view) - setAuthorsText(view) + setAuthorsLabel(view) setAuthorOne(view) setAuthorTwo(view) - setLicenseText(view) + setLicenseLabel(view) - setLicense(view) + setLicenseText(view) setAboutProjectTitle(view) + setWebsiteLabel(view) + setWebsiteText(view) - setWebsite(view) + setForumLabel(view) + + setForumText(view) + + setLibrariesTitle(view) val versionLayout = view.findViewById(R.id.versionLayout) versionLayout.setOnClickListener { displayChangeLog() } @@ -213,7 +217,7 @@ class AboutFragment : Fragment() { themeProvider.WithTheme { val context = LocalContext.current TextTitleSmall( - text = context.getString(R.string.about_title,appNameProvider.appName), + text = context.getString(R.string.about_title, appNameProvider.appName), modifier = Modifier .fillMaxWidth() .padding( @@ -222,7 +226,7 @@ class AboutFragment : Fragment() { top = 16.dp, bottom = 8.dp, ), - color = MainTheme.colors.secondary + color = MainTheme.colors.secondary, ) } } @@ -259,7 +263,7 @@ class AboutFragment : Fragment() { } } - private fun setAuthorsText(view: View) { + private fun setAuthorsLabel(view: View) { val authorsLabel = view.findViewById(R.id.authorsLabel) authorsLabel.setContent { val context = LocalContext.current @@ -307,7 +311,7 @@ class AboutFragment : Fragment() { } } - private fun setLicenseText(view: View) { + private fun setLicenseLabel(view: View) { val licenseLabel = view.findViewById(R.id.licenseLabel) licenseLabel.setContent { val context = LocalContext.current @@ -323,7 +327,7 @@ class AboutFragment : Fragment() { } } - private fun setLicense(view: View) { + private fun setLicenseText(view: View) { val license = view.findViewById(R.id.license) license.setContent { val context = LocalContext.current @@ -356,7 +360,7 @@ class AboutFragment : Fragment() { } } - private fun setWebsiteText(view: View) { + private fun setWebsiteLabel(view: View) { val websiteLabel = view.findViewById(R.id.websiteLabel) websiteLabel.setContent { val context = LocalContext.current @@ -372,7 +376,7 @@ class AboutFragment : Fragment() { } } - private fun setWebsite(view: View) { + private fun setWebsiteText(view: View) { val website = view.findViewById(R.id.website) website.setContent { val context = LocalContext.current @@ -388,6 +392,55 @@ class AboutFragment : Fragment() { } } + private fun setForumLabel(view: View) { + val forumLabel = view.findViewById(R.id.user_forum_label) + forumLabel.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextTitleMedium( + text = context.getString(R.string.user_forum_title), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + + private fun setForumText(view: View) { + val forum = view.findViewById(R.id.user_forum) + forum.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextBodyMedium( + text = context.getString(R.string.user_forum_url), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } + + private fun setLibrariesTitle(view: View) { + val librariesTitle = view.findViewById(R.id.librariesTitle) + librariesTitle.setContent { + val context = LocalContext.current + themeProvider.WithTheme { + TextTitleSmall( + text = context.getString(R.string.about_libraries), + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 16.dp, bottom = 8.dp, start = 16.dp, end = 16.dp), + color = MainTheme.colors.secondary, + ) + } + } + } + private fun displayChangeLog() { findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) } diff --git a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml index c3cf846283c..2bd82fe82be 100644 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml @@ -300,32 +300,24 @@ android:paddingEnd="0dp" > - - - Date: Wed, 31 Dec 2025 22:18:14 +0600 Subject: [PATCH 08/13] refactor(AboutFragment): convert libraries recyclerView into ComposeView --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 89 ++++++++++++------- .../src/main/res/layout/about_library.xml | 34 ------- .../src/main/res/layout/fragment_about.xml | 2 +- 3 files changed, 56 insertions(+), 69 deletions(-) delete mode 100644 legacy/ui/legacy/src/main/res/layout/about_library.xml diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index 5984d8da759..9d95d3d4335 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -12,10 +12,14 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.selection.selectable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView @@ -24,14 +28,14 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleSmall import app.k9mail.core.ui.compose.theme2.MainTheme import com.fsck.k9.ui.R -import com.google.android.material.textview.MaterialTextView +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.immutableListOf +import kotlinx.collections.immutable.persistentListOf import net.thunderbird.core.common.provider.AppNameProvider import net.thunderbird.core.logging.legacy.Log import net.thunderbird.core.ui.theme.api.FeatureThemeProvider @@ -121,13 +125,10 @@ class AboutFragment : Fragment() { openUrl(getString(R.string.user_forum_url)) } - val manager = LinearLayoutManager(view.context) - val librariesRecyclerView = view.findViewById(R.id.libraries) - librariesRecyclerView.apply { - layoutManager = manager - adapter = LibrariesAdapter(USED_LIBRARIES) - isNestedScrollingEnabled = false - isFocusable = false + view.findViewById(R.id.libraries).setContent { + themeProvider.WithTheme { + LibraryList(libraries = USED_LIBRARIES) + } } } @@ -457,7 +458,7 @@ class AboutFragment : Fragment() { } companion object { - private val USED_LIBRARIES = arrayOf( + private val USED_LIBRARIES = persistentListOf( Library( "Android Jetpack libraries", "https://developer.android.com/jetpack", @@ -551,7 +552,7 @@ class AboutFragment : Fragment() { "https://github.com/splitwise/TokenAutoComplete/", "Apache License, Version 2.0", ), - Library("ZXing", "https://github.com/zxing/zxing", "Apache License, Version 2.0"), + Library("ZXing", "https://github.com/zxing/zxing", "Apache License, Version 2.0") ) } } @@ -567,29 +568,49 @@ private fun Context.openUrl(url: String) { } } -private data class Library(val name: String, val url: String, val license: String) - -private class LibrariesAdapter(private val dataset: Array) : - RecyclerView.Adapter() { - - class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val name: MaterialTextView = view.findViewById(R.id.name) - val license: MaterialTextView = view.findViewById(R.id.license) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.about_library, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, index: Int) { - val library = dataset[index] - holder.name.text = library.name - holder.license.text = library.license - holder.itemView.setOnClickListener { - holder.itemView.context.openUrl(library.url) +@Composable +private fun LibraryList( + libraries: ImmutableList, +) { + Column(modifier = Modifier.fillMaxWidth()) { + libraries.forEach { library -> + LibraryItem(library = library) } } +} - override fun getItemCount() = dataset.size +@Composable +fun LibraryItem( + library: Library, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + Column( + modifier = modifier + .selectable( + selected = false, + onClick = { context.openUrl(library.url) }, + ) + .padding(horizontal = 16.dp, vertical = 12.dp) + .fillMaxWidth() + .wrapContentHeight(), + ) { + TextTitleMedium( + text = library.name, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + TextBodyMedium( + text = library.license, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } } + +@Stable +data class Library(val name: String, val url: String, val license: String) diff --git a/legacy/ui/legacy/src/main/res/layout/about_library.xml b/legacy/ui/legacy/src/main/res/layout/about_library.xml deleted file mode 100644 index 1aa54a53d24..00000000000 --- a/legacy/ui/legacy/src/main/res/layout/about_library.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - diff --git a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml index 2bd82fe82be..41ce511b28f 100644 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml @@ -320,7 +320,7 @@ android:layout_height="wrap_content" /> - Date: Thu, 1 Jan 2026 02:09:51 +0600 Subject: [PATCH 09/13] refactor(AboutFragment): replace fragment's xml-layout with composable --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 772 +++++++----------- 1 file changed, 280 insertions(+), 492 deletions(-) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index 9d95d3d4335..ab84e38971e 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -12,18 +12,29 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment @@ -34,7 +45,6 @@ import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleSmall import app.k9mail.core.ui.compose.theme2.MainTheme import com.fsck.k9.ui.R import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.immutableListOf import kotlinx.collections.immutable.persistentListOf import net.thunderbird.core.common.provider.AppNameProvider import net.thunderbird.core.logging.legacy.Log @@ -49,403 +59,26 @@ class AboutFragment : Fragment() { private val themeProvider: FeatureThemeProvider by inject() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_about, container, false) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setAppLogo(view) - - setVersionImage(view) - - setAuthorsImage(view) - - setSourceCodeImage(view) - - setLicenceImage(view) - - setLinkImage(view) - - setForumImage(view) - - setAboutTitle(view) - - setVersionLabel(view) - - setVersionText(view) - - setAuthorsLabel(view) - - setAuthorOne(view) - - setAuthorTwo(view) - - setLicenseLabel(view) - - setLicenseText(view) - - setAboutProjectTitle(view) - - setWebsiteLabel(view) - - setWebsiteText(view) - - setForumLabel(view) - - setForumText(view) - - setLibrariesTitle(view) - - val versionLayout = view.findViewById(R.id.versionLayout) - versionLayout.setOnClickListener { displayChangeLog() } - - val authorsLayout = view.findViewById(R.id.authorsLayout) - authorsLayout.setOnClickListener { - openUrl(getString(R.string.app_authors_url)) - } - - val licenseLayout = view.findViewById(R.id.licenseLayout) - licenseLayout.setOnClickListener { - openUrl(getString(R.string.app_license_url)) - } - - val sourceCodeLayout = view.findViewById(R.id.sourceCodeLayout) - sourceCodeLayout.setOnClickListener { - openUrl(getString(R.string.app_source_url)) - } - - val websiteLayout = view.findViewById(R.id.websiteLayout) - websiteLayout.setOnClickListener { - openUrl(getString(R.string.app_webpage_url)) - } - - val userForumLayout = view.findViewById(R.id.userForumLayout) - userForumLayout.setOnClickListener { - openUrl(getString(R.string.user_forum_url)) - } - - view.findViewById(R.id.libraries).setContent { - themeProvider.WithTheme { - LibraryList(libraries = USED_LIBRARIES) - } - } - } - - private fun setForumImage(view: View) { - val forumImage = view.findViewById(R.id.forum_image) - forumImage.setContent { - Image( - painter = painterResource(id = DesignSystemR.drawable.ic_forum), - modifier = Modifier.size(size = 32.dp), - contentDescription = null, - ) - } - } - - private fun setLinkImage(view: View) { - val linkImage = view.findViewById(R.id.link_image) - linkImage.setContent { - Image( - painter = painterResource(id = DesignSystemR.drawable.ic_link), - modifier = Modifier.size(size = 32.dp), - contentDescription = null, - ) - } - } - - private fun setLicenceImage(view: View) { - val licenceImage = view.findViewById(R.id.licence_image) - licenceImage.setContent { - Image( - painter = painterResource(id = DesignSystemR.drawable.ic_description), - modifier = Modifier.size(size = 32.dp), - contentDescription = null, - ) - } - } - - private fun setSourceCodeImage(view: View) { - val sourceCodeImage = view.findViewById(R.id.source_code_image) - sourceCodeImage.setContent { - Image( - painter = painterResource(id = DesignSystemR.drawable.ic_code), - modifier = Modifier.size(size = 32.dp), - contentDescription = null, + return ComposeView(requireContext()).apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, ) - } - } - - private fun setAuthorsImage(view: View) { - val authorsImage = view.findViewById(R.id.authors_image) - authorsImage.setContent { - Image( - painter = painterResource(id = DesignSystemR.drawable.ic_group), - modifier = Modifier.size(size = 32.dp), - contentDescription = null, - ) - } - } - - private fun setVersionImage(view: View) { - val versionImage = view.findViewById(R.id.version_image) - versionImage.setContent { - Image( - painter = painterResource(id = DesignSystemR.drawable.ic_info), - modifier = Modifier.size(size = 32.dp), - contentDescription = null, - ) - } - } - - private fun setAppLogo(view: View) { - val appLogo = view.findViewById(R.id.app_logo) - appLogo.setContent { - val context = LocalContext.current - val typedValue = remember { TypedValue() } - context.theme.resolveAttribute(Theme2CommonR.attr.appLogo, typedValue, true) - Image( - painter = painterResource(id = typedValue.resourceId), - modifier = Modifier.size(size = 128.dp), - contentDescription = null, - ) - } - } - - private fun setAboutTitle(view: View) { - val aboutTitle = view.findViewById(R.id.about_title) - aboutTitle.setContent { - themeProvider.WithTheme { - val context = LocalContext.current - TextTitleSmall( - text = context.getString(R.string.about_title, appNameProvider.appName), - modifier = Modifier - .fillMaxWidth() - .padding( - start = 16.dp, - end = 16.dp, - top = 16.dp, - bottom = 8.dp, - ), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setVersionLabel(view: View) { - val versionLabel = view.findViewById(R.id.versionLabel) - versionLabel.setContent { - themeProvider.WithTheme { - val context = LocalContext.current - TextTitleMedium( - text = context.getString(R.string.version), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setVersionText(view: View) { - val versionLabel = view.findViewById(R.id.version) - versionLabel.setContent { - themeProvider.WithTheme { - TextBodyMedium( - text = getVersionNumber() ?: "?", - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setAuthorsLabel(view: View) { - val authorsLabel = view.findViewById(R.id.authorsLabel) - authorsLabel.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextTitleMedium( - text = context.getString(R.string.authors), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setAuthorOne(view: View) { - val authorOne = view.findViewById(R.id.authorOne) - authorOne.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextBodyMedium( - text = context.getString(R.string.about_app_authors_k9), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setAuthorTwo(view: View) { - val authorTwo = view.findViewById(R.id.authorTwo) - authorTwo.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextBodyMedium( - text = context.getString(R.string.about_app_authors_thunderbird), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setLicenseLabel(view: View) { - val licenseLabel = view.findViewById(R.id.licenseLabel) - licenseLabel.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextTitleMedium( - text = context.getString(R.string.license), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setLicenseText(view: View) { - val license = view.findViewById(R.id.license) - license.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextBodyMedium( - text = context.getString(R.string.app_license), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setAboutProjectTitle(view: View) { - val aboutProjectTitle = view.findViewById(R.id.aboutProjectTitle) - aboutProjectTitle.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextTitleSmall( - text = context.getString(R.string.about_project_title), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(top = 16.dp, bottom = 8.dp, start = 16.dp, end = 16.dp), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setWebsiteLabel(view: View) { - val websiteLabel = view.findViewById(R.id.websiteLabel) - websiteLabel.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextTitleMedium( - text = context.getString(R.string.about_website_title), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setWebsiteText(view: View) { - val website = view.findViewById(R.id.website) - website.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextBodyMedium( - text = context.getString(R.string.app_webpage_url), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setForumLabel(view: View) { - val forumLabel = view.findViewById(R.id.user_forum_label) - forumLabel.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextTitleMedium( - text = context.getString(R.string.user_forum_title), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setForumText(view: View) { - val forum = view.findViewById(R.id.user_forum) - forum.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextBodyMedium( - text = context.getString(R.string.user_forum_url), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - color = MainTheme.colors.secondary, - ) - } - } - } - - private fun setLibrariesTitle(view: View) { - val librariesTitle = view.findViewById(R.id.librariesTitle) - librariesTitle.setContent { - val context = LocalContext.current - themeProvider.WithTheme { - TextTitleSmall( - text = context.getString(R.string.about_libraries), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(top = 16.dp, bottom = 8.dp, start = 16.dp, end = 16.dp), - color = MainTheme.colors.secondary, - ) + setContent { + themeProvider.WithTheme { + AboutScreen( + appName = appNameProvider.appName, + versionNumber = getVersionNumber() ?: "?", + displayChangeLog = { findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) }, + displayAuthors = { openUrl(getString(R.string.app_authors_url)) }, + displayLicense = { openUrl(getString(R.string.app_license_url)) }, + displayWebSite = { openUrl(getString(R.string.app_webpage_url)) }, + displayForum = { openUrl(getString(R.string.user_forum_url)) }, + ) + } } } } - private fun displayChangeLog() { - findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) - } - private fun getVersionNumber(): String? { return try { val context = requireContext() @@ -456,105 +89,6 @@ class AboutFragment : Fragment() { null } } - - companion object { - private val USED_LIBRARIES = persistentListOf( - Library( - "Android Jetpack libraries", - "https://developer.android.com/jetpack", - "Apache License, Version 2.0", - ), - Library( - "AndroidX Preference extended", - "https://github.com/takisoft/preferencex-android", - "Apache License, Version 2.0", - ), - Library("AppAuth for Android", "https://github.com/openid/AppAuth-Android", "Apache License, Version 2.0"), - Library("Apache HttpComponents", "https://hc.apache.org/", "Apache License, Version 2.0"), - Library("AutoValue", "https://github.com/google/auto", "Apache License, Version 2.0"), - Library("CircleImageView", "https://github.com/hdodenhof/CircleImageView", "Apache License, Version 2.0"), - Library("ckChangeLog", "https://github.com/cketti/ckChangeLog", "Apache License, Version 2.0"), - Library("Commons IO", "https://commons.apache.org/io/", "Apache License, Version 2.0"), - Library("ColorPicker", "https://github.com/gregkorossy/ColorPicker", "Apache License, Version 2.0"), - Library("DateTimePicker", "https://github.com/gregkorossy/DateTimePicker", "Apache License, Version 2.0"), - Library("Error Prone annotations", "https://github.com/google/error-prone", "Apache License, Version 2.0"), - Library("FlexboxLayout", "https://github.com/google/flexbox-layout", "Apache License, Version 2.0"), - Library("FastAdapter", "https://github.com/mikepenz/FastAdapter", "Apache License, Version 2.0"), - Library("Glide", "https://github.com/bumptech/glide", "BSD, part MIT and Apache 2.0"), - Library("jsoup", "https://jsoup.org/", "MIT License"), - Library("jutf7", "http://jutf7.sourceforge.net/", "MIT License"), - Library("JZlib", "http://www.jcraft.com/jzlib/", "BSD-style License"), - Library("jcip-annotations", "https://jcip.net/", "Public License"), - Library( - "Jetbrains Annotations for JVM-based languages", - "https://github.com/JetBrains/java-annotations", - "Apache License, Version 2.0", - ), - Library( - "Jetbrains Compose Runtime", - "https://github.com/JetBrains/compose-multiplatform-core", - "Apache License, Version 2.0", - ), - Library("Koin", "https://insert-koin.io/", "Apache License, Version 2.0"), - Library( - "Kotlin Android Extensions Runtime", - "https://github.com/JetBrains/kotlin/tree/master/plugins/android-extensions/android-extensions-runtime", - "Apache License, Version 2.0", - ), - Library("Kotlin Parcelize Runtime", "https://github.com/JetBrains/kotlin", "Apache License, Version 2.0"), - Library( - "Kotlin Standard Library", - "https://kotlinlang.org/api/latest/jvm/stdlib/", - "Apache License, Version 2.0", - ), - Library( - "KotlinX Coroutines", - "https://github.com/Kotlin/kotlinx.coroutines", - "Apache License, Version 2.0", - ), - Library("KotlinX DateTime", "https://github.com/Kotlin/kotlinx-datetime", "Apache License, Version 2.0"), - Library( - "KotlinX Immutable Collections", - "https://github.com/Kotlin/kotlinx.collections.immutable", - "Apache License, Version 2.0", - ), - Library( - "KotlinX Serialization", - "https://github.com/Kotlin/kotlinx.serialization", - "Apache License, Version 2.0", - ), - Library( - "ListenableFuture", - "https://github.com/google/guava", - "Apache License, Version 2.0", - ), - Library( - "Material Components for Android", - "https://github.com/material-components/material-components-android", - "Apache License, Version 2.0", - ), - Library("Mime4j", "https://james.apache.org/mime4j/", "Apache License, Version 2.0"), - Library("MiniDNS", "https://github.com/MiniDNS/minidns", "Multiple, Apache License, Version 2.0"), - Library("Moshi", "https://github.com/square/moshi", "Apache License, Version 2.0"), - Library("OkHttp", "https://github.com/square/okhttp", "Apache License, Version 2.0"), - Library("Okio", "https://github.com/square/okio", "Apache License, Version 2.0"), - Library( - "SafeContentResolver", - "https://github.com/cketti/SafeContentResolver", - "Apache License, Version 2.0", - ), - Library("SearchPreference", "https://github.com/ByteHamster/SearchPreference", "MIT License"), - Library("SLF4J", "https://www.slf4j.org/", "MIT License"), - Library("Stately", "https://github.com/touchlab/Stately", "Apache License, Version 2.0"), - Library("Timber", "https://github.com/JakeWharton/timber", "Apache License, Version 2.0"), - Library( - "TokenAutoComplete", - "https://github.com/splitwise/TokenAutoComplete/", - "Apache License, Version 2.0", - ), - Library("ZXing", "https://github.com/zxing/zxing", "Apache License, Version 2.0") - ) - } } private fun Fragment.openUrl(url: String) = requireContext().openUrl(url) @@ -612,5 +146,259 @@ fun LibraryItem( } } +@Composable +fun AboutScreen( + appName: String, + versionNumber: String, + modifier: Modifier = Modifier, + displayChangeLog: () -> Unit = {}, + displayAuthors: () -> Unit = {}, + displayLicense: () -> Unit = {}, + displayWebSite: () -> Unit = {}, + displayForum: () -> Unit = {}, +) { + val scrollState = rememberScrollState() + val context = LocalContext.current + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + AppLogo() + SectionTitle(title = context.getString(R.string.about_title, appName)) + SectionContent( + sectionLabel = context.getString(R.string.version), + sectionText = versionNumber, + sectionImageId = DesignSystemR.drawable.ic_info, + onClick = displayChangeLog, + ) + SectionContent( + sectionLabel = context.getString(R.string.authors), + sectionText = context.getString(R.string.about_app_authors_k9), + secondarySectionText = context.getString(R.string.about_app_authors_thunderbird), + sectionImageId = DesignSystemR.drawable.ic_group, + onClick = displayAuthors, + ) + + SectionContent( + sectionLabel = context.getString(R.string.license), + sectionText = context.getString(R.string.app_license), + sectionImageId = DesignSystemR.drawable.ic_code, + onClick = displayLicense, + ) + + SectionTitle(title = context.getString(R.string.about_project_title)) + + SectionContent( + sectionLabel = context.getString(R.string.about_website_title), + sectionText = context.getString(R.string.app_webpage_url), + sectionImageId = DesignSystemR.drawable.ic_link, + onClick = displayWebSite, + ) + + SectionContent( + sectionLabel = context.getString(R.string.user_forum_title), + sectionText = context.getString(R.string.user_forum_url), + sectionImageId = DesignSystemR.drawable.ic_forum, + onClick = displayForum, + ) + + SectionTitle(title = context.getString(R.string.about_libraries)) + LibraryList(libraries = USED_LIBRARIES) + } +} + +@Composable +fun AppLogo(modifier: Modifier = Modifier) { + val context = LocalContext.current + val typedValue = remember { TypedValue() } + context.theme.resolveAttribute(Theme2CommonR.attr.appLogo, typedValue, true) + Row( + modifier = modifier + .fillMaxWidth() + .padding(all = 16.dp), + horizontalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(id = typedValue.resourceId), + modifier = Modifier.size(size = 100.dp), + contentDescription = null, + ) + } +} + +@Composable +fun SectionTitle(title: String, modifier: Modifier = Modifier) { + TextTitleSmall( + text = title, + modifier = modifier + .fillMaxWidth() + .padding( + start = 16.dp, + end = 16.dp, + top = 16.dp, + bottom = 8.dp, + ), + color = MainTheme.colors.secondary, + ) +} + +@Composable +fun SectionContent( + sectionLabel: String, + sectionText: String, + sectionImageId: Int, + modifier: Modifier = Modifier, + secondarySectionText: String? = null, + onClick: () -> Unit = {}, +) { + Row( + modifier = modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth() + .wrapContentHeight() + .clickable(onClick = onClick), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = sectionImageId), + modifier = Modifier + .width(24.dp) + .height(24.dp), + contentDescription = null, + ) + Spacer(modifier = Modifier.width(30.dp)) + Column( + modifier = Modifier + .weight(1f) + .wrapContentHeight(), + ) { + TextTitleMedium( + text = sectionLabel, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + + TextBodyMedium( + text = sectionText, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + + secondarySectionText?.let { + TextBodyMedium( + text = it, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + color = MainTheme.colors.secondary, + ) + } + } + } +} + @Stable data class Library(val name: String, val url: String, val license: String) + +private val USED_LIBRARIES = persistentListOf( + Library( + "Android Jetpack libraries", + "https://developer.android.com/jetpack", + "Apache License, Version 2.0", + ), + Library( + "AndroidX Preference extended", + "https://github.com/takisoft/preferencex-android", + "Apache License, Version 2.0", + ), + Library("AppAuth for Android", "https://github.com/openid/AppAuth-Android", "Apache License, Version 2.0"), + Library("Apache HttpComponents", "https://hc.apache.org/", "Apache License, Version 2.0"), + Library("AutoValue", "https://github.com/google/auto", "Apache License, Version 2.0"), + Library("CircleImageView", "https://github.com/hdodenhof/CircleImageView", "Apache License, Version 2.0"), + Library("ckChangeLog", "https://github.com/cketti/ckChangeLog", "Apache License, Version 2.0"), + Library("Commons IO", "https://commons.apache.org/io/", "Apache License, Version 2.0"), + Library("ColorPicker", "https://github.com/gregkorossy/ColorPicker", "Apache License, Version 2.0"), + Library("DateTimePicker", "https://github.com/gregkorossy/DateTimePicker", "Apache License, Version 2.0"), + Library("Error Prone annotations", "https://github.com/google/error-prone", "Apache License, Version 2.0"), + Library("FlexboxLayout", "https://github.com/google/flexbox-layout", "Apache License, Version 2.0"), + Library("FastAdapter", "https://github.com/mikepenz/FastAdapter", "Apache License, Version 2.0"), + Library("Glide", "https://github.com/bumptech/glide", "BSD, part MIT and Apache 2.0"), + Library("jsoup", "https://jsoup.org/", "MIT License"), + Library("jutf7", "http://jutf7.sourceforge.net/", "MIT License"), + Library("JZlib", "http://www.jcraft.com/jzlib/", "BSD-style License"), + Library("jcip-annotations", "https://jcip.net/", "Public License"), + Library( + "Jetbrains Annotations for JVM-based languages", + "https://github.com/JetBrains/java-annotations", + "Apache License, Version 2.0", + ), + Library( + "Jetbrains Compose Runtime", + "https://github.com/JetBrains/compose-multiplatform-core", + "Apache License, Version 2.0", + ), + Library("Koin", "https://insert-koin.io/", "Apache License, Version 2.0"), + Library( + "Kotlin Android Extensions Runtime", + "https://github.com/JetBrains/kotlin/tree/master/plugins/android-extensions/android-extensions-runtime", + "Apache License, Version 2.0", + ), + Library("Kotlin Parcelize Runtime", "https://github.com/JetBrains/kotlin", "Apache License, Version 2.0"), + Library( + "Kotlin Standard Library", + "https://kotlinlang.org/api/latest/jvm/stdlib/", + "Apache License, Version 2.0", + ), + Library( + "KotlinX Coroutines", + "https://github.com/Kotlin/kotlinx.coroutines", + "Apache License, Version 2.0", + ), + Library("KotlinX DateTime", "https://github.com/Kotlin/kotlinx-datetime", "Apache License, Version 2.0"), + Library( + "KotlinX Immutable Collections", + "https://github.com/Kotlin/kotlinx.collections.immutable", + "Apache License, Version 2.0", + ), + Library( + "KotlinX Serialization", + "https://github.com/Kotlin/kotlinx.serialization", + "Apache License, Version 2.0", + ), + Library( + "ListenableFuture", + "https://github.com/google/guava", + "Apache License, Version 2.0", + ), + Library( + "Material Components for Android", + "https://github.com/material-components/material-components-android", + "Apache License, Version 2.0", + ), + Library("Mime4j", "https://james.apache.org/mime4j/", "Apache License, Version 2.0"), + Library("MiniDNS", "https://github.com/MiniDNS/minidns", "Multiple, Apache License, Version 2.0"), + Library("Moshi", "https://github.com/square/moshi", "Apache License, Version 2.0"), + Library("OkHttp", "https://github.com/square/okhttp", "Apache License, Version 2.0"), + Library("Okio", "https://github.com/square/okio", "Apache License, Version 2.0"), + Library( + "SafeContentResolver", + "https://github.com/cketti/SafeContentResolver", + "Apache License, Version 2.0", + ), + Library("SearchPreference", "https://github.com/ByteHamster/SearchPreference", "MIT License"), + Library("SLF4J", "https://www.slf4j.org/", "MIT License"), + Library("Stately", "https://github.com/touchlab/Stately", "Apache License, Version 2.0"), + Library("Timber", "https://github.com/JakeWharton/timber", "Apache License, Version 2.0"), + Library( + "TokenAutoComplete", + "https://github.com/splitwise/TokenAutoComplete/", + "Apache License, Version 2.0", + ), + Library("ZXing", "https://github.com/zxing/zxing", "Apache License, Version 2.0"), +) From e4f04ff86e1610b47d7a481df79c9095cd5bda08 Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Thu, 1 Jan 2026 03:00:13 +0600 Subject: [PATCH 10/13] refactor(AboutFragment): removed unused fragment_about.xml --- .../com/fsck/k9/ui/settings/AboutFragment.kt | 91 ++--- .../src/main/res/layout/fragment_about.xml | 330 ------------------ 2 files changed, 48 insertions(+), 373 deletions(-) delete mode 100644 legacy/ui/legacy/src/main/res/layout/fragment_about.xml diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index ab84e38971e..c9fda17d3bf 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import app.k9mail.core.ui.compose.designsystem.atom.Surface import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleSmall @@ -53,7 +54,6 @@ import org.koin.android.ext.android.inject import app.k9mail.core.ui.legacy.designsystem.R as DesignSystemR import app.k9mail.core.ui.legacy.theme2.common.R as Theme2CommonR -@Suppress("TooManyFunctions") class AboutFragment : Fragment() { private val appNameProvider: AppNameProvider by inject() private val themeProvider: FeatureThemeProvider by inject() @@ -159,52 +159,57 @@ fun AboutScreen( ) { val scrollState = rememberScrollState() val context = LocalContext.current - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(scrollState), + Surface( + modifier = Modifier + .fillMaxSize(), ) { - AppLogo() - SectionTitle(title = context.getString(R.string.about_title, appName)) - SectionContent( - sectionLabel = context.getString(R.string.version), - sectionText = versionNumber, - sectionImageId = DesignSystemR.drawable.ic_info, - onClick = displayChangeLog, - ) - SectionContent( - sectionLabel = context.getString(R.string.authors), - sectionText = context.getString(R.string.about_app_authors_k9), - secondarySectionText = context.getString(R.string.about_app_authors_thunderbird), - sectionImageId = DesignSystemR.drawable.ic_group, - onClick = displayAuthors, - ) + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + AppLogo() + SectionTitle(title = context.getString(R.string.about_title, appName)) + SectionContent( + sectionLabel = context.getString(R.string.version), + sectionText = versionNumber, + sectionImageId = DesignSystemR.drawable.ic_info, + onClick = displayChangeLog, + ) + SectionContent( + sectionLabel = context.getString(R.string.authors), + sectionText = context.getString(R.string.about_app_authors_k9), + secondarySectionText = context.getString(R.string.about_app_authors_thunderbird), + sectionImageId = DesignSystemR.drawable.ic_group, + onClick = displayAuthors, + ) - SectionContent( - sectionLabel = context.getString(R.string.license), - sectionText = context.getString(R.string.app_license), - sectionImageId = DesignSystemR.drawable.ic_code, - onClick = displayLicense, - ) + SectionContent( + sectionLabel = context.getString(R.string.license), + sectionText = context.getString(R.string.app_license), + sectionImageId = DesignSystemR.drawable.ic_code, + onClick = displayLicense, + ) - SectionTitle(title = context.getString(R.string.about_project_title)) + SectionTitle(title = context.getString(R.string.about_project_title)) - SectionContent( - sectionLabel = context.getString(R.string.about_website_title), - sectionText = context.getString(R.string.app_webpage_url), - sectionImageId = DesignSystemR.drawable.ic_link, - onClick = displayWebSite, - ) + SectionContent( + sectionLabel = context.getString(R.string.about_website_title), + sectionText = context.getString(R.string.app_webpage_url), + sectionImageId = DesignSystemR.drawable.ic_link, + onClick = displayWebSite, + ) - SectionContent( - sectionLabel = context.getString(R.string.user_forum_title), - sectionText = context.getString(R.string.user_forum_url), - sectionImageId = DesignSystemR.drawable.ic_forum, - onClick = displayForum, - ) + SectionContent( + sectionLabel = context.getString(R.string.user_forum_title), + sectionText = context.getString(R.string.user_forum_url), + sectionImageId = DesignSystemR.drawable.ic_forum, + onClick = displayForum, + ) - SectionTitle(title = context.getString(R.string.about_libraries)) - LibraryList(libraries = USED_LIBRARIES) + SectionTitle(title = context.getString(R.string.about_libraries)) + LibraryList(libraries = USED_LIBRARIES) + } } } @@ -254,10 +259,10 @@ fun SectionContent( ) { Row( modifier = modifier + .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 8.dp) .fillMaxWidth() - .wrapContentHeight() - .clickable(onClick = onClick), + .wrapContentHeight(), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, ) { diff --git a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml b/legacy/ui/legacy/src/main/res/layout/fragment_about.xml deleted file mode 100644 index 41ce511b28f..00000000000 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ /dev/null @@ -1,330 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 4642e0bf356aa72707835b2105e77af44eb59df0 Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Sat, 3 Jan 2026 10:14:06 +0600 Subject: [PATCH 11/13] refactor(AboutFragment): introduce UDF -Introduce AboutContract and enabled MVI. -Move complex logic to AboutViewModel. --- .../appVersion/DefaultAppVersionProvider.kt | 23 ++ .../app/common/core/AppCommonCoreModule.kt | 4 + .../common/provider/AppVersionProvider.kt | 7 + .../main/java/com/fsck/k9/ui/KoinModule.kt | 3 + .../com/fsck/k9/ui/settings/AboutContract.kt | 29 ++ .../com/fsck/k9/ui/settings/AboutFragment.kt | 251 +++++++----------- .../com/fsck/k9/ui/settings/AboutViewModel.kt | 122 +++++++++ .../java/com/fsck/k9/ui/settings/Library.kt | 6 + .../res/navigation/navigation_settings.xml | 1 - 9 files changed, 290 insertions(+), 156 deletions(-) create mode 100644 app-common/src/main/kotlin/net/thunderbird/app/common/appVersion/DefaultAppVersionProvider.kt create mode 100644 core/common/src/commonMain/kotlin/net/thunderbird/core/common/provider/AppVersionProvider.kt create mode 100644 legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutContract.kt create mode 100644 legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutViewModel.kt create mode 100644 legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/Library.kt diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/appVersion/DefaultAppVersionProvider.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/appVersion/DefaultAppVersionProvider.kt new file mode 100644 index 00000000000..00ebc49184a --- /dev/null +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/appVersion/DefaultAppVersionProvider.kt @@ -0,0 +1,23 @@ +package net.thunderbird.app.common.appVersion + +import android.content.Context +import android.content.pm.PackageManager +import net.thunderbird.core.common.provider.AppVersionProvider +import net.thunderbird.core.logging.Logger + +private const val TAG = "DefaultAppVersionProvider" + +class DefaultAppVersionProvider( + private val context: Context, + private var logger: Logger, +) : AppVersionProvider { + override fun getVersionNumber(): String { + try { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + return packageInfo.versionName ?: "?" + } catch (e: PackageManager.NameNotFoundException) { + logger.error(TAG, e, { "Error getting PackageInfo" }) + return "?" + } + } +} diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt index 62cabdb846c..c815d39af04 100644 --- a/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt +++ b/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt @@ -1,9 +1,11 @@ package net.thunderbird.app.common.core import com.eygraber.uri.toAndroidUri +import net.thunderbird.app.common.appVersion.DefaultAppVersionProvider import net.thunderbird.app.common.core.configstore.appCommonCoreConfigStoreModule import net.thunderbird.app.common.core.logging.appCommonCoreLogger import net.thunderbird.app.common.core.ui.appCommonCoreUiModule +import net.thunderbird.core.common.provider.AppVersionProvider import net.thunderbird.core.file.AndroidDirectoryProvider import net.thunderbird.core.file.AndroidFileSystemManager import net.thunderbird.core.file.AndroidMimeTypeProvider @@ -50,6 +52,8 @@ val appCommonCoreModule: Module = module { } } + single { DefaultAppVersionProvider(context = androidContext(), logger = get()) } + single { AndroidMimeTypeResolver( mimeTypeProvider = get(), diff --git a/core/common/src/commonMain/kotlin/net/thunderbird/core/common/provider/AppVersionProvider.kt b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/provider/AppVersionProvider.kt new file mode 100644 index 00000000000..120f801dfc4 --- /dev/null +++ b/core/common/src/commonMain/kotlin/net/thunderbird/core/common/provider/AppVersionProvider.kt @@ -0,0 +1,7 @@ +package net.thunderbird.core.common.provider +/** + * Provides the application version. + */ +interface AppVersionProvider { + fun getVersionNumber(): String +} 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 7184308507d..9d5474c67af 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 @@ -8,8 +8,10 @@ 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.settings.AboutViewModel import com.fsck.k9.ui.share.ShareIntentBuilder import net.thunderbird.core.common.inject.getList +import org.koin.core.module.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module @@ -23,6 +25,7 @@ val uiModule = module { ) } single { get() } + viewModel { AboutViewModel(appVersionProvider = get()) } factory(named("MessageView")) { get().createForMessageView() } factory { (context: Context) -> SizeFormatter(context.resources) } factory { ShareIntentBuilder(resourceProvider = get(), textPartFinder = get(), quoteDateFormatter = get()) } diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutContract.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutContract.kt new file mode 100644 index 00000000000..d83bf82c97a --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutContract.kt @@ -0,0 +1,29 @@ +package com.fsck.k9.ui.settings + +import androidx.compose.runtime.Stable +import app.k9mail.core.ui.compose.common.mvi.UnidirectionalViewModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal interface AboutContract { + + interface ViewModel : + UnidirectionalViewModel + + @Stable + data class State( + val version: String = "", + val libraries: ImmutableList = persistentListOf(), + ) + + sealed interface Event { + data object OnChangeLogClick : Event + data class OnSectionContentClick(val url: String) : Event + data class OnLibraryClick(val library: Library) : Event + } + + sealed interface Effect { + data class OpenUrl(val url: String) : Effect + data object OpenChangeLog : Effect + } +} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt index c9fda17d3bf..b7a6be49111 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutFragment.kt @@ -3,7 +3,6 @@ package com.fsck.k9.ui.settings import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle import android.util.TypedValue @@ -19,17 +18,13 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView @@ -39,60 +34,95 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import app.k9mail.core.ui.compose.common.mvi.observe import app.k9mail.core.ui.compose.designsystem.atom.Surface import app.k9mail.core.ui.compose.designsystem.atom.text.TextBodyMedium import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleMedium import app.k9mail.core.ui.compose.designsystem.atom.text.TextTitleSmall import app.k9mail.core.ui.compose.theme2.MainTheme import com.fsck.k9.ui.R +import com.fsck.k9.ui.settings.AboutContract.Effect +import com.fsck.k9.ui.settings.AboutContract.Event import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import net.thunderbird.core.common.provider.AppNameProvider -import net.thunderbird.core.logging.legacy.Log import net.thunderbird.core.ui.theme.api.FeatureThemeProvider import org.koin.android.ext.android.inject import app.k9mail.core.ui.legacy.designsystem.R as DesignSystemR import app.k9mail.core.ui.legacy.theme2.common.R as Theme2CommonR class AboutFragment : Fragment() { - private val appNameProvider: AppNameProvider by inject() private val themeProvider: FeatureThemeProvider by inject() + private val appNameProvider: AppNameProvider by inject() + private val viewModel: AboutViewModel by inject() + @Suppress("LongMethod") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return ComposeView(requireContext()).apply { setViewCompositionStrategy( ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, ) + val appLogoResId = resolveAppLogoResId(requireContext()) + val aboutTitle = context.getString(R.string.about_title, appNameProvider.appName) + val projectTitle = context.getString(R.string.about_project_title) + val librariesTitle = context.getString(R.string.about_libraries) + setContent { + val (state, dispatch) = viewModel.observe { effect -> + when (effect) { + is Effect.OpenChangeLog -> + findNavController() + .navigate(R.id.action_aboutScreen_to_changelogScreen) + + is Effect.OpenUrl -> + context.openUrl(effect.url) + } + } themeProvider.WithTheme { AboutScreen( - appName = appNameProvider.appName, - versionNumber = getVersionNumber() ?: "?", - displayChangeLog = { findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) }, - displayAuthors = { openUrl(getString(R.string.app_authors_url)) }, - displayLicense = { openUrl(getString(R.string.app_license_url)) }, - displayWebSite = { openUrl(getString(R.string.app_webpage_url)) }, - displayForum = { openUrl(getString(R.string.user_forum_url)) }, + versionNumber = state.value.version, + appLogoResId = appLogoResId, + libraries = state.value.libraries, + aboutTitle = aboutTitle, + projectTitle = projectTitle, + librariesTitle = librariesTitle, + displayChangeLog = { + dispatch(Event.OnChangeLogClick) + }, + displayAuthors = { + dispatch( + Event.OnSectionContentClick( + getString(R.string.app_authors_url), + ), + ) + }, + displayLicense = { + dispatch( + Event.OnSectionContentClick( + getString(R.string.app_license_url), + ), + ) + }, + displayWebSite = { + dispatch( + Event.OnSectionContentClick( + getString(R.string.app_webpage_url), + ), + ) + }, + displayForum = { + dispatch( + Event.OnSectionContentClick( + getString(R.string.user_forum_url), + ), + ) + }, ) } } } } - - private fun getVersionNumber(): String? { - return try { - val context = requireContext() - val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) - packageInfo.versionName - } catch (e: PackageManager.NameNotFoundException) { - Log.e(e, "Error getting PackageInfo") - null - } - } } -private fun Fragment.openUrl(url: String) = requireContext().openUrl(url) - private fun Context.openUrl(url: String) { try { val viewIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) @@ -121,11 +151,13 @@ fun LibraryItem( val context = LocalContext.current Column( modifier = modifier - .selectable( - selected = false, + .clickable( onClick = { context.openUrl(library.url) }, ) - .padding(horizontal = 16.dp, vertical = 12.dp) + .padding( + horizontal = MainTheme.spacings.double, + vertical = MainTheme.spacings.oneHalf, + ) .fillMaxWidth() .wrapContentHeight(), ) { @@ -134,22 +166,24 @@ fun LibraryItem( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), - color = MainTheme.colors.secondary, ) TextBodyMedium( text = library.license, modifier = Modifier .fillMaxWidth() .wrapContentHeight(), - color = MainTheme.colors.secondary, ) } } @Composable fun AboutScreen( - appName: String, versionNumber: String, + aboutTitle: String, + projectTitle: String, + librariesTitle: String, + appLogoResId: Int, + libraries: ImmutableList, modifier: Modifier = Modifier, displayChangeLog: () -> Unit = {}, displayAuthors: () -> Unit = {}, @@ -168,8 +202,8 @@ fun AboutScreen( .fillMaxSize() .verticalScroll(scrollState), ) { - AppLogo() - SectionTitle(title = context.getString(R.string.about_title, appName)) + AppLogo(logoResId = appLogoResId) + SectionTitle(title = aboutTitle) SectionContent( sectionLabel = context.getString(R.string.version), sectionText = versionNumber, @@ -191,7 +225,7 @@ fun AboutScreen( onClick = displayLicense, ) - SectionTitle(title = context.getString(R.string.about_project_title)) + SectionTitle(title = projectTitle) SectionContent( sectionLabel = context.getString(R.string.about_website_title), @@ -207,25 +241,22 @@ fun AboutScreen( onClick = displayForum, ) - SectionTitle(title = context.getString(R.string.about_libraries)) - LibraryList(libraries = USED_LIBRARIES) + SectionTitle(title = librariesTitle) + LibraryList(libraries = libraries) } } } @Composable -fun AppLogo(modifier: Modifier = Modifier) { - val context = LocalContext.current - val typedValue = remember { TypedValue() } - context.theme.resolveAttribute(Theme2CommonR.attr.appLogo, typedValue, true) +fun AppLogo(logoResId: Int, modifier: Modifier = Modifier) { Row( modifier = modifier .fillMaxWidth() - .padding(all = 16.dp), + .padding(all = MainTheme.spacings.double), horizontalArrangement = Arrangement.Center, ) { Image( - painter = painterResource(id = typedValue.resourceId), + painter = painterResource(id = logoResId), modifier = Modifier.size(size = 100.dp), contentDescription = null, ) @@ -239,12 +270,11 @@ fun SectionTitle(title: String, modifier: Modifier = Modifier) { modifier = modifier .fillMaxWidth() .padding( - start = 16.dp, - end = 16.dp, - top = 16.dp, - bottom = 8.dp, + start = MainTheme.spacings.double, + end = MainTheme.spacings.double, + top = MainTheme.spacings.double, + bottom = MainTheme.spacings.default, ), - color = MainTheme.colors.secondary, ) } @@ -260,7 +290,7 @@ fun SectionContent( Row( modifier = modifier .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 8.dp) + .padding(horizontal = MainTheme.spacings.double, vertical = MainTheme.spacings.default) .fillMaxWidth() .wrapContentHeight(), horizontalArrangement = Arrangement.Start, @@ -269,11 +299,10 @@ fun SectionContent( Image( painter = painterResource(id = sectionImageId), modifier = Modifier - .width(24.dp) - .height(24.dp), + .size(MainTheme.sizes.icon), contentDescription = null, ) - Spacer(modifier = Modifier.width(30.dp)) + Spacer(modifier = Modifier.width(MainTheme.spacings.triple)) Column( modifier = Modifier .weight(1f) @@ -284,7 +313,6 @@ fun SectionContent( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), - color = MainTheme.colors.secondary, ) TextBodyMedium( @@ -292,7 +320,6 @@ fun SectionContent( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), - color = MainTheme.colors.secondary, ) secondarySectionText?.let { @@ -301,109 +328,23 @@ fun SectionContent( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), - color = MainTheme.colors.secondary, ) } } } } -@Stable -data class Library(val name: String, val url: String, val license: String) +fun resolveAppLogoResId(context: Context): Int { + val typedValue = TypedValue() + val resolved = context.theme.resolveAttribute( + Theme2CommonR.attr.appLogo, + typedValue, + true, + ) -private val USED_LIBRARIES = persistentListOf( - Library( - "Android Jetpack libraries", - "https://developer.android.com/jetpack", - "Apache License, Version 2.0", - ), - Library( - "AndroidX Preference extended", - "https://github.com/takisoft/preferencex-android", - "Apache License, Version 2.0", - ), - Library("AppAuth for Android", "https://github.com/openid/AppAuth-Android", "Apache License, Version 2.0"), - Library("Apache HttpComponents", "https://hc.apache.org/", "Apache License, Version 2.0"), - Library("AutoValue", "https://github.com/google/auto", "Apache License, Version 2.0"), - Library("CircleImageView", "https://github.com/hdodenhof/CircleImageView", "Apache License, Version 2.0"), - Library("ckChangeLog", "https://github.com/cketti/ckChangeLog", "Apache License, Version 2.0"), - Library("Commons IO", "https://commons.apache.org/io/", "Apache License, Version 2.0"), - Library("ColorPicker", "https://github.com/gregkorossy/ColorPicker", "Apache License, Version 2.0"), - Library("DateTimePicker", "https://github.com/gregkorossy/DateTimePicker", "Apache License, Version 2.0"), - Library("Error Prone annotations", "https://github.com/google/error-prone", "Apache License, Version 2.0"), - Library("FlexboxLayout", "https://github.com/google/flexbox-layout", "Apache License, Version 2.0"), - Library("FastAdapter", "https://github.com/mikepenz/FastAdapter", "Apache License, Version 2.0"), - Library("Glide", "https://github.com/bumptech/glide", "BSD, part MIT and Apache 2.0"), - Library("jsoup", "https://jsoup.org/", "MIT License"), - Library("jutf7", "http://jutf7.sourceforge.net/", "MIT License"), - Library("JZlib", "http://www.jcraft.com/jzlib/", "BSD-style License"), - Library("jcip-annotations", "https://jcip.net/", "Public License"), - Library( - "Jetbrains Annotations for JVM-based languages", - "https://github.com/JetBrains/java-annotations", - "Apache License, Version 2.0", - ), - Library( - "Jetbrains Compose Runtime", - "https://github.com/JetBrains/compose-multiplatform-core", - "Apache License, Version 2.0", - ), - Library("Koin", "https://insert-koin.io/", "Apache License, Version 2.0"), - Library( - "Kotlin Android Extensions Runtime", - "https://github.com/JetBrains/kotlin/tree/master/plugins/android-extensions/android-extensions-runtime", - "Apache License, Version 2.0", - ), - Library("Kotlin Parcelize Runtime", "https://github.com/JetBrains/kotlin", "Apache License, Version 2.0"), - Library( - "Kotlin Standard Library", - "https://kotlinlang.org/api/latest/jvm/stdlib/", - "Apache License, Version 2.0", - ), - Library( - "KotlinX Coroutines", - "https://github.com/Kotlin/kotlinx.coroutines", - "Apache License, Version 2.0", - ), - Library("KotlinX DateTime", "https://github.com/Kotlin/kotlinx-datetime", "Apache License, Version 2.0"), - Library( - "KotlinX Immutable Collections", - "https://github.com/Kotlin/kotlinx.collections.immutable", - "Apache License, Version 2.0", - ), - Library( - "KotlinX Serialization", - "https://github.com/Kotlin/kotlinx.serialization", - "Apache License, Version 2.0", - ), - Library( - "ListenableFuture", - "https://github.com/google/guava", - "Apache License, Version 2.0", - ), - Library( - "Material Components for Android", - "https://github.com/material-components/material-components-android", - "Apache License, Version 2.0", - ), - Library("Mime4j", "https://james.apache.org/mime4j/", "Apache License, Version 2.0"), - Library("MiniDNS", "https://github.com/MiniDNS/minidns", "Multiple, Apache License, Version 2.0"), - Library("Moshi", "https://github.com/square/moshi", "Apache License, Version 2.0"), - Library("OkHttp", "https://github.com/square/okhttp", "Apache License, Version 2.0"), - Library("Okio", "https://github.com/square/okio", "Apache License, Version 2.0"), - Library( - "SafeContentResolver", - "https://github.com/cketti/SafeContentResolver", - "Apache License, Version 2.0", - ), - Library("SearchPreference", "https://github.com/ByteHamster/SearchPreference", "MIT License"), - Library("SLF4J", "https://www.slf4j.org/", "MIT License"), - Library("Stately", "https://github.com/touchlab/Stately", "Apache License, Version 2.0"), - Library("Timber", "https://github.com/JakeWharton/timber", "Apache License, Version 2.0"), - Library( - "TokenAutoComplete", - "https://github.com/splitwise/TokenAutoComplete/", - "Apache License, Version 2.0", - ), - Library("ZXing", "https://github.com/zxing/zxing", "Apache License, Version 2.0"), -) + return if (resolved && typedValue.resourceId != 0) { + typedValue.resourceId + } else { + 0 + } +} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutViewModel.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutViewModel.kt new file mode 100644 index 00000000000..27656d68fd3 --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/AboutViewModel.kt @@ -0,0 +1,122 @@ +package com.fsck.k9.ui.settings + +import app.k9mail.core.ui.compose.common.mvi.BaseViewModel +import com.fsck.k9.ui.settings.AboutContract.Effect +import com.fsck.k9.ui.settings.AboutContract.Event +import com.fsck.k9.ui.settings.AboutContract.State +import kotlinx.collections.immutable.persistentListOf +import net.thunderbird.core.common.provider.AppVersionProvider + +internal class AboutViewModel( + appVersionProvider: AppVersionProvider, +) : BaseViewModel( + initialState = State( + version = appVersionProvider.getVersionNumber(), + libraries = USED_LIBRARIES, + ), +) { + override fun event(event: Event) { + when (event) { + is Event.OnChangeLogClick -> emitEffect(Effect.OpenChangeLog) + is Event.OnSectionContentClick -> emitEffect(Effect.OpenUrl(event.url)) + is Event.OnLibraryClick -> emitEffect(Effect.OpenUrl(event.library.url)) + } + } +} + +private val USED_LIBRARIES = persistentListOf( + Library( + "Android Jetpack libraries", + "https://developer.android.com/jetpack", + "Apache License, Version 2.0", + ), + Library( + "AndroidX Preference extended", + "https://github.com/takisoft/preferencex-android", + "Apache License, Version 2.0", + ), + Library("AppAuth for Android", "https://github.com/openid/AppAuth-Android", "Apache License, Version 2.0"), + Library("Apache HttpComponents", "https://hc.apache.org/", "Apache License, Version 2.0"), + Library("AutoValue", "https://github.com/google/auto", "Apache License, Version 2.0"), + Library("CircleImageView", "https://github.com/hdodenhof/CircleImageView", "Apache License, Version 2.0"), + Library("ckChangeLog", "https://github.com/cketti/ckChangeLog", "Apache License, Version 2.0"), + Library("Commons IO", "https://commons.apache.org/io/", "Apache License, Version 2.0"), + Library("ColorPicker", "https://github.com/gregkorossy/ColorPicker", "Apache License, Version 2.0"), + Library("DateTimePicker", "https://github.com/gregkorossy/DateTimePicker", "Apache License, Version 2.0"), + Library("Error Prone annotations", "https://github.com/google/error-prone", "Apache License, Version 2.0"), + Library("FlexboxLayout", "https://github.com/google/flexbox-layout", "Apache License, Version 2.0"), + Library("FastAdapter", "https://github.com/mikepenz/FastAdapter", "Apache License, Version 2.0"), + Library("Glide", "https://github.com/bumptech/glide", "BSD, part MIT and Apache 2.0"), + Library("jsoup", "https://jsoup.org/", "MIT License"), + Library("jutf7", "http://jutf7.sourceforge.net/", "MIT License"), + Library("JZlib", "http://www.jcraft.com/jzlib/", "BSD-style License"), + Library("jcip-annotations", "https://jcip.net/", "Public License"), + Library( + "Jetbrains Annotations for JVM-based languages", + "https://github.com/JetBrains/java-annotations", + "Apache License, Version 2.0", + ), + Library( + "Jetbrains Compose Runtime", + "https://github.com/JetBrains/compose-multiplatform-core", + "Apache License, Version 2.0", + ), + Library("Koin", "https://insert-koin.io/", "Apache License, Version 2.0"), + Library( + "Kotlin Android Extensions Runtime", + "https://github.com/JetBrains/kotlin/tree/master/plugins/android-extensions/android-extensions-runtime", + "Apache License, Version 2.0", + ), + Library("Kotlin Parcelize Runtime", "https://github.com/JetBrains/kotlin", "Apache License, Version 2.0"), + Library( + "Kotlin Standard Library", + "https://kotlinlang.org/api/latest/jvm/stdlib/", + "Apache License, Version 2.0", + ), + Library( + "KotlinX Coroutines", + "https://github.com/Kotlin/kotlinx.coroutines", + "Apache License, Version 2.0", + ), + Library("KotlinX DateTime", "https://github.com/Kotlin/kotlinx-datetime", "Apache License, Version 2.0"), + Library( + "KotlinX Immutable Collections", + "https://github.com/Kotlin/kotlinx.collections.immutable", + "Apache License, Version 2.0", + ), + Library( + "KotlinX Serialization", + "https://github.com/Kotlin/kotlinx.serialization", + "Apache License, Version 2.0", + ), + Library( + "ListenableFuture", + "https://github.com/google/guava", + "Apache License, Version 2.0", + ), + Library( + "Material Components for Android", + "https://github.com/material-components/material-components-android", + "Apache License, Version 2.0", + ), + Library("Mime4j", "https://james.apache.org/mime4j/", "Apache License, Version 2.0"), + Library("MiniDNS", "https://github.com/MiniDNS/minidns", "Multiple, Apache License, Version 2.0"), + Library("Moshi", "https://github.com/square/moshi", "Apache License, Version 2.0"), + Library("OkHttp", "https://github.com/square/okhttp", "Apache License, Version 2.0"), + Library("Okio", "https://github.com/square/okio", "Apache License, Version 2.0"), + Library( + "SafeContentResolver", + "https://github.com/cketti/SafeContentResolver", + "Apache License, Version 2.0", + ), + Library("SearchPreference", "https://github.com/ByteHamster/SearchPreference", "MIT License"), + Library("SLF4J", "https://www.slf4j.org/", "MIT License"), + Library("Stately", "https://github.com/touchlab/Stately", "Apache License, Version 2.0"), + Library("Timber", "https://github.com/JakeWharton/timber", "Apache License, Version 2.0"), + Library( + "TokenAutoComplete", + "https://github.com/splitwise/TokenAutoComplete/", + "Apache License, Version 2.0", + ), + Library("ZXing", "https://github.com/zxing/zxing", "Apache License, Version 2.0"), +) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/Library.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/Library.kt new file mode 100644 index 00000000000..1a42dd6e0af --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/settings/Library.kt @@ -0,0 +1,6 @@ +package com.fsck.k9.ui.settings + +import androidx.compose.runtime.Stable + +@Stable +data class Library(val name: String, val url: String, val license: String) diff --git a/legacy/ui/legacy/src/main/res/navigation/navigation_settings.xml b/legacy/ui/legacy/src/main/res/navigation/navigation_settings.xml index ce61f9279ff..862b1dd9725 100644 --- a/legacy/ui/legacy/src/main/res/navigation/navigation_settings.xml +++ b/legacy/ui/legacy/src/main/res/navigation/navigation_settings.xml @@ -44,7 +44,6 @@ android:id="@+id/aboutScreen" android:name="com.fsck.k9.ui.settings.AboutFragment" android:label="@string/about_action" - tools:layout="@layout/fragment_about" > Date: Sat, 3 Jan 2026 10:48:03 +0600 Subject: [PATCH 12/13] test: add test coverage for AboutViewModel --- .../fsck/k9/ui/settings/AboutViewModelTest.kt | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 legacy/ui/legacy/src/test/java/com/fsck/k9/ui/settings/AboutViewModelTest.kt diff --git a/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/settings/AboutViewModelTest.kt b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/settings/AboutViewModelTest.kt new file mode 100644 index 00000000000..75472cc9479 --- /dev/null +++ b/legacy/ui/legacy/src/test/java/com/fsck/k9/ui/settings/AboutViewModelTest.kt @@ -0,0 +1,69 @@ +package com.fsck.k9.ui.settings + +import app.cash.turbine.test +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.fsck.k9.ui.settings.AboutContract.Effect +import com.fsck.k9.ui.settings.AboutContract.Event +import kotlin.test.Test +import kotlinx.coroutines.test.runTest +import net.thunderbird.core.common.provider.AppVersionProvider +import org.junit.Before +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +class AboutViewModelTest { + + private lateinit var viewModel: AboutViewModel + + @Before + fun setUp() { + viewModel = AboutViewModel( + appVersionProvider = mock { + on { getVersionNumber() } doReturn "9.9.9" + }, + ) + } + + @Test + fun `initial state contains app version and libraries`() { + val state = viewModel.state.value + assertThat(state.version, "9.9.9") + assertThat(state.libraries.isNotEmpty()).isEqualTo(true) + } + + @Test + fun `OnChangeLogClick emits OpenChangeLog effect`() = runTest { + viewModel.effect.test { + viewModel.event(Event.OnChangeLogClick) + + assertThat(awaitItem()).isEqualTo(Effect.OpenChangeLog) + } + } + + @Test + fun `OnSectionContentClick emits OpenUrl with correct url`() = runTest { + val url = "https://example.com" + + viewModel.effect.test { + viewModel.event(Event.OnSectionContentClick(url)) + + assertThat(awaitItem()).isEqualTo(Effect.OpenUrl(url)) + } + } + + @Test + fun `OnLibraryClick emits OpenUrl with library url`() = runTest { + val library = Library( + name = "TestLib", + url = "https://test.lib", + license = "Apache", + ) + + viewModel.effect.test { + viewModel.event(Event.OnLibraryClick(library)) + + assertThat(Effect.OpenUrl("https://test.lib")).isEqualTo(awaitItem()) + } + } +} From df92e174fdfcc40f076853e7affa81d328411a5a Mon Sep 17 00:00:00 2001 From: shamim-emon Date: Sat, 3 Jan 2026 19:41:55 +0600 Subject: [PATCH 13/13] feat: add AboutScreen preview --- .../fsck/k9/ui/settings/AboutScreenPreview.kt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/settings/AboutScreenPreview.kt diff --git a/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/settings/AboutScreenPreview.kt b/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/settings/AboutScreenPreview.kt new file mode 100644 index 00000000000..e42e770a3b4 --- /dev/null +++ b/legacy/ui/legacy/src/debug/kotlin/com/fsck/k9/ui/settings/AboutScreenPreview.kt @@ -0,0 +1,44 @@ +package com.fsck.k9.ui.settings + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewLightDark +import app.k9mail.core.ui.compose.designsystem.PreviewWithThemesLightDark +import app.k9mail.core.ui.compose.theme2.k9mail.R +import kotlinx.collections.immutable.persistentListOf + +@Composable +@PreviewLightDark +internal fun AboutScreenPreview() { + PreviewWithThemesLightDark { + AboutScreen( + aboutTitle = "About K-9 Mail", + projectTitle = "Open Source Project", + librariesTitle = "Libraries", + versionNumber = "17.0a1", + appLogoResId = R.drawable.core_ui_theme2_k9mail_logo, + libraries = fakeLibraryList, + ) + } +} + +private val fakeLibraryList = persistentListOf( + Library( + "Android Jetpack libraries", + "https://developer.android.com/jetpack", + "Apache License, Version 2.0", + ), + Library( + "AndroidX Preference extended", + "https://github.com/takisoft/preferencex-android", + "Apache License, Version 2.0", + ), + Library("AppAuth for Android", "https://github.com/openid/AppAuth-Android", "Apache License, Version 2.0"), + Library("Apache HttpComponents", "https://hc.apache.org/", "Apache License, Version 2.0"), + Library("AutoValue", "https://github.com/google/auto", "Apache License, Version 2.0"), + Library("CircleImageView", "https://github.com/hdodenhof/CircleImageView", "Apache License, Version 2.0"), + Library("ckChangeLog", "https://github.com/cketti/ckChangeLog", "Apache License, Version 2.0"), + Library("Commons IO", "https://commons.apache.org/io/", "Apache License, Version 2.0"), + Library("ColorPicker", "https://github.com/gregkorossy/ColorPicker", "Apache License, Version 2.0"), + Library("DateTimePicker", "https://github.com/gregkorossy/DateTimePicker", "Apache License, Version 2.0"), + Library("Error Prone annotations", "https://github.com/google/error-prone", "Apache License, Version 2.0"), +)