Skip to content

Commit 649fd9f

Browse files
committed
implemented Native Notifications
1 parent a7f095f commit 649fd9f

File tree

13 files changed

+249
-4
lines changed

13 files changed

+249
-4
lines changed

composeApp/src/androidMain/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33

4+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
45
<application
56
android:name=".MyTodoApp"
67
android:allowBackup="true"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package io.jadu
22

33
import android.app.Application
4+
import io.jadu.todoApp.ui.notification.PlatformActivityProvider
45

56
class MyTodoApp : Application() {
67
override fun onCreate() {
78
super.onCreate()
9+
registerActivityLifecycleCallbacks(PlatformActivityProvider)
810
initKoin(this)
911
}
1012
}

composeApp/src/androidMain/kotlin/io/jadu/di.android.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package io.jadu
33
import android.app.Application
44
import createDataStore
55
import getDatabaseBuilder
6+
import io.jadu.todoApp.ui.notification.LocalNotificationManager
67
import org.koin.android.ext.koin.androidContext
78
import org.koin.core.module.Module
89
import org.koin.dsl.module
910

1011
val androidModule = module {
1112
single { getDatabaseBuilder(androidContext()) }
1213
single { createDataStore(androidContext()) }
14+
single { LocalNotificationManager(androidContext()) }
1315
}
1416

1517
actual fun platformModule(): Module = androidModule
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package io.jadu.todoApp.ui.notification
2+
3+
import android.Manifest
4+
import android.app.NotificationChannel
5+
import android.app.NotificationManager
6+
import android.content.Context
7+
import android.content.pm.PackageManager
8+
import android.os.Build
9+
import androidx.core.app.ActivityCompat
10+
import androidx.core.app.NotificationCompat
11+
import androidx.core.app.NotificationManagerCompat
12+
import androidx.core.content.ContextCompat
13+
import kotlin.random.Random
14+
15+
actual class LocalNotificationManager (
16+
private val context: Context
17+
) {
18+
actual fun showNotification(title: String, body: String) {
19+
val channelId = "default_channel_id"
20+
val notificationId = Random.nextInt()
21+
22+
// 1. Create the NotificationChannel (Required for API 26+)
23+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
24+
val name = "General Notifications"
25+
val descriptionText = "Default channel for app notifications"
26+
val importance = NotificationManager.IMPORTANCE_DEFAULT
27+
val channel = NotificationChannel(channelId, name, importance).apply {
28+
description = descriptionText
29+
}
30+
// Register the channel with the system
31+
val notificationManager: NotificationManager =
32+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
33+
notificationManager.createNotificationChannel(channel)
34+
}
35+
36+
// 2. Build the notification
37+
val builder = NotificationCompat.Builder(context, channelId)
38+
.setSmallIcon(android.R.drawable.ic_dialog_info) // Replace with your app's icon
39+
.setContentTitle(title)
40+
.setContentText(body)
41+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
42+
.setAutoCancel(true)
43+
44+
// 3. Show the notification
45+
with(NotificationManagerCompat.from(context)) {
46+
// Check for permission strictly required on Android 13+
47+
try {
48+
notify(notificationId, builder.build())
49+
} catch (e: SecurityException) {
50+
// Handle missing permission
51+
println("Notification permission missing")
52+
}
53+
}
54+
}
55+
56+
actual fun hasPermission(): Boolean {
57+
return if (Build.VERSION.SDK_INT >= 33) { // Android 13+
58+
ContextCompat.checkSelfPermission(
59+
context,
60+
Manifest.permission.POST_NOTIFICATIONS
61+
) == PackageManager.PERMISSION_GRANTED
62+
} else {
63+
true // Android 12 and below have implicit permission
64+
}
65+
}
66+
67+
actual suspend fun requestPermission(): Boolean {
68+
if (hasPermission()) return true
69+
70+
// for Android 13
71+
if (Build.VERSION.SDK_INT >= 33) {
72+
val activity = PlatformActivityProvider.currentActivity
73+
if (activity!=null) {
74+
ActivityCompat.requestPermissions(
75+
activity,
76+
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
77+
123 // Request Code
78+
)
79+
return false
80+
} else {
81+
println("LocalNotificationService needs an Activity Context to request permissions")
82+
return false
83+
}
84+
}
85+
return true
86+
}
87+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.jadu.todoApp.ui.notification
2+
3+
import android.app.Activity
4+
import android.app.Application
5+
import android.os.Bundle
6+
import java.lang.ref.WeakReference
7+
8+
object PlatformActivityProvider : Application.ActivityLifecycleCallbacks {
9+
// WeakReference prevents memory leaks
10+
private var currentActivityRef: WeakReference<Activity>? = null
11+
12+
val currentActivity: Activity?
13+
get() = currentActivityRef?.get()
14+
15+
16+
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
17+
override fun onActivityStarted(activity: Activity) {
18+
currentActivityRef = WeakReference(activity)
19+
}
20+
override fun onActivityResumed(activity: Activity) {
21+
currentActivityRef = WeakReference(activity)
22+
}
23+
override fun onActivityPaused(activity: Activity) {}
24+
override fun onActivityStopped(activity: Activity) {}
25+
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
26+
override fun onActivityDestroyed(activity: Activity) {}
27+
}

composeApp/src/commonMain/kotlin/io/jadu/di.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.room.RoomDatabase
55
import io.jadu.todoApp.data.TodoRepositoryImpl
66
import io.jadu.todoApp.data.local.AppDatabase
77
import io.jadu.todoApp.domain.TodoRepository
8+
import io.jadu.todoApp.ui.notification.NotificationViewModel
89
import io.jadu.todoApp.ui.viewModel.AddProjectViewModel
910
import io.jadu.todoApp.ui.viewModel.EditTodoViewModel
1011
import io.jadu.todoApp.ui.viewModel.HomeScreenViewModel
@@ -43,6 +44,7 @@ val appModule = module {
4344
single { HomeScreenViewModel(get(), get()) }
4445
single { SettingsViewModel(get(), get()) }
4546
single { MostUsedCategoryViewModel(get()) }
47+
single { NotificationViewModel(get()) }
4648
}
4749

4850

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.jadu.todoApp.ui.notification
2+
3+
expect class LocalNotificationManager {
4+
fun hasPermission(): Boolean
5+
suspend fun requestPermission(): Boolean
6+
fun showNotification(title: String, body: String)
7+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.jadu.todoApp.ui.notification
2+
3+
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.viewModelScope
5+
import kotlinx.coroutines.launch
6+
7+
class NotificationViewModel(private val manager: LocalNotificationManager): ViewModel() {
8+
9+
fun onEnableNotificationsClicked() {
10+
viewModelScope.launch {
11+
if (!manager.hasPermission()) {
12+
val granted = manager.requestPermission()
13+
14+
if (granted) {
15+
manager.showNotification("Success", "Notifications enabled!")
16+
}
17+
} else {
18+
manager.showNotification("Success", "Notifications enabled!")
19+
}
20+
}
21+
}
22+
}

composeApp/src/commonMain/kotlin/io/jadu/todoApp/ui/screens/homescreen/components/HomePageContent.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
2121
import androidx.compose.ui.text.style.TextAlign
2222
import androidx.compose.ui.unit.dp
2323
import androidx.navigation.NavController
24+
import io.jadu.todoApp.ui.notification.NotificationViewModel
2425
import io.jadu.todoApp.ui.route.NavRoute
2526
import io.jadu.todoApp.ui.theme.BodyLarge
2627
import io.jadu.todoApp.ui.theme.Spacing
@@ -33,7 +34,8 @@ import org.koin.compose.koinInject
3334
@Composable
3435
fun HomePageContent(
3536
navController: NavController,
36-
viewModel: HomeScreenViewModel = koinInject()
37+
viewModel: HomeScreenViewModel = koinInject(),
38+
notificationVM: NotificationViewModel = koinInject()
3739
) {
3840
val uiState by viewModel.uiState.collectAsState()
3941

@@ -46,7 +48,12 @@ fun HomePageContent(
4648
Column(
4749
modifier = Modifier.padding(Spacing.s4)
4850
) {
49-
UserHeader(userProfile = uiState.userProfile)
51+
UserHeader(
52+
userProfile = uiState.userProfile,
53+
onNotificationClick = {
54+
notificationVM.onEnableNotificationsClicked()
55+
}
56+
)
5057
VSpacer(Spacing.s6)
5158
TaskProgressCard(
5259
progress = uiState.todayProgress,

composeApp/src/commonMain/kotlin/io/jadu/todoApp/ui/screens/homescreen/components/UserHeader.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.ColorFilter
2121
import androidx.compose.ui.layout.ContentScale
2222
import coil3.compose.AsyncImage
2323
import io.jadu.todoApp.data.model.UserProfile
24+
import io.jadu.todoApp.ui.components.bounceClickable
2425
import io.jadu.todoApp.ui.theme.BodyLarge
2526
import io.jadu.todoApp.ui.theme.BodySmall
2627
import io.jadu.todoApp.ui.theme.BodyXXLarge
@@ -36,7 +37,8 @@ import todo_list.composeapp.generated.resources.user_octagon
3637
@Composable
3738
@Preview(showBackground = true)
3839
fun UserHeader(
39-
userProfile: UserProfile = UserProfile()
40+
userProfile: UserProfile = UserProfile(),
41+
onNotificationClick: () -> Unit
4042
) {
4143

4244
val shouldShowNotificationDot = remember { mutableStateOf(true) }
@@ -103,7 +105,9 @@ fun UserHeader(
103105
}
104106

105107
Box(
106-
modifier = Modifier.size(Spacing.s6)
108+
modifier = Modifier
109+
.size(Spacing.s6)
110+
.bounceClickable(onClick = onNotificationClick)
107111
) {
108112
Image(
109113
painter = painterResource(Res.drawable.notification),

0 commit comments

Comments
 (0)