diff --git a/build-logic/src/main/java/extensions/KotlinMultiPlatformExt.kt b/build-logic/src/main/java/extensions/KotlinMultiPlatformExt.kt index bd263ef0..ae29329c 100644 --- a/build-logic/src/main/java/extensions/KotlinMultiPlatformExt.kt +++ b/build-logic/src/main/java/extensions/KotlinMultiPlatformExt.kt @@ -12,6 +12,8 @@ fun KotlinMultiplatformExtension.iosTarget() { iosTarget.binaries.framework { baseName = Config.appName isStatic = true + + export("io.github.mirzemehdi:kmpnotifier:1.5.1") } } } diff --git a/build/kotlin/commonizedNativeDistributionLocation.txt b/build/kotlin/commonizedNativeDistributionLocation.txt index aac56bbe..9f81b5aa 100644 --- a/build/kotlin/commonizedNativeDistributionLocation.txt +++ b/build/kotlin/commonizedNativeDistributionLocation.txt @@ -1 +1 @@ -/Users/pedroalvarez/.konan/kotlin-native-prebuilt-macos-aarch64-2.1.10/klib/commonized/2.1.10 \ No newline at end of file +/Users/junior/.konan/kotlin-native-prebuilt-macos-aarch64-2.1.10/klib/commonized/2.1.10 \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 6f25013b..bb2dfced 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -27,6 +27,7 @@ kotlin { implementation(projects.coreNavigation) implementation(projects.coreNetworking) implementation(projects.coreLocalStorage) + implementation(projects.coreBackgroundWork) implementation(libs.navigation.compose) @@ -35,6 +36,7 @@ kotlin { implementation(compose.components.resources) implementation(libs.koin.core) + api(libs.kmpnotifier) } } } \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com.codandotv.streamplayerapp/presentation/CustomApplication.kt b/composeApp/src/androidMain/kotlin/com.codandotv.streamplayerapp/presentation/CustomApplication.kt index 8cbc1494..a3911710 100644 --- a/composeApp/src/androidMain/kotlin/com.codandotv.streamplayerapp/presentation/CustomApplication.kt +++ b/composeApp/src/androidMain/kotlin/com.codandotv.streamplayerapp/presentation/CustomApplication.kt @@ -1,7 +1,11 @@ package com.codandotv.streamplayerapp.presentation import android.app.Application +import com.codandotv.streamplayerapp.core.shared.ui.R +import com.codandotv.streamplayerapp.core_background_work.worker.WorkScheduler import com.codandotv.streamplayerapp.di.AppModule +import com.mmk.kmpnotifier.notification.NotifierManager +import com.mmk.kmpnotifier.notification.configuration.NotificationPlatformConfiguration import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin @@ -13,5 +17,16 @@ class CustomApplication : Application() { androidContext(this@CustomApplication.applicationContext) modules(AppModule.list) } + WorkScheduler.scheduleSync(this) + initializeNotification() + } + + fun initializeNotification() { + NotifierManager.initialize( + configuration = NotificationPlatformConfiguration.Android( + notificationIconResId = R.mipmap.ic_netflix, + showPushNotification = true, + ) + ) } } \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com.codandotv.streamplayerapp/presentation/MainActivity.kt b/composeApp/src/androidMain/kotlin/com.codandotv.streamplayerapp/presentation/MainActivity.kt index 83efb537..40c2663c 100644 --- a/composeApp/src/androidMain/kotlin/com.codandotv.streamplayerapp/presentation/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com.codandotv.streamplayerapp/presentation/MainActivity.kt @@ -6,13 +6,21 @@ import androidx.activity.compose.setContent import com.codandotv.streamplayerapp.StreamPlayerApp import com.google.firebase.Firebase import com.google.firebase.initialize +import com.mmk.kmpnotifier.permission.permissionUtil class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Firebase.initialize(this) + requestNotificationPermission() setContent { StreamPlayerApp() } } + + private fun requestNotificationPermission() { + val permissionUtil by permissionUtil() + permissionUtil.askNotificationPermission() + } } diff --git a/composeApp/src/commonMain/kotlin/com.codandotv.streamplayerapp/SyncBridge.kt b/composeApp/src/commonMain/kotlin/com.codandotv.streamplayerapp/SyncBridge.kt new file mode 100644 index 00000000..0d3a785c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com.codandotv.streamplayerapp/SyncBridge.kt @@ -0,0 +1,25 @@ +package com.codandotv.streamplayerapp + +import com.codandotv.streamplayerapp.core_background_work.SyncManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koin.core.context.startKoin +import org.koin.dsl.module +import org.koin.mp.KoinPlatform.getKoin + +object SyncBridge { + suspend fun syncData() { + getKoin().get().syncData() + } + + fun syncData(completionHandler: () -> Unit) { + CoroutineScope(Dispatchers.Default).launch { + try { + syncData() + } finally { + completionHandler() + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com.codandotv.streamplayerapp/di/AppModule.kt b/composeApp/src/commonMain/kotlin/com.codandotv.streamplayerapp/di/AppModule.kt index ee2abc42..8267f699 100644 --- a/composeApp/src/commonMain/kotlin/com.codandotv.streamplayerapp/di/AppModule.kt +++ b/composeApp/src/commonMain/kotlin/com.codandotv.streamplayerapp/di/AppModule.kt @@ -1,9 +1,10 @@ package com.codandotv.streamplayerapp.di -import PermissionsModule +import com.codandotv.streamplayerapp.core_background_work.di.SyncModule import com.codandotv.streamplayerapp.core_local_storage.di.LocalStorageModule import com.codandotv.streamplayerapp.core_networking.di.NetworkModule import com.codandotv.streamplayerapp.core_shared.qualifier.QualifierDispatcherIO +import com.codandotv.streamplayerapp.feature_list_streams.list.di.ListStreamModule import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import org.koin.dsl.module @@ -12,5 +13,5 @@ object AppModule { private val module = module { single(QualifierDispatcherIO) { Dispatchers.IO } } - val list = module + NetworkModule.module + LocalStorageModule.module + val list = module + NetworkModule.module + LocalStorageModule.module + SyncModule.module + ListStreamModule.module } \ No newline at end of file diff --git a/core-background-work/.gitignore b/core-background-work/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/core-background-work/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core-background-work/build.gradle.kts b/core-background-work/build.gradle.kts new file mode 100644 index 00000000..b55e44b0 --- /dev/null +++ b/core-background-work/build.gradle.kts @@ -0,0 +1,23 @@ +@file:Suppress("UnstableApiUsage") + +plugins { + id("com.streamplayer.kmp-library") + id("com.google.devtools.ksp") +} + +kotlin { + sourceSets { + androidMain.dependencies { + implementation(libs.work.runtime) + } + + commonMain.dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.koin.core) + implementation(projects.coreShared) + implementation(projects.featureListStreams) + api(libs.kmpnotifier) + } + } +} \ No newline at end of file diff --git a/core-background-work/consumer-rules.pro b/core-background-work/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/core-background-work/proguard-rules.pro b/core-background-work/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/core-background-work/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/core-background-work/src/androidMain/kotlin/com/codandotv/streamplayerapp/core_background_work/worker/SyncWorker.kt b/core-background-work/src/androidMain/kotlin/com/codandotv/streamplayerapp/core_background_work/worker/SyncWorker.kt new file mode 100644 index 00000000..59fe012b --- /dev/null +++ b/core-background-work/src/androidMain/kotlin/com/codandotv/streamplayerapp/core_background_work/worker/SyncWorker.kt @@ -0,0 +1,26 @@ +package com.codandotv.streamplayerapp.core_background_work.worker + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.codandotv.streamplayerapp.core_background_work.SyncManager +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class SyncWorker( + context: Context, + params: WorkerParameters +) : CoroutineWorker(context, params), KoinComponent { + + private val syncManager: SyncManager by inject() + + override suspend fun doWork(): Result { + return try { + syncManager.syncData() + Result.success() + } catch (e: Exception) { + println("Erro no SyncWorker ${e.message}") + Result.retry() + } + } +} diff --git a/core-background-work/src/androidMain/kotlin/com/codandotv/streamplayerapp/core_background_work/worker/WorkScheduler.kt b/core-background-work/src/androidMain/kotlin/com/codandotv/streamplayerapp/core_background_work/worker/WorkScheduler.kt new file mode 100644 index 00000000..22bb8e72 --- /dev/null +++ b/core-background-work/src/androidMain/kotlin/com/codandotv/streamplayerapp/core_background_work/worker/WorkScheduler.kt @@ -0,0 +1,21 @@ +package com.codandotv.streamplayerapp.core_background_work.worker + +import android.content.Context +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import java.util.concurrent.TimeUnit + +object WorkScheduler { + fun scheduleSync(context: Context) { + val workRequest = PeriodicWorkRequestBuilder( + 15, TimeUnit.MINUTES + ).build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + "SyncWorker", + ExistingPeriodicWorkPolicy.KEEP, + workRequest + ) + } +} diff --git a/core-background-work/src/commonMain/kotlin/com/codandotv/streamplayerapp/core_background_work/NotifierHelper.kt b/core-background-work/src/commonMain/kotlin/com/codandotv/streamplayerapp/core_background_work/NotifierHelper.kt new file mode 100644 index 00000000..08ca362d --- /dev/null +++ b/core-background-work/src/commonMain/kotlin/com/codandotv/streamplayerapp/core_background_work/NotifierHelper.kt @@ -0,0 +1,26 @@ +package com.codandotv.streamplayerapp.core_background_work + +import com.mmk.kmpnotifier.notification.NotificationImage +import com.mmk.kmpnotifier.notification.Notifier +import com.mmk.kmpnotifier.notification.NotifierManager +import kotlin.random.Random + +object NotifierHelper { + fun showSimpleNotification( + title: String = "Notificação Simples", + body: String = "Corpo da Notificação Simples", + imageUrl: String = "https://github.com/user-attachments/assets/a0f38159-b31d-4a47-97a7-cc230e15d30b" + ) { + val notifier = NotifierManager.getLocalNotifier() + notifier.notify { + id = Random.Default.nextInt(0, Int.MAX_VALUE) + this.title = title + this.body = body + payloadData = mapOf( + Notifier.Companion.KEY_URL to imageUrl, + "extraKey" to "randomValue" + ) + image = NotificationImage.Url(imageUrl) + } + } +} \ No newline at end of file diff --git a/core-background-work/src/commonMain/kotlin/com/codandotv/streamplayerapp/core_background_work/SyncManager.kt b/core-background-work/src/commonMain/kotlin/com/codandotv/streamplayerapp/core_background_work/SyncManager.kt new file mode 100644 index 00000000..a43aa783 --- /dev/null +++ b/core-background-work/src/commonMain/kotlin/com/codandotv/streamplayerapp/core_background_work/SyncManager.kt @@ -0,0 +1,28 @@ +package com.codandotv.streamplayerapp.core_background_work + +import com.codandotv.streamplayerapp.feature_list_streams.list.data.ListStreamRepository +import com.codandotv.streamplayerapp.feature_list_streams.list.domain.model.Stream +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first + +class SyncManager( + private val repository: ListStreamRepository +) { + suspend fun syncData() { + + val title: Stream = repository.topRatedStream().first() + val messageTitle = "${title.name} -Já disponível no app!" + val messageBody = "Confira a sinopse: ${title.description}" + val imageUrl = title.posterPathUrl + + NotifierHelper.showSimpleNotification( + title = messageTitle, + body = messageBody, + imageUrl = imageUrl + ) + + println("SyncManager: Fazendo alguma tarefa de sincronização...") + delay(2000) + println("SyncManager: Sincronização concluída!") + } +} \ No newline at end of file diff --git a/core-background-work/src/commonMain/kotlin/com/codandotv/streamplayerapp/core_background_work/di/SyncModule.kt b/core-background-work/src/commonMain/kotlin/com/codandotv/streamplayerapp/core_background_work/di/SyncModule.kt new file mode 100644 index 00000000..867efc05 --- /dev/null +++ b/core-background-work/src/commonMain/kotlin/com/codandotv/streamplayerapp/core_background_work/di/SyncModule.kt @@ -0,0 +1,10 @@ +package com.codandotv.streamplayerapp.core_background_work.di + +import com.codandotv.streamplayerapp.core_background_work.SyncManager +import org.koin.dsl.module + +object SyncModule { + val module = module { + single { SyncManager(get()) } + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed404ca3..6e0d9001 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,6 +38,9 @@ accompanist = "0.31.3-beta" activity-compose = "1.8.2" popcorngp = "3.1.2" +work-runtime = "2.10.1" +kmpnotifier = "1.5.1" + #Firebase google-services = "4.4.2" firebase-bom = "33.13.0" @@ -121,6 +124,10 @@ coroutines_test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines- popcorn_guineapig = { group = "io.github.codandotv", name = "popcornguineapig", version.ref = "popcorngp" } +work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "work-runtime" } +kmpnotifier = { module = "io.github.mirzemehdi:kmpnotifier", version.ref = "kmpnotifier" } + + [bundles] test = ["junit", "mockk", "mockk_android", "viewmodel_test", "koin_test", "coroutines_test"] test_multiplatform = ["kotlin_test", "kotlin_test_common", "coroutines_test",] diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index aa02a0c6..1dde3c3b 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 5BAD97872D7CCDDA00D93987 /* Lottie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAD97862D7CCDDA00D93987 /* Lottie.swift */; }; 5BAD97892D7CCEA700D93987 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 5BAD97882D7CCEA700D93987 /* Lottie */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; + 7908F1992DE366F100D2455C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7908F1982DE366EC00D2455C /* AppDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -64,6 +65,7 @@ 7555FF7B242A565900829871 /* KotlinProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KotlinProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7908F1982DE366EC00D2455C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ @@ -156,6 +158,7 @@ 7555FF7D242A565900829871 /* iosApp */ = { isa = PBXGroup; children = ( + 7908F1982DE366EC00D2455C /* AppDelegate.swift */, 3FD330BB2DD2CFC300633ECC /* GoogleService-Info.plist */, 5BAD97862D7CCDDA00D93987 /* Lottie.swift */, 5BA9B50C2DA887EE00EF12ED /* iosApp.xctestplan */, @@ -340,6 +343,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7908F1992DE366F100D2455C /* AppDelegate.swift in Sources */, 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, 7555FF83242A565900829871 /* ContentView.swift in Sources */, 5BAD97872D7CCDDA00D93987 /* Lottie.swift in Sources */, @@ -537,7 +541,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; + DEVELOPMENT_TEAM = 4U9U2835SF; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -565,7 +569,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = "${TEAM_ID}"; + DEVELOPMENT_TEAM = 4U9U2835SF; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme b/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme index 4019680b..e76a2fbd 100644 --- a/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme +++ b/iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme @@ -43,7 +43,7 @@ Bool { + + NotifierManager.shared.initialize(configuration: NotificationPlatformConfigurationIos( + showPushNotification: true, + askNotificationPermissionOnStart: true, + notificationSoundName: nil + ) + ) + + + BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.codandotv.streamplayerapp", using: nil) { task in + self.handleSyncWork(task: task as! BGProcessingTask) + } + print("Tarefa registrada!") + + scheduleSyncWork() + return true + } + + func scheduleSyncWork() { + let request = BGProcessingTaskRequest(identifier: "com.codandotv.streamplayerapp") + request.requiresNetworkConnectivity = false + request.requiresExternalPower = false + request.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 minutos + + do { + try BGTaskScheduler.shared.submit(request) + print("Tarefa agendada com sucesso para pelo menos: \(request.earliestBeginDate!)") + } catch { + print("Falha ao agendar tarefa: \(error.localizedDescription)") + } + + + } + + func handleSyncWork(task: BGProcessingTask) { + scheduleSyncWork() + + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + + let operation = BlockOperation { + print("Tarefa executando operação de sync...") + SyncBridge.shared.syncData { + print("Tarefa do rodando no iOS") + task.setTaskCompleted(success: true) + } + } + + task.expirationHandler = { + print("Tarefa expirada.") + queue.cancelAllOperations() + } + + queue.addOperation(operation) + } +} diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index b1ada5c0..6a1ff3fa 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -27,6 +27,17 @@ UIApplicationSupportsMultipleScenes + UIBackgroundModes + + remote-notification + processing + fetch + + + BGTaskSchedulerPermittedIdentifiers + + com.codandotv.streamplayerapp + UILaunchScreen UIRequiredDeviceCapabilities diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index c4472c1e..59bf5cb8 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -4,12 +4,13 @@ import FirebaseCore @main struct iOSApp: App { - + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + init() { KoinIosHelper().doInitKoin(lottieViewProvider: LottieViewProviderImpl()) FirebaseApp.configure() } - + var body: some Scene { WindowGroup { ContentView() diff --git a/settings.gradle.kts b/settings.gradle.kts index b965577d..68208411 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,6 +38,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":feature-news") include(":core-camera-gallery") include(":core-permission") +include(":core-background-work") kover { enableCoverage()