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/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"), +) 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 fa9c48cd276..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 @@ -12,323 +11,340 @@ 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.padding import androidx.compose.foundation.layout.size -import androidx.compose.runtime.remember +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +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 import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +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.google.android.material.textview.MaterialTextView +import com.fsck.k9.ui.settings.AboutContract.Effect +import com.fsck.k9.ui.settings.AboutContract.Event +import kotlinx.collections.immutable.ImmutableList 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 -@Suppress("TooManyFunctions") class AboutFragment : Fragment() { + 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 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) - - val titleTextView = view.findViewById(R.id.about_title) - titleTextView.text = getString(R.string.about_title, appNameProvider.appName) - - val versionTextView = view.findViewById(R.id.version) - versionTextView.text = getVersionNumber() ?: "?" - - 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)) - } - - val manager = LinearLayoutManager(view.context) - val librariesRecyclerView = view.findViewById(R.id.libraries) - librariesRecyclerView.apply { - layoutManager = manager - adapter = LibrariesAdapter(USED_LIBRARIES) - isNestedScrollingEnabled = false - isFocusable = false - } - } - - 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, + 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( + 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 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 Context.openUrl(url: String) { + try { + val viewIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(viewIntent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(this, R.string.error_activity_not_found, Toast.LENGTH_SHORT).show() } +} - 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, - ) +@Composable +private fun LibraryList( + libraries: ImmutableList, +) { + Column(modifier = Modifier.fillMaxWidth()) { + libraries.forEach { library -> + LibraryItem(library = library) } } +} - 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, +@Composable +fun LibraryItem( + library: Library, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + Column( + modifier = modifier + .clickable( + onClick = { context.openUrl(library.url) }, ) - } + .padding( + horizontal = MainTheme.spacings.double, + vertical = MainTheme.spacings.oneHalf, + ) + .fillMaxWidth() + .wrapContentHeight(), + ) { + TextTitleMedium( + text = library.name, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + ) + TextBodyMedium( + text = library.license, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + ) } +} - 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, +@Composable +fun AboutScreen( + versionNumber: String, + aboutTitle: String, + projectTitle: String, + librariesTitle: String, + appLogoResId: Int, + libraries: ImmutableList, + modifier: Modifier = Modifier, + displayChangeLog: () -> Unit = {}, + displayAuthors: () -> Unit = {}, + displayLicense: () -> Unit = {}, + displayWebSite: () -> Unit = {}, + displayForum: () -> Unit = {}, +) { + val scrollState = rememberScrollState() + val context = LocalContext.current + Surface( + modifier = Modifier + .fillMaxSize(), + ) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(scrollState), + ) { + AppLogo(logoResId = appLogoResId) + SectionTitle(title = aboutTitle) + 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, ) - } - } - 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, + SectionContent( + sectionLabel = context.getString(R.string.license), + sectionText = context.getString(R.string.app_license), + sectionImageId = DesignSystemR.drawable.ic_code, + onClick = displayLicense, ) - } - } - 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, + SectionTitle(title = projectTitle) + + SectionContent( + sectionLabel = context.getString(R.string.about_website_title), + sectionText = context.getString(R.string.app_webpage_url), + sectionImageId = DesignSystemR.drawable.ic_link, + onClick = displayWebSite, ) - } - } - private fun displayChangeLog() { - findNavController().navigate(R.id.action_aboutScreen_to_changelogScreen) - } + SectionContent( + sectionLabel = context.getString(R.string.user_forum_title), + sectionText = context.getString(R.string.user_forum_url), + sectionImageId = DesignSystemR.drawable.ic_forum, + onClick = displayForum, + ) - 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 + SectionTitle(title = librariesTitle) + LibraryList(libraries = libraries) } } +} - companion object { - private val USED_LIBRARIES = arrayOf( - 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"), +@Composable +fun AppLogo(logoResId: Int, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(all = MainTheme.spacings.double), + horizontalArrangement = Arrangement.Center, + ) { + Image( + painter = painterResource(id = logoResId), + modifier = Modifier.size(size = 100.dp), + contentDescription = 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)) - startActivity(viewIntent) - } catch (e: ActivityNotFoundException) { - Toast.makeText(this, R.string.error_activity_not_found, Toast.LENGTH_SHORT).show() - } +@Composable +fun SectionTitle(title: String, modifier: Modifier = Modifier) { + TextTitleSmall( + text = title, + modifier = modifier + .fillMaxWidth() + .padding( + start = MainTheme.spacings.double, + end = MainTheme.spacings.double, + top = MainTheme.spacings.double, + bottom = MainTheme.spacings.default, + ), + ) } -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) - } +@Composable +fun SectionContent( + sectionLabel: String, + sectionText: String, + sectionImageId: Int, + modifier: Modifier = Modifier, + secondarySectionText: String? = null, + onClick: () -> Unit = {}, +) { + Row( + modifier = modifier + .clickable(onClick = onClick) + .padding(horizontal = MainTheme.spacings.double, vertical = MainTheme.spacings.default) + .fillMaxWidth() + .wrapContentHeight(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = sectionImageId), + modifier = Modifier + .size(MainTheme.sizes.icon), + contentDescription = null, + ) + Spacer(modifier = Modifier.width(MainTheme.spacings.triple)) + Column( + modifier = Modifier + .weight(1f) + .wrapContentHeight(), + ) { + TextTitleMedium( + text = sectionLabel, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + ) - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.about_library, parent, false) - return ViewHolder(view) - } + TextBodyMedium( + text = sectionText, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + ) - 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) + secondarySectionText?.let { + TextBodyMedium( + text = it, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + ) + } } } +} - override fun getItemCount() = dataset.size +fun resolveAppLogoResId(context: Context): Int { + val typedValue = TypedValue() + val resolved = context.theme.resolveAttribute( + Theme2CommonR.attr.appLogo, + typedValue, + true, + ) + + 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/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 deleted file mode 100644 index ee4da0d9749..00000000000 --- a/legacy/ui/legacy/src/main/res/layout/fragment_about.xml +++ /dev/null @@ -1,360 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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" > { + 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()) + } + } +}