From 108fbfe08dead32dd6c1bc53fa82982246e70418 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Wed, 10 Dec 2025 16:38:41 +0100 Subject: [PATCH] fix(sync): set up wire after logging in even with the app in the background --- app/build.gradle.kts | 4 + .../com/wire/android/di/CoreLogicModule.kt | 4 - .../notification/CallNotificationManager.kt | 9 +- .../MessageNotificationManager.kt | 4 +- .../services/PlayingAudioMessageService.kt | 2 +- .../wire/android/ui/WireActivityViewModel.kt | 9 +- ...reForegroundNotificationDetailsProvider.kt | 2 +- .../android/workmanager/WireWorkerFactory.kt | 4 + .../worker/AssetUploadObserverWorker.kt | 2 +- .../worker/DeleteConversationLocallyWorker.kt | 2 +- .../worker/NotificationFetchWorker.kt | 2 +- .../worker/PersistentWebsocketCheckWorker.kt | 2 +- .../android/ui/WireActivityViewModelTest.kt | 8 +- .../kotlin/scripts/infrastructure.gradle.kts | 5 - core/di/.gitignore | 1 + core/di/build.gradle.kts | 11 ++ core/di/consumer-rules.pro | 0 core/di/lint-baseline.xml | 4 + core/di/proguard-rules.pro | 0 .../com/wire/android/di/KaliumCoreLogic.kt | 24 ++++ core/media/.gitignore | 1 + core/media/build.gradle.kts | 11 ++ core/media/consumer-rules.pro | 0 core/media/lint-baseline.xml | 4 + core/media/proguard-rules.pro | 0 .../com/wire/android/media/PingRinger.kt | 18 +-- core/notification/.gitignore | 1 + core/notification/build.gradle.kts | 14 +++ core/notification/consumer-rules.pro | 0 core/notification/lint-baseline.xml | 4 + core/notification/proguard-rules.pro | 0 .../NotificationChannelsManager.kt | 5 +- .../notification/NotificationConstants.kt | 0 .../res/drawable/notification_icon_small.xml | 0 features/sync/.gitignore | 1 + features/sync/build.gradle.kts | 63 ++++++++++ features/sync/consumer-rules.pro | 0 features/sync/lint-baseline.xml | 4 + features/sync/proguard-rules.pro | 21 ++++ features/sync/src/main/AndroidManifest.xml | 2 + .../wire/android/sync/InitialSyncWorker.kt | 114 ++++++++++++++++++ .../android/sync/MonitorSyncWorkUseCase.kt | 78 ++++++++++++ features/sync/src/main/res/values/strings.xml | 21 ++++ settings.gradle.kts | 2 + 44 files changed, 426 insertions(+), 37 deletions(-) create mode 100644 core/di/.gitignore create mode 100644 core/di/build.gradle.kts create mode 100644 core/di/consumer-rules.pro create mode 100644 core/di/lint-baseline.xml create mode 100644 core/di/proguard-rules.pro create mode 100644 core/di/src/main/kotlin/com/wire/android/di/KaliumCoreLogic.kt create mode 100644 core/media/.gitignore create mode 100644 core/media/build.gradle.kts create mode 100644 core/media/consumer-rules.pro create mode 100644 core/media/lint-baseline.xml create mode 100644 core/media/proguard-rules.pro rename {app => core/media}/src/main/kotlin/com/wire/android/media/PingRinger.kt (86%) create mode 100644 core/notification/.gitignore create mode 100644 core/notification/build.gradle.kts create mode 100644 core/notification/consumer-rules.pro create mode 100644 core/notification/lint-baseline.xml create mode 100644 core/notification/proguard-rules.pro rename {app => core/notification}/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt (97%) rename {app => core/notification}/src/main/kotlin/com/wire/android/notification/NotificationConstants.kt (100%) rename {app => core/notification}/src/main/res/drawable/notification_icon_small.xml (100%) create mode 100644 features/sync/.gitignore create mode 100644 features/sync/build.gradle.kts create mode 100644 features/sync/consumer-rules.pro create mode 100644 features/sync/lint-baseline.xml create mode 100644 features/sync/proguard-rules.pro create mode 100644 features/sync/src/main/AndroidManifest.xml create mode 100644 features/sync/src/main/kotlin/com/wire/android/sync/InitialSyncWorker.kt create mode 100644 features/sync/src/main/kotlin/com/wire/android/sync/MonitorSyncWorkUseCase.kt create mode 100644 features/sync/src/main/res/values/strings.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 095225e2b9c..1b618256214 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -155,7 +155,11 @@ dependencies { // features implementation(project(":features:cells")) implementation(project(":features:sketch")) + implementation(projects.features.sync) implementation(project(":features:meetings")) + implementation(projects.core.di) + implementation(projects.core.media) + implementation(projects.core.notification) implementation(project(":core:ui-common")) // kover diff --git a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt index de5fab84c52..ce7be9c7c9e 100644 --- a/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/CoreLogicModule.kt @@ -70,10 +70,6 @@ import kotlinx.coroutines.runBlocking import javax.inject.Qualifier import javax.inject.Singleton -@Qualifier -@Retention(AnnotationRetention.BINARY) -annotation class KaliumCoreLogic - @Qualifier @Retention(AnnotationRetention.BINARY) annotation class CurrentSessionFlowService diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index d8590056318..9072b009876 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -29,6 +29,7 @@ import androidx.core.app.NotificationCompat.CallStyle import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person import com.wire.android.R +import com.wire.android.feature.notification.R as NR import com.wire.android.appLogger import com.wire.android.di.KaliumCoreLogic import com.wire.android.di.NoSession @@ -218,7 +219,7 @@ class CallNotificationBuilder @Inject constructor( return notificationBuilder .setPriority(NotificationCompat.PRIORITY_LOW) .setCategory(NotificationCompat.CATEGORY_CALL) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(NR.drawable.notification_icon_small) .setContentTitle(data.conversationName) .setContentText(context.getString(R.string.notification_outgoing_call_tap_to_return)) .setSubText(data.userName) @@ -248,7 +249,7 @@ class CallNotificationBuilder @Inject constructor( val notification = NotificationCompat.Builder(context, channelId) .setPriority(NotificationCompat.PRIORITY_MAX) .setCategory(NotificationCompat.CATEGORY_CALL) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(NR.drawable.notification_icon_small) .setContentTitle(title) .setContentText(content) .setSubText(data.userName) @@ -296,7 +297,7 @@ class CallNotificationBuilder @Inject constructor( .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setCategory(NotificationCompat.CATEGORY_CALL) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(NR.drawable.notification_icon_small) .setAutoCancel(true) .setOngoing(true) .setUsesChronometer(true) @@ -324,7 +325,7 @@ class CallNotificationBuilder @Inject constructor( .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setCategory(NotificationCompat.CATEGORY_CALL) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(NR.drawable.notification_icon_small) .setAutoCancel(true) .setOngoing(true) .setContentIntent(openAppPendingIntent(context)) diff --git a/app/src/main/kotlin/com/wire/android/notification/MessageNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/MessageNotificationManager.kt index 181c241941c..87ae1d80180 100644 --- a/app/src/main/kotlin/com/wire/android/notification/MessageNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/MessageNotificationManager.kt @@ -171,7 +171,7 @@ class MessageNotificationManager private fun getSummaryNotification(userId: QualifiedID, userName: String): Notification { val channelId = NotificationConstants.getMessagesChannelId(userId) return NotificationCompat.Builder(context, channelId) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(com.wire.android.feature.notification.R.drawable.notification_icon_small) .setGroup(NotificationConstants.getMessagesGroupKey(userId)) .setStyle(NotificationCompat.InboxStyle().setSummaryText(userName)) .setGroupSummary(true) @@ -530,7 +530,7 @@ class MessageNotificationManager priority = NotificationCompat.PRIORITY_MAX setCategory(NotificationCompat.CATEGORY_MESSAGE) - setSmallIcon(R.drawable.notification_icon_small) + setSmallIcon(com.wire.android.feature.notification.R.drawable.notification_icon_small) setGroup(NotificationConstants.getMessagesGroupKey(userId)) setAutoCancel(true) } diff --git a/app/src/main/kotlin/com/wire/android/services/PlayingAudioMessageService.kt b/app/src/main/kotlin/com/wire/android/services/PlayingAudioMessageService.kt index fe5a5c18ab3..83312d2269e 100644 --- a/app/src/main/kotlin/com/wire/android/services/PlayingAudioMessageService.kt +++ b/app/src/main/kotlin/com/wire/android/services/PlayingAudioMessageService.kt @@ -148,7 +148,7 @@ class PlayingAudioMessageService : Service() { return NotificationCompat.Builder(this, PLAYING_AUDIO_CHANNEL_ID) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(com.wire.android.feature.notification.R.drawable.notification_icon_small) .setStyle(NotificationCompat.DecoratedCustomViewStyle()) .setCustomContentView(notificationLayout) .setCategory(NotificationCompat.CATEGORY_SERVICE) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index a6a62a5f37a..a6486f4b7b7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -40,6 +40,7 @@ import com.wire.android.feature.SwitchAccountActions import com.wire.android.feature.SwitchAccountParam import com.wire.android.feature.SwitchAccountResult import com.wire.android.services.ServicesManager +import com.wire.android.sync.MonitorSyncWorkUseCase import com.wire.android.ui.authentication.devices.model.displayName import com.wire.android.ui.common.ActionsViewModel import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState @@ -130,6 +131,7 @@ class WireActivityViewModel @Inject constructor( private val workManager: Lazy, private val isProfileQRCodeEnabledFactory: IsProfileQRCodeEnabledUseCaseProvider.Factory, private val observeSelfUserFactory: ObserveSelfUserUseCaseProvider.Factory, + private val monitorSyncWorkUseCase: MonitorSyncWorkUseCase, ) : ActionsViewModel() { var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState()) @@ -163,6 +165,7 @@ class WireActivityViewModel @Inject constructor( observeSelectedAccent() observeLogoutState() resetNewRegistrationAnalyticsState() + viewModelScope.launch(dispatchers.io()) { monitorSyncWorkUseCase() } } private suspend fun shouldEnrollToE2ei(): Boolean = observeCurrentValidUserId.first()?.let { @@ -491,9 +494,9 @@ class WireActivityViewModel @Inject constructor( is CheckConversationInviteCodeUseCase.Result.Failure -> globalAppState = - globalAppState.copy( - conversationJoinedDialog = JoinConversationViaCodeState.Error(result) - ) + globalAppState.copy( + conversationJoinedDialog = JoinConversationViaCodeState.Error(result) + ) } } } diff --git a/app/src/main/kotlin/com/wire/android/workmanager/WireForegroundNotificationDetailsProvider.kt b/app/src/main/kotlin/com/wire/android/workmanager/WireForegroundNotificationDetailsProvider.kt index f099b090614..a96017f5c9f 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/WireForegroundNotificationDetailsProvider.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/WireForegroundNotificationDetailsProvider.kt @@ -18,7 +18,7 @@ package com.wire.android.workmanager -import com.wire.android.R +import com.wire.android.feature.notification.R import com.wire.kalium.logic.sync.ForegroundNotificationDetailsProvider object WireForegroundNotificationDetailsProvider : ForegroundNotificationDetailsProvider { diff --git a/app/src/main/kotlin/com/wire/android/workmanager/WireWorkerFactory.kt b/app/src/main/kotlin/com/wire/android/workmanager/WireWorkerFactory.kt index 999e345ce48..2694aab2483 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/WireWorkerFactory.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/WireWorkerFactory.kt @@ -26,6 +26,7 @@ import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.StartPersistentWebsocketIfNecessaryUseCase import com.wire.android.notification.NotificationChannelsManager import com.wire.android.notification.WireNotificationManager +import com.wire.android.sync.InitialSyncWorker import com.wire.android.workmanager.worker.DeleteConversationLocallyWorker import com.wire.android.workmanager.worker.NotificationFetchWorker import com.wire.android.workmanager.worker.PersistentWebsocketCheckWorker @@ -66,6 +67,9 @@ class WireWorkerFactory @Inject constructor( AssetUploadObserverWorker::class.java.canonicalName -> AssetUploadObserverWorker(appContext, workerParameters, coreLogic, notificationChannelsManager) + InitialSyncWorker::class.java.canonicalName -> + InitialSyncWorker(appContext, workerParameters, coreLogic, notificationChannelsManager) + else -> null } } diff --git a/app/src/main/kotlin/com/wire/android/workmanager/worker/AssetUploadObserverWorker.kt b/app/src/main/kotlin/com/wire/android/workmanager/worker/AssetUploadObserverWorker.kt index dd753bfd3fb..9ac5039da15 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/worker/AssetUploadObserverWorker.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/worker/AssetUploadObserverWorker.kt @@ -79,7 +79,7 @@ class AssetUploadObserverWorker @AssistedInject constructor( ) val notification = NotificationCompat.Builder(applicationContext, NotificationConstants.OTHER_CHANNEL_ID) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(com.wire.android.feature.notification.R.drawable.notification_icon_small) .setAutoCancel(true) .setSilent(true) .setCategory(NotificationCompat.CATEGORY_SERVICE) diff --git a/app/src/main/kotlin/com/wire/android/workmanager/worker/DeleteConversationLocallyWorker.kt b/app/src/main/kotlin/com/wire/android/workmanager/worker/DeleteConversationLocallyWorker.kt index b1a90563660..b203d976926 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/worker/DeleteConversationLocallyWorker.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/worker/DeleteConversationLocallyWorker.kt @@ -94,7 +94,7 @@ class DeleteConversationLocallyWorker @AssistedInject constructor( ) val notification = NotificationCompat.Builder(applicationContext, NotificationConstants.OTHER_CHANNEL_ID) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(com.wire.android.feature.notification.R.drawable.notification_icon_small) .setAutoCancel(true) .setSilent(true) .setCategory(NotificationCompat.CATEGORY_SERVICE) diff --git a/app/src/main/kotlin/com/wire/android/workmanager/worker/NotificationFetchWorker.kt b/app/src/main/kotlin/com/wire/android/workmanager/worker/NotificationFetchWorker.kt index 327a0e93bbe..7f4065fd65c 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/worker/NotificationFetchWorker.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/worker/NotificationFetchWorker.kt @@ -61,7 +61,7 @@ class NotificationFetchWorker ) val notification = NotificationCompat.Builder(applicationContext, NotificationConstants.MESSAGE_SYNC_CHANNEL_ID) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(com.wire.android.feature.notification.R.drawable.notification_icon_small) .setAutoCancel(true) .setSilent(true) .setCategory(NotificationCompat.CATEGORY_SERVICE) diff --git a/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt b/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt index 31178e9ddbb..c957eef1bfe 100644 --- a/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt +++ b/app/src/main/kotlin/com/wire/android/workmanager/worker/PersistentWebsocketCheckWorker.kt @@ -68,7 +68,7 @@ class PersistentWebsocketCheckWorker val title = "${applicationContext.getString(R.string.app_name)} " + applicationContext.getString(R.string.settings_service_is_running) val notification = NotificationCompat.Builder(applicationContext, NotificationConstants.OTHER_CHANNEL_ID) - .setSmallIcon(R.drawable.notification_icon_small) + .setSmallIcon(com.wire.android.feature.notification.R.drawable.notification_icon_small) .setAutoCancel(true) .setSilent(true) .setCategory(NotificationCompat.CATEGORY_SERVICE) diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 271c2f474cf..acd3d11e383 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -40,6 +40,7 @@ import com.wire.android.framework.TestClient import com.wire.android.framework.TestUser import com.wire.android.framework.TestUser.SELF_USER import com.wire.android.services.ServicesManager +import com.wire.android.sync.MonitorSyncWorkUseCase import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState import com.wire.android.ui.common.dialogs.CustomServerNoNetworkDialogState import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModelTest @@ -770,6 +771,7 @@ class WireActivityViewModelTest { // Default empty values mockUri() + coEvery { monitorSyncWorkUseCase() } returns Unit coEvery { currentSessionFlow() } returns flowOf() coEvery { getServerConfigUseCase(any()) } returns GetServerConfigResult.Success(newServerConfig(1).links) coEvery { deepLinkProcessor(any(), any()) } returns DeepLinkResult.Unknown @@ -860,6 +862,9 @@ class WireActivityViewModelTest { @MockK lateinit var observeSelfUserFactory: ObserveSelfUserUseCaseProvider.Factory + @MockK + lateinit var monitorSyncWorkUseCase: MonitorSyncWorkUseCase + private val viewModel by lazy { WireActivityViewModel( coreLogic = { coreLogic }, @@ -881,7 +886,8 @@ class WireActivityViewModelTest { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory = observeIfE2EIRequiredDuringLoginUseCaseProviderFactory, workManager = { workManager }, isProfileQRCodeEnabledFactory = isProfileQRCodeEnabledFactory, - observeSelfUserFactory = observeSelfUserFactory + observeSelfUserFactory = observeSelfUserFactory, + monitorSyncWorkUseCase = monitorSyncWorkUseCase, ) } diff --git a/buildSrc/src/main/kotlin/scripts/infrastructure.gradle.kts b/buildSrc/src/main/kotlin/scripts/infrastructure.gradle.kts index a904a1ff006..a6558636a67 100644 --- a/buildSrc/src/main/kotlin/scripts/infrastructure.gradle.kts +++ b/buildSrc/src/main/kotlin/scripts/infrastructure.gradle.kts @@ -26,11 +26,6 @@ tasks.register("clean", Delete::class) { delete(rootProject.layout.buildDirectory) } -tasks.named("wrapper") { - gradleVersion = findVersion("gradle").requiredVersion - distributionType = Wrapper.DistributionType.ALL -} - tasks.register("runUnitTests") { description = "Runs all Unit Tests." dependsOn(":app:test${Default.BUILD_VARIANT}UnitTest") diff --git a/core/di/.gitignore b/core/di/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/core/di/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/di/build.gradle.kts b/core/di/build.gradle.kts new file mode 100644 index 00000000000..3aafaace2d4 --- /dev/null +++ b/core/di/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id(libs.plugins.wire.android.library.get().pluginId) + id(libs.plugins.wire.kover.get().pluginId) + alias(libs.plugins.compose.compiler) +} + +dependencies { + implementation(libs.androidx.core) + implementation(libs.hilt.android) + implementation(libs.compose.material3) +} diff --git a/core/di/consumer-rules.pro b/core/di/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/di/lint-baseline.xml b/core/di/lint-baseline.xml new file mode 100644 index 00000000000..a84701647ba --- /dev/null +++ b/core/di/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/di/proguard-rules.pro b/core/di/proguard-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/di/src/main/kotlin/com/wire/android/di/KaliumCoreLogic.kt b/core/di/src/main/kotlin/com/wire/android/di/KaliumCoreLogic.kt new file mode 100644 index 00000000000..f9d230ef7a0 --- /dev/null +++ b/core/di/src/main/kotlin/com/wire/android/di/KaliumCoreLogic.kt @@ -0,0 +1,24 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * 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 http://www.gnu.org/licenses/. + */ +package com.wire.android.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class KaliumCoreLogic diff --git a/core/media/.gitignore b/core/media/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/core/media/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/media/build.gradle.kts b/core/media/build.gradle.kts new file mode 100644 index 00000000000..3aafaace2d4 --- /dev/null +++ b/core/media/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id(libs.plugins.wire.android.library.get().pluginId) + id(libs.plugins.wire.kover.get().pluginId) + alias(libs.plugins.compose.compiler) +} + +dependencies { + implementation(libs.androidx.core) + implementation(libs.hilt.android) + implementation(libs.compose.material3) +} diff --git a/core/media/consumer-rules.pro b/core/media/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/media/lint-baseline.xml b/core/media/lint-baseline.xml new file mode 100644 index 00000000000..a84701647ba --- /dev/null +++ b/core/media/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/media/proguard-rules.pro b/core/media/proguard-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/src/main/kotlin/com/wire/android/media/PingRinger.kt b/core/media/src/main/kotlin/com/wire/android/media/PingRinger.kt similarity index 86% rename from app/src/main/kotlin/com/wire/android/media/PingRinger.kt rename to core/media/src/main/kotlin/com/wire/android/media/PingRinger.kt index 7286193ceb7..1a210db3af3 100644 --- a/app/src/main/kotlin/com/wire/android/media/PingRinger.kt +++ b/core/media/src/main/kotlin/com/wire/android/media/PingRinger.kt @@ -1,6 +1,6 @@ /* * Wire - * Copyright (C) 2024 Wire Swiss GmbH + * Copyright (C) 2025 Wire Swiss GmbH * * 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 @@ -17,8 +17,10 @@ */ package com.wire.android.media +import android.Manifest +import android.annotation.SuppressLint import android.content.Context -import android.content.Context.VIBRATOR_SERVICE +import android.content.pm.PackageManager import android.media.AudioAttributes import android.media.AudioManager import android.media.MediaPlayer @@ -26,7 +28,6 @@ import android.os.Build import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager -import com.wire.android.appLogger import javax.inject.Inject import javax.inject.Singleton @@ -45,7 +46,7 @@ class PingRinger @Inject constructor(private val context: Context) { vibratorManager?.defaultVibrator } else { @Suppress("DEPRECATION") - context.getSystemService(VIBRATOR_SERVICE) as Vibrator? + context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? } } @@ -78,23 +79,24 @@ class PingRinger @Inject constructor(private val context: Context) { @Suppress("NestedBlockDepth") private fun vibrateIfNeeded(isReceivingPing: Boolean) { if (isReceivingPing) { + val hasVibratePermission = context.checkSelfPermission(Manifest.permission.VIBRATE) == PackageManager.PERMISSION_GRANTED + if (!hasVibratePermission) { + return + } vibrator?.let { if (!it.hasVibrator()) { - appLogger.i("Device does not support vibration") return } val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager? val ringerMode = audioManager?.ringerMode if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) { - appLogger.i("Starting vibration") + @SuppressLint("MissingPermission") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { it.vibrate(VibrationEffect.createWaveform(VIBRATE_PATTERN, DO_NOT_REPEAT)) } else { @Suppress("DEPRECATION") it.vibrate(VIBRATE_PATTERN, DO_NOT_REPEAT) } - } else { - appLogger.i("Skipping vibration") } } } diff --git a/core/notification/.gitignore b/core/notification/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/core/notification/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/notification/build.gradle.kts b/core/notification/build.gradle.kts new file mode 100644 index 00000000000..aff8001885d --- /dev/null +++ b/core/notification/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id(libs.plugins.wire.android.library.get().pluginId) + id(libs.plugins.wire.kover.get().pluginId) + alias(libs.plugins.compose.compiler) +} + +dependencies { + implementation(projects.core.media) + implementation("com.wire.kalium:kalium-common") + implementation("com.wire.kalium:kalium-data") + implementation(libs.androidx.core) + implementation(libs.hilt.android) + implementation(libs.compose.material3) +} diff --git a/core/notification/consumer-rules.pro b/core/notification/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/notification/lint-baseline.xml b/core/notification/lint-baseline.xml new file mode 100644 index 00000000000..a84701647ba --- /dev/null +++ b/core/notification/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/core/notification/proguard-rules.pro b/core/notification/proguard-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt b/core/notification/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt similarity index 97% rename from app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt rename to core/notification/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt index ce2e1476950..6aa0d551b7c 100644 --- a/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt +++ b/core/notification/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt @@ -1,6 +1,6 @@ /* * Wire - * Copyright (C) 2024 Wire Swiss GmbH + * Copyright (C) 2025 Wire Swiss GmbH * * 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 @@ -27,7 +27,6 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelGroupCompat import androidx.core.app.NotificationManagerCompat -import com.wire.android.appLogger import com.wire.android.media.PingRinger import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.UserId @@ -58,7 +57,6 @@ class NotificationChannelsManager @Inject constructor( * And removing the ChannelGroups (with all the channels in it) that are not belongs to any user in a list (user logged out e.x.) */ fun createUserNotificationChannels(allUsers: List) { - appLogger.i("$TAG: creating all the notification channels for ${allUsers.size} users") if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return allUsers.forEach { user -> @@ -88,7 +86,6 @@ class NotificationChannelsManager @Inject constructor( notificationManagerCompat.notificationChannelGroups .filter { group -> groupsToKeep.none { it == group.id } } .forEach { group -> - appLogger.i("$TAG: deleting notification channels for ${group.name} group") notificationManagerCompat.deleteNotificationChannelGroup(group.id) } } diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationConstants.kt b/core/notification/src/main/kotlin/com/wire/android/notification/NotificationConstants.kt similarity index 100% rename from app/src/main/kotlin/com/wire/android/notification/NotificationConstants.kt rename to core/notification/src/main/kotlin/com/wire/android/notification/NotificationConstants.kt diff --git a/app/src/main/res/drawable/notification_icon_small.xml b/core/notification/src/main/res/drawable/notification_icon_small.xml similarity index 100% rename from app/src/main/res/drawable/notification_icon_small.xml rename to core/notification/src/main/res/drawable/notification_icon_small.xml diff --git a/features/sync/.gitignore b/features/sync/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/features/sync/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/sync/build.gradle.kts b/features/sync/build.gradle.kts new file mode 100644 index 00000000000..0a20e0da993 --- /dev/null +++ b/features/sync/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + id(libs.plugins.wire.android.library.get().pluginId) + id(libs.plugins.wire.kover.get().pluginId) + id(libs.plugins.wire.hilt.get().pluginId) + id(BuildPlugins.kotlinParcelize) + id(BuildPlugins.junit5) + alias(libs.plugins.ksp) + id(libs.plugins.wire.android.navigation.get().pluginId) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) +} + +dependencies { + implementation("com.wire.kalium:kalium-common") + implementation("com.wire.kalium:kalium-logic") + implementation(projects.core.notification) + implementation(projects.core.uiCommon) + implementation(projects.core.di) + implementation(libs.androidx.core) + implementation(libs.androidx.appcompat) + implementation(libs.material) + + // hilt + implementation(libs.hilt.navigationCompose) + implementation(libs.hilt.work) + + // smaller view models + implementation(libs.resaca.core) + implementation(libs.resaca.hilt) + implementation(libs.bundlizer.core) + + val composeBom = platform(libs.compose.bom) + implementation(composeBom) + implementation(libs.compose.ui) + implementation(libs.compose.ui.graphics) + implementation(libs.compose.material.core) + implementation(libs.compose.material3) + implementation(libs.androidx.lifecycle.viewModelCompose) + implementation(libs.compose.ui.preview) + + implementation(libs.ktx.dateTime) + + implementation(libs.androidx.paging3) + implementation(libs.androidx.paging3Compose) + + testImplementation(libs.junit5.core) + testImplementation(libs.coroutines.test) + testImplementation(libs.mockk.core) + testImplementation(libs.turbine) + testImplementation(libs.androidx.paging.testing) + testRuntimeOnly(libs.junit5.engine) + androidTestImplementation(libs.androidx.test.extJunit) + androidTestImplementation(libs.androidx.espresso.core) +} + +android { + ksp { + // TODO: MOVE TO CONVENTION PLUGIN + // No reason to keep adding this manually to each module. We can use `project.name` + arg("compose-destinations.moduleName", "sync") + arg("compose-destinations.mode", "destinations") + } +} diff --git a/features/sync/consumer-rules.pro b/features/sync/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/features/sync/lint-baseline.xml b/features/sync/lint-baseline.xml new file mode 100644 index 00000000000..98cd24e06c5 --- /dev/null +++ b/features/sync/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/features/sync/proguard-rules.pro b/features/sync/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/features/sync/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/features/sync/src/main/AndroidManifest.xml b/features/sync/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..2b9803aa7a4 --- /dev/null +++ b/features/sync/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/features/sync/src/main/kotlin/com/wire/android/sync/InitialSyncWorker.kt b/features/sync/src/main/kotlin/com/wire/android/sync/InitialSyncWorker.kt new file mode 100644 index 00000000000..f4590eb67f9 --- /dev/null +++ b/features/sync/src/main/kotlin/com/wire/android/sync/InitialSyncWorker.kt @@ -0,0 +1,114 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * 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 http://www.gnu.org/licenses/. + */ +package com.wire.android.sync + +import android.content.Context +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import com.wire.android.di.KaliumCoreLogic +import com.wire.android.feature.sync.R +import com.wire.android.notification.NotificationChannelsManager +import com.wire.android.notification.NotificationConstants +import com.wire.android.notification.NotificationIds +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.session.GetAllSessionsResult +import com.wire.kalium.work.Work +import com.wire.kalium.work.WorkId +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.launch +import com.wire.android.feature.notification.R as NR + +@HiltWorker +class InitialSyncWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted parameters: WorkerParameters, + @KaliumCoreLogic private val coreLogic: CoreLogic, + private val notificationChannelsManager: NotificationChannelsManager, +) : CoroutineWorker(context, parameters) { + + private val workId: WorkId? = parameters.getWorkId() + + override suspend fun doWork(): Result = if (workId == null) { + Log.e("InitialSyncWorker", "WorkId is null, cannot start monitoring work.") + Result.failure() + } else { + Log.i("InitialSyncWorker", "Starting InitialSyncWorker.") + val result = coreLogic.globalScope { + session.allSessions() + } + if (result !is GetAllSessionsResult.Success) { + Log.e("InitialSyncWorker", "Failure to get active sessions. Not waiting for Sync.") + Result.failure() + } else { + coroutineScope { + result.sessions.forEach { session -> + launch { + coreLogic.sessionScope(session.userId) { + syncExecutor.request { + Log.i("InitialSyncWorker", "Waiting for Initial Sync for user '${session.userId}' to finish.") + longWork.observeWorkStatus(workId).takeWhile { + it !is Work.Status.Complete + }.collect() + Log.i("InitialSyncWorker", "Initial Sync for user '${session.userId}' complete.") + } + } + } + } + } + Log.i("InitialSyncWorker", "Initial Sync complete for all users") + Result.success() + } + } + + override suspend fun getForegroundInfo(): ForegroundInfo { + notificationChannelsManager.createRegularChannel( + NotificationConstants.OTHER_CHANNEL_ID, + NotificationConstants.OTHER_CHANNEL_NAME + ) + + val notification = NotificationCompat.Builder(applicationContext, NotificationConstants.OTHER_CHANNEL_ID) + .setSmallIcon(NR.drawable.notification_icon_small) + .setAutoCancel(true) + .setSilent(true) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setContentTitle(applicationContext.getString(R.string.notification_setting_up_wire)) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setProgress(0, 0, true) + .build() + + return ForegroundInfo(NotificationIds.UPLOADING_DATA_NOTIFICATION_ID.ordinal, notification) + } + + companion object { + private const val WORK_ID_KEY = "workId" + private fun WorkerParameters.getWorkId(): WorkId? = inputData.getString(WORK_ID_KEY)?.let { WorkId(it) } + + fun createInputData(workId: WorkId) = Data.Builder() + .putString(WORK_ID_KEY, workId.id) + .build() + } +} diff --git a/features/sync/src/main/kotlin/com/wire/android/sync/MonitorSyncWorkUseCase.kt b/features/sync/src/main/kotlin/com/wire/android/sync/MonitorSyncWorkUseCase.kt new file mode 100644 index 00000000000..1af66e0a8c3 --- /dev/null +++ b/features/sync/src/main/kotlin/com/wire/android/sync/MonitorSyncWorkUseCase.kt @@ -0,0 +1,78 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * 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 http://www.gnu.org/licenses/. + */ +package com.wire.android.sync + +import android.content.Context +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import com.wire.android.di.KaliumCoreLogic +import com.wire.kalium.common.logger.kaliumLogger +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.feature.session.GetAllSessionsResult +import com.wire.kalium.logic.feature.session.ObserveSessionsUseCase +import com.wire.kalium.work.Work +import com.wire.kalium.work.WorkId +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import javax.inject.Inject + +class MonitorSyncWorkUseCase @Inject constructor( + @param:ApplicationContext private val context: Context, + @param:KaliumCoreLogic private val coreLogic: CoreLogic, + private val allSessionsUseCase: ObserveSessionsUseCase +) { + + /** + * Monitors if any user session has an ongoing Sync work being done, by: + * 1. Gets all session scopes + * 2. Gets the longWork scope from each session + * 3. Get the observeNewWorks from each scope + * 4. Merge the flows + * 5. Launch a worker if any new work is of type InitialSync + */ + suspend operator fun invoke() { + allSessionsUseCase().filterIsInstance().map { result -> + result.sessions.map { session -> + coreLogic.sessionScope(session.userId) { longWork } + } + }.flatMapLatest { scopes -> + scopes.map { scope -> + scope.observeNewWorks() + }.merge() + }.collect { + if (it.type is Work.Type.InitialSync) { + kaliumLogger.withTextTag("MonitorSyncWorkUseCase").i("Launching worker!") + launchWorker(it.id) + } + } + } + + private fun launchWorker(workId: WorkId) { + val request = OneTimeWorkRequestBuilder() + .setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST) + .setInputData(InitialSyncWorker.createInputData(workId)) + .build() + + WorkManager.getInstance(context) + .enqueue(request) + } +} diff --git a/features/sync/src/main/res/values/strings.xml b/features/sync/src/main/res/values/strings.xml new file mode 100644 index 00000000000..16db44aa1df --- /dev/null +++ b/features/sync/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + Setting up Wire + diff --git a/settings.gradle.kts b/settings.gradle.kts index 1aa9c59e33a..4f73dacb576 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -65,3 +65,5 @@ dependencyResolutionManagement { // so Reloaded's Dependabot doesn't try to look into Kalium's build.gradle.kts, which is inaccessible as it is a git submodule. // See: https://github.com/dependabot/dependabot-core/issues/7201#issuecomment-1571319655 apply(from = "include_builds.gradle.kts") + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")