diff --git a/app/core/src/main/java/com/fsck/k9/K9.kt b/app/core/src/main/java/com/fsck/k9/K9.kt index 9d2b1f07be019c339b641319dc704d533610d811..8b49d17e15213d5d9de0302900c654a190b40c79 100644 --- a/app/core/src/main/java/com/fsck/k9/K9.kt +++ b/app/core/src/main/java/com/fsck/k9/K9.kt @@ -126,6 +126,10 @@ object K9 : EarlyInit { @JvmStatic var backgroundOps = BACKGROUND_OPS.ALWAYS + var isNetworkQuietTimeEnabled = true + var networkQuietTimeStarts: String? = "7:00" + var networkQuietTimeEnds: String? = "22:00" + @JvmStatic var isShowAnimations = true @@ -291,6 +295,17 @@ object K9 : EarlyInit { return quietTimeChecker.isQuietTime } + val isNetworkQuietTime: Boolean + get() { + if (!isNetworkQuietTimeEnabled) { + return false + } + + val clock = DI.get() + val quietTimeChecker = QuietTimeChecker(clock, networkQuietTimeStarts, networkQuietTimeEnds) + return quietTimeChecker.isQuietTime + } + @Synchronized @JvmStatic fun isSortAscending(sortType: SortType): Boolean { @@ -340,6 +355,11 @@ object K9 : EarlyInit { quietTimeStarts = storage.getString("quietTimeStarts", "21:00") quietTimeEnds = storage.getString("quietTimeEnds", "7:00") + isNetworkQuietTimeEnabled = storage.getBoolean("networkQuietTimeEnabled", true) + networkQuietTimeStarts = storage.getString("networkQuietTimeStarts", "22:00") + networkQuietTimeEnds = storage.getString("networkQuietTimeEnds", "7:00") + + isSentSoundEnabled = storage.getBoolean("isSentSoundEnabled", true) messageListDensity = storage.getEnum("messageListDensity", UiDensity.Default) @@ -418,6 +438,10 @@ object K9 : EarlyInit { editor.putString("quietTimeStarts", quietTimeStarts) editor.putString("quietTimeEnds", quietTimeEnds) + editor.putBoolean("networkQuietTimeEnabled", isNetworkQuietTimeEnabled) + editor.putString("networkQuietTimeStarts", networkQuietTimeStarts) + editor.putString("networkQuietTimeEnds", networkQuietTimeEnds) + editor.putEnum("messageListDensity", messageListDensity) editor.putBoolean("messageListSenderAboveSubject", isMessageListSenderAboveSubject) editor.putBoolean("showUnifiedInbox", isShowUnifiedInbox) diff --git a/app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java b/app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java index 6844b08d5ffb0242439edcd49b8d59e3de076436..b3a005115759420493309819ad84f97e114f494a 100644 --- a/app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java +++ b/app/core/src/main/java/com/fsck/k9/QuietTimeChecker.java @@ -6,13 +6,13 @@ import java.util.Calendar; import kotlinx.datetime.Clock; -class QuietTimeChecker { +public class QuietTimeChecker { private final Clock clock; private final int quietTimeStart; private final int quietTimeEnd; - QuietTimeChecker(Clock clock, String quietTimeStart, String quietTimeEnd) { + public QuietTimeChecker(Clock clock, String quietTimeStart, String quietTimeEnd) { this.clock = clock; this.quietTimeStart = parseTime(quietTimeStart); this.quietTimeEnd = parseTime(quietTimeEnd); @@ -43,4 +43,17 @@ class QuietTimeChecker { return minutesSinceMidnight >= quietTimeStart && minutesSinceMidnight <= quietTimeEnd; } } + + public int minuteToNextEndTime() { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(clock.now().toEpochMilliseconds()); + + int nowInMin = (calendar.get(Calendar.HOUR_OF_DAY) * 60) + calendar.get(Calendar.MINUTE); + + if (quietTimeEnd >= nowInMin) { + return quietTimeEnd - nowInMin; + } + + return (24 * 60) - nowInMin + quietTimeEnd; + } } diff --git a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java index 356e9abb4993733abd515fc220891302696d7027..f7abacd2f73ffdd4b50658ed8c8a35f77a772b60 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/app/core/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -531,8 +531,13 @@ public class MessagingController { ); } - public void synchronizeMailboxBlocking(Account account, String folderServerId) { - long folderId = getFolderId(account, folderServerId); + public void synchronizeMailboxBlocking(Account account, String folderServerId, boolean initialSync) { + final long folderId = getFolderId(account, folderServerId); + + if (!initialSync && K9.INSTANCE.isNetworkQuietTime() && folderId == account.getInboxFolderId()) { + Timber.i("Network quite time enabled. Skipping sync inbox"); + return; + } final CountDownLatch latch = new CountDownLatch(1); putBackground("synchronizeMailbox", null, () -> { diff --git a/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushController.kt b/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushController.kt index 2b7a284cba90a45b5a3e31d3e752b22712d6c672..fba03a2722cb1c46eee68da8513e5d2a9d5dee2b 100644 --- a/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushController.kt +++ b/app/core/src/main/java/com/fsck/k9/controller/push/AccountPushController.kt @@ -29,8 +29,8 @@ internal class AccountPushController( private var backendPusher: BackendPusher? = null private val backendPusherCallback = object : BackendPusherCallback { - override fun onPushEvent(folderServerId: String) { - syncFolders(folderServerId) + override fun onPushEvent(folderServerId: String, initialSync: Boolean) { + syncFolders(folderServerId, initialSync) } override fun onPushError(exception: Exception) { @@ -91,8 +91,8 @@ internal class AccountPushController( backendPusher?.updateFolders(folderServerIds) } - private fun syncFolders(folderServerId: String) { - messagingController.synchronizeMailboxBlocking(account, folderServerId) + private fun syncFolders(folderServerId: String, initialSync: Boolean) { + messagingController.synchronizeMailboxBlocking(account, folderServerId, initialSync) } private fun disablePush() { diff --git a/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt b/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt index 1328476521e8f93c9c3a4c3398bc52704741e48d..27c17f47e352da835fbd250e5921b90c4cfac4cb 100644 --- a/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt +++ b/app/core/src/main/java/com/fsck/k9/job/K9JobManager.kt @@ -2,6 +2,7 @@ package com.fsck.k9.job import androidx.work.WorkManager import com.fsck.k9.Account +import com.fsck.k9.K9 import com.fsck.k9.Preferences import timber.log.Timber @@ -13,6 +14,16 @@ class K9JobManager( fun scheduleAllMailJobs() { Timber.v("scheduling all jobs") scheduleMailSync() + manageSyncAfterQuiteTime() + } + + fun manageSyncAfterQuiteTime() { + if (!K9.isNetworkQuietTimeEnabled || K9.networkQuietTimeStarts == null || K9.networkQuietTimeEnds == null) { + mailSyncWorkerManager.cancelSyncAfterQuitePeriodJob() + return + } + + mailSyncWorkerManager.scheduleSyncAfterQuitePeriod() } fun scheduleMailSync(account: Account) { @@ -20,7 +31,7 @@ class K9JobManager( mailSyncWorkerManager.scheduleMailSync(account) } - private fun scheduleMailSync() { + fun scheduleMailSync() { cancelAllMailSyncJobs() preferences.accounts.forEach { account -> diff --git a/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt b/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt index 47064aaeb80145c4ad42b21a5452bf1738030c19..4c0798ae17cb54523004711ae6a9e9c613cecb08 100644 --- a/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt +++ b/app/core/src/main/java/com/fsck/k9/job/KoinModule.kt @@ -15,4 +15,7 @@ val jobModule = module { factory { (parameters: WorkerParameters) -> MailSyncWorker(messagingController = get(), preferences = get(), context = get(), parameters) } + factory { (parameters: WorkerParameters) -> + SyncAfterQuitePeriodWorker(jobManager = get(), context = get(), parameters = parameters) + } } diff --git a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt index 841e7667b82c223a6c1fd0c8820e5f9fbbd61915..63a2f566042804fcf03c50bee2daeb6939c61a6f 100644 --- a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt +++ b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorker.kt @@ -31,6 +31,11 @@ class MailSyncWorker( return Result.success() } + if (K9.isNetworkQuietTime) { + Timber.d("Running inside network quite time period. Skipping mail sync.") + return Result.success() + } + val account = preferences.getAccount(accountUuid) if (account == null) { Timber.e("Account %s not found. Can't perform mail sync.", accountUuid) diff --git a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt index a373aba3397f98522ad3039349496b907304fb69..d550ed5d7a0d0871eb8be823d8e196a0f45a8d1a 100644 --- a/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt +++ b/app/core/src/main/java/com/fsck/k9/job/MailSyncWorkerManager.kt @@ -11,6 +11,7 @@ import androidx.work.workDataOf import com.fsck.k9.Account import com.fsck.k9.K9 import com.fsck.k9.OsAccountManagerUtil +import com.fsck.k9.QuietTimeChecker import java.util.concurrent.TimeUnit import kotlinx.datetime.Clock import timber.log.Timber @@ -89,8 +90,36 @@ class MailSyncWorkerManager( return "$MAIL_SYNC_TAG:$accountUuid" } + fun scheduleSyncAfterQuitePeriod() { + if (isNeverSyncInBackground()) return + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresStorageNotLow(true) + .build() + + val syncIntervalMinutes = (24 * 60).toLong() + + val quietTimeChecker = QuietTimeChecker(clock, K9.networkQuietTimeStarts, K9.networkQuietTimeEnds) + val initialDelay = (quietTimeChecker.minuteToNextEndTime() + 1).toLong() + + val mailSyncRequest = PeriodicWorkRequestBuilder(syncIntervalMinutes, TimeUnit.MINUTES) + .setInitialDelay(initialDelay, TimeUnit.MINUTES) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, INITIAL_BACKOFF_DELAY_MINUTES, TimeUnit.MINUTES) + .setConstraints(constraints) + .addTag(SYNC_AFTER_QUITE_PERIOD_TAG) + .build() + + workManager.enqueueUniquePeriodicWork(SYNC_AFTER_QUITE_PERIOD_TAG, ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, mailSyncRequest) + } + + fun cancelSyncAfterQuitePeriodJob() { + workManager.cancelUniqueWork(SYNC_AFTER_QUITE_PERIOD_TAG) + } + companion object { const val MAIL_SYNC_TAG = "MailSync" private const val INITIAL_BACKOFF_DELAY_MINUTES = 5L + private const val SYNC_AFTER_QUITE_PERIOD_TAG = "MailSyncAfterQuitePeriod" } } diff --git a/app/core/src/main/java/com/fsck/k9/job/SyncAfterQuitePeriodWorker.kt b/app/core/src/main/java/com/fsck/k9/job/SyncAfterQuitePeriodWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..2e8ce9f8d4b89643feba97818f02cbf40784cb92 --- /dev/null +++ b/app/core/src/main/java/com/fsck/k9/job/SyncAfterQuitePeriodWorker.kt @@ -0,0 +1,33 @@ +/* + * Copyright MURENA SAS 2023 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.fsck.k9.job + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters + +class SyncAfterQuitePeriodWorker( + private val jobManager: K9JobManager, + context: Context, + parameters: WorkerParameters, +) : Worker(context, parameters) { + + override fun doWork(): Result { + jobManager.scheduleMailSync() + return Result.success() + } +} diff --git a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java index 1f2fb45007cf0d8eb26d21c5d2d58b316b06cf81..1a9b9a220a7eb79a2d5e565e0596ce380d63742b 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/GeneralSettingsDescriptions.java @@ -285,6 +285,16 @@ public class GeneralSettingsDescriptions { s.put("fontSizeMessageViewAccountName", Settings.versions( new V(87, new FontSizeSetting(FontSizes.FONT_DEFAULT)) )); + s.put("networkQuietTimeEnabled", Settings.versions( + new V(89, new BooleanSetting(true)) + )); + s.put("networkQuietTimeStarts", Settings.versions( + new V(89, new TimeSetting("7:00")) + )); + s.put("networkQuietTimeEnds", Settings.versions( + new V(89, new TimeSetting("22:00")) + )); + SETTINGS = Collections.unmodifiableMap(s); diff --git a/app/core/src/main/java/com/fsck/k9/preferences/Settings.java b/app/core/src/main/java/com/fsck/k9/preferences/Settings.java index 1aa80b3a590537511aaf36c772e787649d18f009..44199380c349dbce66c822641c45242d6b2d415f 100644 --- a/app/core/src/main/java/com/fsck/k9/preferences/Settings.java +++ b/app/core/src/main/java/com/fsck/k9/preferences/Settings.java @@ -36,7 +36,7 @@ public class Settings { * * @see SettingsExporter */ - public static final int VERSION = 88; + public static final int VERSION = 89; static Map validate(int version, Map> settings, Map importedSettings, boolean useDefaultValues) { diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt index 1fd9d4814d3db3ea945489294a43f54fa5b2e001..b208b4f2a07c1b905836821bf90820aa8fb23786 100644 --- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt +++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/settings/general/GeneralSettingsDataStore.kt @@ -40,6 +40,7 @@ class GeneralSettingsDataStore( "messageview_show_next" -> K9.isMessageViewShowNext "quiet_time_enabled" -> K9.isQuietTimeEnabled "disable_notifications_during_quiet_time" -> !K9.isNotificationDuringQuietTimeEnabled + "network_quiet_time_enabled" -> K9.isNetworkQuietTimeEnabled "sent_sound_enabled" -> K9.isSentSoundEnabled "privacy_hide_useragent" -> K9.isHideUserAgent "privacy_hide_timezone" -> K9.isHideTimeZone @@ -71,6 +72,7 @@ class GeneralSettingsDataStore( "messageview_return_to_list" -> K9.isMessageViewReturnToList = value "messageview_show_next" -> K9.isMessageViewShowNext = value "quiet_time_enabled" -> K9.isQuietTimeEnabled = value + "network_quiet_time_enabled" -> setNetworkQuiteTimeEnable(value) "disable_notifications_during_quiet_time" -> K9.isNotificationDuringQuietTimeEnabled = !value "sent_sound_enabled" -> K9.isSentSoundEnabled = value "privacy_hide_useragent" -> K9.isHideUserAgent = value @@ -113,6 +115,8 @@ class GeneralSettingsDataStore( "notification_quick_delete" -> K9.notificationQuickDeleteBehaviour.name "lock_screen_notification_visibility" -> K9.lockScreenNotificationVisibility.name "background_ops" -> K9.backgroundOps.name + "network_quiet_time_starts" -> K9.networkQuietTimeStarts + "network_quiet_time_ends" -> K9.networkQuietTimeEnds "quiet_time_starts" -> K9.quietTimeStarts "quiet_time_ends" -> K9.quietTimeEnds "message_list_subject_font" -> K9.fontSizes.messageListSubject.toString() @@ -149,6 +153,8 @@ class GeneralSettingsDataStore( K9.lockScreenNotificationVisibility = K9.LockScreenNotificationVisibility.valueOf(value) } "background_ops" -> setBackgroundOps(value) + "network_quiet_time_starts" -> setNetworkQuiteTimeStarts(value) + "network_quiet_time_ends" -> setNetworkQuiteTimeEnds(value) "quiet_time_starts" -> K9.quietTimeStarts = value "quiet_time_ends" -> K9.quietTimeEnds = value "message_list_subject_font" -> K9.fontSizes.messageListSubject = value.toInt() @@ -281,6 +287,27 @@ class GeneralSettingsDataStore( } } + private fun setNetworkQuiteTimeEnable(newValue: Boolean) { + if (newValue != K9.isNetworkQuietTimeEnabled) { + K9.isNetworkQuietTimeEnabled = newValue + jobManager.manageSyncAfterQuiteTime() + } + } + + private fun setNetworkQuiteTimeStarts(newValue: String) { + if (newValue != K9.networkQuietTimeStarts) { + K9.networkQuietTimeStarts = newValue + jobManager.manageSyncAfterQuiteTime() + } + } + + private fun setNetworkQuiteTimeEnds(newValue: String) { + if (newValue != K9.networkQuietTimeEnds) { + K9.networkQuietTimeEnds = newValue + jobManager.manageSyncAfterQuiteTime() + } + } + private fun swipeActionToString(action: SwipeAction) = when (action) { SwipeAction.None -> "none" SwipeAction.ToggleSelection -> "toggle_selection" diff --git a/app/ui/legacy/src/main/res/color/preference_text_color.xml b/app/ui/legacy/src/main/res/color/preference_text_color.xml new file mode 100644 index 0000000000000000000000000000000000000000..676bf6dc846786baabc802bde49d86823b5f9022 --- /dev/null +++ b/app/ui/legacy/src/main/res/color/preference_text_color.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/app/ui/legacy/src/main/res/layout/custom_preference.xml b/app/ui/legacy/src/main/res/layout/custom_preference.xml index 096701e5deb7da262ffacccf4c6936edaa1acb3a..1cf9eed862895758c2b208c9dfc003aa19b6f4ed 100644 --- a/app/ui/legacy/src/main/res/layout/custom_preference.xml +++ b/app/ui/legacy/src/main/res/layout/custom_preference.xml @@ -48,7 +48,7 @@ android:singleLine="true" android:textStyle="bold" android:textAppearance="?android:attr/textAppearanceLarge" - android:textColor="@color/color_default_secondary_text" + android:textColor="@color/preference_text_color" android:textSize="14sp" android:layout_marginBottom="2dp" android:ellipsize="marquee" @@ -61,7 +61,7 @@ android:layout_alignStart="@android:id/title" android:layout_alignLeft="@android:id/title" android:textAppearance="?android:attr/textAppearanceSmall" - android:textColor="@color/color_default_secondary_text" + android:textColor="@color/preference_text_color" android:textSize="12sp" android:textStyle="normal" android:maxLines="4" /> diff --git a/app/ui/legacy/src/main/res/values/strings.xml b/app/ui/legacy/src/main/res/values/strings.xml index 9a4cb3ac71fe4938d64c112a3c6fb817461b8b51..bc4ba7979cf962c6c426dcd0fb9fa738ef2d5273 100644 --- a/app/ui/legacy/src/main/res/values/strings.xml +++ b/app/ui/legacy/src/main/res/values/strings.xml @@ -297,8 +297,8 @@ Disable ringing, buzzing and flashing at night Disable notifications Completely disable notifications during Quiet Time - Quiet Time starts - Quiet Time ends + Quiet Time starts at + Quiet Time ends at Email sent sound Play a sound when an email is successfully sent Set up a new account @@ -1138,4 +1138,6 @@ You can keep this message and use it as a backup for your secret key. If you wan Email address Name and email address + + Disable automatic sync at specific time when activated diff --git a/app/ui/legacy/src/main/res/xml/general_settings.xml b/app/ui/legacy/src/main/res/xml/general_settings.xml index 10e4c28fb2ad8fd57a98f8e2983e5aa78511e18b..0d0553eaef7f47fea3a9bf324bc76c62501901b4 100644 --- a/app/ui/legacy/src/main/res/xml/general_settings.xml +++ b/app/ui/legacy/src/main/res/xml/general_settings.xml @@ -484,6 +484,31 @@ app:useSimpleSummaryProvider="true" android:title="@string/background_ops_label" /> + + + + + +