From b60d3356ff6ca63cd8340e00a88c0f8ad3209a6b Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 2 Sep 2026 16:15:46 +0200 Subject: [PATCH] fix(login): only expedite WorkManager requests on Android 12+ Expedited WorkRequests on API < 31 fall back to an implicit foreground service, which requires the Worker to override getForegroundInfo(). None of our Workers do, so any expedited request crashed with IllegalStateException on older Android versions. For CapabilitiesWorker during login, that crash meant its completion event was never posted, leaving the login spinner stuck forever. Add setExpeditedIfSupported(), which only calls setExpedited() on API 31+ (where expedited work runs via JobScheduler and doesn't need getForegroundInfo()), and use it at all 21 call sites introduced by 39331e7ef. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe --- .../account/AccountVerificationActivity.kt | 6 ++--- .../account/data/io/LocalLoginDataSource.kt | 4 +-- .../com/nextcloud/talk/chat/ChatActivity.kt | 8 +++--- .../ConversationInfoActivity.kt | 8 +++--- .../ConversationsListActivity.kt | 8 +++--- .../talk/jobs/ChatMessageCatchUpWorker.kt | 4 +-- .../talk/jobs/PushRegistrationWorker.kt | 4 +-- .../talk/jobs/ShareOperationWorker.kt | 4 +-- .../viewmodels/MediaViewerViewModel.kt | 4 +-- .../talk/services/UnifiedPushService.kt | 4 +-- .../talk/settings/SettingsActivity.kt | 6 ++--- .../ui/dialog/SaveToStorageDialogFragment.kt | 4 +-- .../nextcloud/talk/utils/FileViewerUtils.kt | 3 +-- .../talk/utils/WorkRequestExtensions.kt | 25 +++++++++++++++++++ 14 files changed, 58 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/utils/WorkRequestExtensions.kt diff --git a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt index 139d505139..a26b37fa4b 100644 --- a/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/account/AccountVerificationActivity.kt @@ -18,7 +18,7 @@ import android.util.Log import android.widget.Toast import androidx.work.Data import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector @@ -495,7 +495,7 @@ class AccountVerificationActivity : BaseActivity() { val capabilitiesWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java) .setInputData(userData) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(capabilitiesWork) } @@ -591,7 +591,7 @@ class AccountVerificationActivity : BaseActivity() { private fun deleteUserAndStartServerSelection(userId: Long) { userManager.scheduleUserForDeletionWithId(userId).blockingGet() val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) diff --git a/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt index 2c3e53f6fa..64f36e90c0 100644 --- a/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt +++ b/app/src/main/java/com/nextcloud/talk/account/data/io/LocalLoginDataSource.kt @@ -10,7 +10,7 @@ package com.nextcloud.talk.account.data.io import android.content.Context import androidx.lifecycle.LiveData import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkInfo import androidx.work.WorkManager import com.nextcloud.talk.account.data.model.LoginCompletion @@ -33,7 +33,7 @@ class LocalLoginDataSource(val userManager: UserManager, val appPreferences: App fun startAccountRemovalWorker(): LiveData { val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(context).enqueue(accountRemovalWork) diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 19578d31f2..7e17316b0d 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -92,7 +92,7 @@ import androidx.media3.session.SessionToken import androidx.recyclerview.widget.LinearLayoutManager import androidx.work.Data import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector @@ -2380,7 +2380,7 @@ class ChatActivity : val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java) .setInputData(data) .addTag(fileId) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(downloadWorker) @@ -2538,7 +2538,7 @@ class ChatActivity : .build() val worker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java) .setInputData(data) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(worker) } @@ -3344,7 +3344,7 @@ class ChatActivity : val deleteConversationWorker = OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java) .setInputData(data.build()) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(deleteConversationWorker) diff --git a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt index 6bd495101f..bddda18c82 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationinfo/ConversationInfoActivity.kt @@ -27,7 +27,7 @@ import androidx.lifecycle.lifecycleScope import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector @@ -504,7 +504,7 @@ class ConversationInfoActivity : BaseActivity() { val addParticipantsWorker = OneTimeWorkRequest.Builder(AddParticipantsToConversationWorker::class.java) .setInputData(data) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(addParticipantsWorker) WorkManager.getInstance(context).getWorkInfoByIdLiveData(addParticipantsWorker.id) @@ -519,7 +519,7 @@ class ConversationInfoActivity : BaseActivity() { workerData?.let { data -> val workRequest = OneTimeWorkRequest.Builder(LeaveConversationWorker::class.java) .setInputData(data) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(context).enqueueUniqueWork( "leave_conversation_work", @@ -594,7 +594,7 @@ class ConversationInfoActivity : BaseActivity() { WorkManager.getInstance(context).enqueue( OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java) .setInputData(it) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() ) diff --git a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt index 21e143ee0a..1352ee3ad2 100644 --- a/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/conversationlist/ConversationsListActivity.kt @@ -32,7 +32,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.work.Data import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector @@ -1231,7 +1231,7 @@ class ConversationsListActivity : BaseActivity() { .build() val worker = OneTimeWorkRequest.Builder(LeaveConversationWorker::class.java) .setInputData(data) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(worker) WorkManager.getInstance(this).getWorkInfoByIdLiveData(worker.id).observeForever { workInfo -> @@ -1336,7 +1336,7 @@ class ConversationsListActivity : BaseActivity() { private fun deleteUserAndRestartApp() { userManager.scheduleUserForDeletionWithId(currentUser!!.id!!).blockingGet() val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) @@ -1476,7 +1476,7 @@ class ConversationsListActivity : BaseActivity() { val deleteConversationWorker = OneTimeWorkRequest.Builder(DeleteConversationWorker::class.java) .setInputData(data.build()) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(deleteConversationWorker) diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ChatMessageCatchUpWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ChatMessageCatchUpWorker.kt index 1e6cd91b03..091753a9dd 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/ChatMessageCatchUpWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/ChatMessageCatchUpWorker.kt @@ -15,7 +15,7 @@ import androidx.work.CoroutineWorker import androidx.work.Data import androidx.work.NetworkType import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkManager import androidx.work.WorkRequest import androidx.work.WorkerParameters @@ -148,7 +148,7 @@ class ChatMessageCatchUpWorker(context: Context, workerParams: WorkerParameters) .setInputData(data) .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, WorkRequest.MIN_BACKOFF_MILLIS, TimeUnit.MILLISECONDS) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(context).enqueue(catchUpWork) diff --git a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt index 1a5ca7a594..177e6ffb74 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/PushRegistrationWorker.kt @@ -11,7 +11,7 @@ import android.content.Context import android.util.Log import androidx.work.Data import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters @@ -287,7 +287,7 @@ class PushRegistrationWorker(context: Context, workerParams: WorkerParameters) : .build() val notificationWork = OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(applicationContext).enqueue(notificationWork) } diff --git a/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt b/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt index 0c18028f67..ed9e5596c7 100644 --- a/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt +++ b/app/src/main/java/com/nextcloud/talk/jobs/ShareOperationWorker.kt @@ -11,7 +11,7 @@ import android.content.Context import android.util.Log import androidx.work.Data import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkManager import androidx.work.Worker import androidx.work.WorkerParameters @@ -120,7 +120,7 @@ class ShareOperationWorker(context: Context, workerParams: WorkerParameters) : W .build() val shareWorker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java) .setInputData(data) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(shareWorker) } diff --git a/app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt b/app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt index 99cea88858..febbf307d7 100644 --- a/app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/mediaviewer/viewmodels/MediaViewerViewModel.kt @@ -12,7 +12,7 @@ import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import androidx.work.Data import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkInfo import androidx.work.WorkManager import com.nextcloud.talk.application.NextcloudTalkApplication @@ -152,7 +152,7 @@ class MediaViewerViewModel @Inject constructor(private val sharedItemsRepository val request = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java) .setInputData(data) .addTag(item.fileId) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(context).enqueue(request) diff --git a/app/src/main/java/com/nextcloud/talk/services/UnifiedPushService.kt b/app/src/main/java/com/nextcloud/talk/services/UnifiedPushService.kt index a5073533af..fc950ae0f9 100644 --- a/app/src/main/java/com/nextcloud/talk/services/UnifiedPushService.kt +++ b/app/src/main/java/com/nextcloud/talk/services/UnifiedPushService.kt @@ -10,7 +10,7 @@ package com.nextcloud.talk.services import android.util.Log import androidx.work.Data import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkManager import com.nextcloud.talk.jobs.NotificationWorker import com.nextcloud.talk.jobs.PushRegistrationWorker @@ -56,7 +56,7 @@ class UnifiedPushService : PushService() { .build() val notificationWork = OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(this).enqueue(notificationWork) } diff --git a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt index 66a2ff0a8f..c07c167f70 100644 --- a/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/settings/SettingsActivity.kt @@ -43,7 +43,7 @@ import androidx.core.net.toUri import androidx.core.view.ViewCompat import androidx.lifecycle.lifecycleScope import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector @@ -311,7 +311,7 @@ class SettingsActivity : private fun loadCapabilitiesAndUpdateSettings(isOnline: Boolean) { val capabilitiesWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(context).enqueue(capabilitiesWork) @@ -988,7 +988,7 @@ class SettingsActivity : private fun removeCurrentAccount() { userManager.scheduleUserForDeletionWithId(currentUser!!.id!!).blockingGet() val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance(applicationContext).enqueue(accountRemovalWork) diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt index 83ad06643c..3c43eb5196 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/SaveToStorageDialogFragment.kt @@ -16,7 +16,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment import androidx.work.Data import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy +import com.nextcloud.talk.utils.setExpeditedIfSupported import androidx.work.WorkInfo import androidx.work.WorkManager import autodagger.AutoInjector @@ -99,7 +99,7 @@ class SaveToStorageDialogFragment : DialogFragment() { val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java) .setInputData(data) .addTag(workerTag) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(saveWorker) diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt index 9aa63187c2..8775d02369 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/FileViewerUtils.kt @@ -22,7 +22,6 @@ import androidx.emoji2.widget.EmojiTextView import androidx.lifecycle.Observer import androidx.work.Data import androidx.work.OneTimeWorkRequest -import androidx.work.OutOfQuotaPolicy import androidx.work.WorkInfo import androidx.work.WorkManager import com.google.android.material.snackbar.Snackbar @@ -358,7 +357,7 @@ class FileViewerUtils(private val context: Context, private val user: User) { downloadWorker = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java) .setInputData(data) .addTag(fileInfo.fileId) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setExpeditedIfSupported() .build() WorkManager.getInstance().enqueue(downloadWorker) val liveData = WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id) diff --git a/app/src/main/java/com/nextcloud/talk/utils/WorkRequestExtensions.kt b/app/src/main/java/com/nextcloud/talk/utils/WorkRequestExtensions.kt new file mode 100644 index 0000000000..fa1d39492a --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/WorkRequestExtensions.kt @@ -0,0 +1,25 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2026 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.utils + +import android.os.Build +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkRequest + +/** + * Marks the request as expedited, but only on API 31+ (Android 12), where expedited work is + * backed by JobScheduler's expedited job support and does not require the Worker to implement + * getForegroundInfo(). On older APIs, expedited work falls back to an implicit foreground + * service and WorkManager throws IllegalStateException if getForegroundInfo() isn't overridden, + * which none of our Workers do. + */ +fun , W : WorkRequest> B.setExpeditedIfSupported(): B = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + } else { + this + }