diff --git a/app/k9mail-jmap/src/test/java/com/fsck/k9/DependencyInjectionTest.kt b/app/k9mail-jmap/src/test/java/com/fsck/k9/DependencyInjectionTest.kt index 3ec888d0f..880498b64 100644 --- a/app/k9mail-jmap/src/test/java/com/fsck/k9/DependencyInjectionTest.kt +++ b/app/k9mail-jmap/src/test/java/com/fsck/k9/DependencyInjectionTest.kt @@ -2,6 +2,8 @@ package com.fsck.k9 import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner +import com.fsck.k9.ui.changelog.ChangeLogMode +import com.fsck.k9.ui.changelog.ChangelogViewModel import com.fsck.k9.ui.endtoend.AutocryptKeyTransferActivity import com.fsck.k9.ui.endtoend.AutocryptKeyTransferPresenter import com.fsck.k9.ui.folders.FolderNameFormatter @@ -39,6 +41,7 @@ class DependencyInjectionTest : AutoCloseKoinTest() { create { parametersOf(lifecycleOwner, autocryptTransferView) } create { parametersOf(RuntimeEnvironment.application) } create { parametersOf(RuntimeEnvironment.application) } + create { parametersOf(ChangeLogMode.CHANGE_LOG) } } } } diff --git a/app/k9mail/src/main/AndroidManifest.xml b/app/k9mail/src/main/AndroidManifest.xml index ea8ce0204..f441f4032 100644 --- a/app/k9mail/src/main/AndroidManifest.xml +++ b/app/k9mail/src/main/AndroidManifest.xml @@ -290,6 +290,10 @@ android:name=".ui.messagesource.MessageSourceActivity" android:label="@string/show_headers_action" /> + + diff --git a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt index 3ec888d0f..880498b64 100644 --- a/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt +++ b/app/k9mail/src/test/java/com/fsck/k9/DependencyInjectionTest.kt @@ -2,6 +2,8 @@ package com.fsck.k9 import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner +import com.fsck.k9.ui.changelog.ChangeLogMode +import com.fsck.k9.ui.changelog.ChangelogViewModel import com.fsck.k9.ui.endtoend.AutocryptKeyTransferActivity import com.fsck.k9.ui.endtoend.AutocryptKeyTransferPresenter import com.fsck.k9.ui.folders.FolderNameFormatter @@ -39,6 +41,7 @@ class DependencyInjectionTest : AutoCloseKoinTest() { create { parametersOf(lifecycleOwner, autocryptTransferView) } create { parametersOf(RuntimeEnvironment.application) } create { parametersOf(RuntimeEnvironment.application) } + create { parametersOf(ChangeLogMode.CHANGE_LOG) } } } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt index 8fc7f0c6b..913dc10e1 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/activity/MessageList.kt @@ -1,5 +1,6 @@ package com.fsck.k9.activity +import android.annotation.SuppressLint import android.app.SearchManager import android.content.Context import android.content.Intent @@ -21,6 +22,7 @@ import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout.DrawerListener import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction +import androidx.lifecycle.Observer import com.fsck.k9.Account import com.fsck.k9.Account.SortType import com.fsck.k9.K9 @@ -47,6 +49,8 @@ import com.fsck.k9.ui.K9Drawer import com.fsck.k9.ui.R import com.fsck.k9.ui.base.K9Activity import com.fsck.k9.ui.base.Theme +import com.fsck.k9.ui.changelog.RecentChangesActivity +import com.fsck.k9.ui.changelog.RecentChangesViewModel import com.fsck.k9.ui.managefolders.ManageFoldersActivity import com.fsck.k9.ui.messagelist.DefaultFolderProvider import com.fsck.k9.ui.messagesource.MessageSourceActivity @@ -59,8 +63,10 @@ import com.fsck.k9.ui.permissions.Permission import com.fsck.k9.ui.permissions.PermissionUiHelper import com.fsck.k9.view.ViewSwitcher import com.fsck.k9.view.ViewSwitcher.OnSwitchCompleteListener +import com.google.android.material.snackbar.Snackbar import com.mikepenz.materialdrawer.util.getOptimalDrawerWidth import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.component.KoinComponent import org.koin.core.component.inject import timber.log.Timber @@ -78,6 +84,8 @@ open class MessageList : OnSwitchCompleteListener, PermissionUiHelper { + private val recentChangesViewModel: RecentChangesViewModel by viewModel() + protected val searchStatusManager: SearchStatusManager by inject() private val preferences: Preferences by inject() private val channelUtils: NotificationChannelManager by inject() @@ -119,6 +127,7 @@ open class MessageList : */ private var messageListWasDisplayed = false private var viewSwitcher: ViewSwitcher? = null + private lateinit var recentChangesSnackbar: Snackbar public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -182,6 +191,7 @@ open class MessageList : initializeLayout() initializeFragments() displayViews() + initializeRecentChangesSnackbar() channelUtils.updateChannels() if (savedInstanceState == null) { @@ -324,6 +334,31 @@ open class MessageList : } } + private val shouldShowRecentChangesHintObserver = Observer { showRecentChangesHint -> + val recentChangesSnackbarVisible = recentChangesSnackbar.isShown + if (showRecentChangesHint && !recentChangesSnackbarVisible) { + recentChangesSnackbar.show() + } else if (!showRecentChangesHint && recentChangesSnackbarVisible) { + recentChangesSnackbar.dismiss() + } + } + + @SuppressLint("ShowToast") + private fun initializeRecentChangesSnackbar() { + recentChangesSnackbar = Snackbar + .make(findViewById(R.id.container), R.string.changelog_snackbar_text, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.okay_action) { launchRecentChangesActivity() } + + recentChangesViewModel.shouldShowRecentChangesHint.observe(this, shouldShowRecentChangesHintObserver) + } + + private fun launchRecentChangesActivity() { + recentChangesViewModel.shouldShowRecentChangesHint.removeObserver(shouldShowRecentChangesHintObserver) + + val intent = Intent(this, RecentChangesActivity::class.java) + startActivity(intent) + } + private fun decodeExtras(intent: Intent): Boolean { val launchData = decodeExtrasToLaunchData(intent) diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangeLogManager.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangeLogManager.kt new file mode 100644 index 000000000..391f56c3d --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangeLogManager.kt @@ -0,0 +1,46 @@ +package com.fsck.k9.ui.changelog + +import android.content.Context +import de.cketti.changelog.ChangeLog +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.onSubscription +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * Manages a [ChangeLog] instance and notifies when its state changes. + */ +class ChangeLogManager( + private val context: Context, + private val appCoroutineScope: CoroutineScope, + private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO +) { + private val mutableChangeLogFlow = MutableSharedFlow(replay = 1) + + val changeLog: ChangeLog by lazy { + ChangeLog.newInstance(context).also { changeLog -> + mutableChangeLogFlow.tryEmit(changeLog) + } + } + + val changeLogFlow: Flow by lazy { + mutableChangeLogFlow.onSubscription { + withContext(backgroundDispatcher) { + // Make sure the changeLog property is initialized now if it hasn't happened before + changeLog + } + } + } + + fun writeCurrentVersion() { + appCoroutineScope.launch(backgroundDispatcher) { + changeLog.writeCurrentVersion() + + mutableChangeLogFlow.emit(changeLog) + } + } +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangelogFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangelogFragment.kt index d410a1ab2..d3860465b 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangelogFragment.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangelogFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.CheckBox import android.widget.TextView import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView @@ -11,13 +12,17 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.fsck.k9.ui.R import com.fsck.k9.ui.base.loader.observeLoading import de.cketti.changelog.ReleaseItem -import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf /** * Displays the changelog entries in a scrolling list */ class ChangelogFragment : Fragment() { - private val viewModel: ChangelogViewModel by inject() + private val viewModel: ChangelogViewModel by viewModel { + val mode = arguments?.getSerializable(ARG_MODE) as? ChangeLogMode ?: error("Missing argument '$ARG_MODE'") + parametersOf(mode) + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_changelog, container, false) @@ -34,6 +39,28 @@ class ChangelogFragment : Fragment() { ) { changeLog -> listView.adapter = ChangelogAdapter(changeLog) } + + setUpShowRecentChangesCheckbox(view) + } + + private fun setUpShowRecentChangesCheckbox(view: View) { + val showRecentChangesCheckBox = view.findViewById(R.id.show_recent_changes_checkbox) + var isInitialValue = true + viewModel.showRecentChangesState.observe(viewLifecycleOwner) { showRecentChanges -> + showRecentChangesCheckBox.isChecked = showRecentChanges + if (isInitialValue) { + // Don't animate when setting initial value + showRecentChangesCheckBox.jumpDrawablesToCurrentState() + isInitialValue = false + } + } + showRecentChangesCheckBox.setOnCheckedChangeListener { _, isChecked -> + viewModel.setShowRecentChanges(isChecked) + } + } + + companion object { + const val ARG_MODE = "mode" } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangelogViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangelogViewModel.kt index 38381e7be..200c3e193 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangelogViewModel.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/ChangelogViewModel.kt @@ -1,18 +1,46 @@ package com.fsck.k9.ui.changelog -import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import com.fsck.k9.preferences.GeneralSettingsManager import com.fsck.k9.ui.base.loader.LoaderState import com.fsck.k9.ui.base.loader.liveDataLoader -import de.cketti.changelog.ChangeLog import de.cketti.changelog.ReleaseItem +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map private typealias ChangeLogState = LoaderState> -class ChangelogViewModel(private val context: Context) : ViewModel() { +class ChangelogViewModel( + private val generalSettingsManager: GeneralSettingsManager, + private val changeLogManager: ChangeLogManager, + private val mode: ChangeLogMode +) : ViewModel() { + val showRecentChangesState: LiveData = + generalSettingsManager.getSettingsFlow() + .map { it.showRecentChanges } + .distinctUntilChanged() + .asLiveData() + val changelogState: LiveData = liveDataLoader { - val changeLog = ChangeLog.newInstance(context) - changeLog.changeLog + val changeLog = changeLogManager.changeLog + when (mode) { + ChangeLogMode.CHANGE_LOG -> changeLog.changeLog + ChangeLogMode.RECENT_CHANGES -> changeLog.recentChanges + } + } + + fun setShowRecentChanges(showRecentChanges: Boolean) { + generalSettingsManager.setShowRecentChanges(showRecentChanges) + } + + override fun onCleared() { + changeLogManager.writeCurrentVersion() } } + +enum class ChangeLogMode { + CHANGE_LOG, + RECENT_CHANGES +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/KoinModule.kt index 1b2a79af6..196b074ff 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/KoinModule.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/KoinModule.kt @@ -1,8 +1,13 @@ package com.fsck.k9.ui.changelog import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.qualifier.named import org.koin.dsl.module val changelogUiModule = module { - viewModel { ChangelogViewModel(context = get()) } + single { ChangeLogManager(context = get(), appCoroutineScope = get(named("AppCoroutineScope"))) } + viewModel { (mode: ChangeLogMode) -> + ChangelogViewModel(generalSettingsManager = get(), changeLogManager = get(), mode = mode) + } + viewModel { RecentChangesViewModel(generalSettingsManager = get(), changeLogManager = get()) } } diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesActivity.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesActivity.kt new file mode 100644 index 000000000..a3f103b16 --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesActivity.kt @@ -0,0 +1,35 @@ +package com.fsck.k9.ui.changelog + +import android.os.Bundle +import android.view.MenuItem +import androidx.core.os.bundleOf +import androidx.fragment.app.commit +import com.fsck.k9.ui.R +import com.fsck.k9.ui.base.K9Activity + +class RecentChangesActivity : K9Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setLayout(R.layout.activity_recent_changes) + setTitle(R.string.changelog_recent_changes_title) + supportActionBar!!.setDisplayHomeAsUpEnabled(true) + + if (savedInstanceState == null) { + val fragment = ChangelogFragment().apply { + arguments = bundleOf(ChangelogFragment.ARG_MODE to ChangeLogMode.RECENT_CHANGES) + } + supportFragmentManager.commit { + add(R.id.fragment_container, fragment) + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return if (item.itemId == android.R.id.home) { + finish() + true + } else { + super.onOptionsItemSelected(item) + } + } +} diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesViewModel.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesViewModel.kt new file mode 100644 index 000000000..e3d8c6c54 --- /dev/null +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/changelog/RecentChangesViewModel.kt @@ -0,0 +1,31 @@ +package com.fsck.k9.ui.changelog + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import com.fsck.k9.preferences.GeneralSettingsManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@OptIn(ExperimentalCoroutinesApi::class) +class RecentChangesViewModel( + private val generalSettingsManager: GeneralSettingsManager, + changeLogManager: ChangeLogManager +) : ViewModel() { + val shouldShowRecentChangesHint = changeLogManager.changeLogFlow.flatMapLatest { changeLog -> + if (changeLog.isFirstRun && !changeLog.isFirstRunEver) { + getShowRecentChangesFlow() + } else { + flowOf(false) + } + }.asLiveData() + + private fun getShowRecentChangesFlow(): Flow { + return generalSettingsManager.getSettingsFlow() + .map { generalSettings -> generalSettings.showRecentChanges } + .distinctUntilChanged() + } +} diff --git a/app/ui/legacy/src/main/res/layout/activity_recent_changes.xml b/app/ui/legacy/src/main/res/layout/activity_recent_changes.xml new file mode 100644 index 000000000..ae38f5ac3 --- /dev/null +++ b/app/ui/legacy/src/main/res/layout/activity_recent_changes.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/app/ui/legacy/src/main/res/layout/fragment_changelog.xml b/app/ui/legacy/src/main/res/layout/fragment_changelog.xml index 72c47c352..2b963c666 100644 --- a/app/ui/legacy/src/main/res/layout/fragment_changelog.xml +++ b/app/ui/legacy/src/main/res/layout/fragment_changelog.xml @@ -1,5 +1,5 @@ - - + + + + + diff --git a/app/ui/legacy/src/main/res/navigation/navigation_settings.xml b/app/ui/legacy/src/main/res/navigation/navigation_settings.xml index e6f6a7d95..3f79ee23f 100644 --- a/app/ui/legacy/src/main/res/navigation/navigation_settings.xml +++ b/app/ui/legacy/src/main/res/navigation/navigation_settings.xml @@ -86,6 +86,24 @@ android:id="@+id/changelogScreen" android:name="com.fsck.k9.ui.changelog.ChangelogFragment" android:label="@string/changelog_title" - tools:layout="@layout/fragment_changelog"/> + tools:layout="@layout/fragment_changelog"> + + + + + + + + diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml index 691055ec2..7762cf10e 100644 --- a/app/ui/legacy/src/main/res/values/strings.xml +++ b/app/ui/legacy/src/main/res/values/strings.xml @@ -26,6 +26,9 @@ Changelog Couldn\'t load the changelog. Version %s + What\'s new + Show recent changes when app was updated + Find out what\'s new in this release