Skip to content
This repository was archived by the owner on Jun 27, 2024. It is now read-only.

Commit cd6fbec

Browse files
authored
Request permission to post notifications on devices running Android 13 and above (#68)
* Add user permission to post notifications in manifest * Add effect to check permission to post notifications * Check permission to post notification, when screen is created and restored * Add view effect to request notification permission * Request notification permission when it's not granted * Extract out notification permission launcher as class property
1 parent a155a43 commit cd6fbec

File tree

12 files changed

+114
-11
lines changed

12 files changed

+114
-11
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
6+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
67

78
<application
89
android:name=".PinnitApp"

app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationScreenViewEffect.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ import java.util.UUID
55
sealed class NotificationScreenViewEffect
66

77
data class UndoNotificationDeleteViewEffect(val notificationUuid: UUID) : NotificationScreenViewEffect()
8+
9+
object RequestNotificationPermissionViewEffect : NotificationScreenViewEffect()

app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreen.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package dev.sasikanth.pinnit.notifications
22

3+
import android.Manifest
4+
import android.annotation.SuppressLint
35
import android.os.Bundle
46
import android.view.LayoutInflater
57
import android.view.View
68
import android.view.ViewGroup
9+
import androidx.activity.result.contract.ActivityResultContracts
710
import androidx.core.view.doOnPreDraw
811
import androidx.core.view.isGone
912
import androidx.core.view.isVisible
@@ -65,6 +68,11 @@ class NotificationsScreen : Fragment(), NotificationsScreenUi {
6568
}
6669
}
6770

71+
private val notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { _ ->
72+
// At the moment we are not handling permission results. Since this is a notification app
73+
// expecting users to provide the notification permission for app to work
74+
}
75+
6876
private var _binding: FragmentNotificationsBinding? = null
6977
private val binding get() = _binding!!
7078

@@ -135,7 +143,10 @@ class NotificationsScreen : Fragment(), NotificationsScreenUi {
135143
}
136144
.show()
137145
}
138-
else -> throw IllegalArgumentException("Unknown view effect: $viewEffect")
146+
147+
RequestNotificationPermissionViewEffect -> requestNotificationPermission()
148+
149+
null -> throw NullPointerException()
139150
}
140151
}
141152

@@ -166,6 +177,11 @@ class NotificationsScreen : Fragment(), NotificationsScreenUi {
166177
adapter.submitList(null)
167178
}
168179

180+
@SuppressLint("InlinedApi")
181+
private fun requestNotificationPermission() {
182+
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
183+
}
184+
169185
private fun onToggleNotificationPinClicked(notification: PinnitNotification) {
170186
viewModel.dispatchEvent(TogglePinStatusClicked(notification))
171187
}

app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffect.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ data class CancelNotificationSchedule(val notificationId: UUID) : NotificationsS
2222
data class RemoveSchedule(val notificationId: UUID) : NotificationsScreenEffect()
2323

2424
data class ScheduleNotification(val notification: PinnitNotification) : NotificationsScreenEffect()
25+
26+
object CheckPermissionToPostNotification : NotificationsScreenEffect()
27+
28+
object RequestNotificationPermission : NotificationsScreenEffect()

app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandler.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,18 @@ class NotificationsScreenEffectHandler @AssistedInject constructor(
4444
is RemoveSchedule -> removeSchedule(effect, dispatchEvent)
4545

4646
is ScheduleNotification -> pinnitNotificationScheduler.scheduleNotification(effect.notification)
47+
48+
CheckPermissionToPostNotification -> checkPermissionToPostNotification(dispatchEvent)
49+
50+
RequestNotificationPermission -> viewEffectConsumer.accept(RequestNotificationPermissionViewEffect)
4751
}
4852
}
4953

54+
private fun checkPermissionToPostNotification(dispatchEvent: (NotificationsScreenEvent) -> Unit) {
55+
val canPostNotifications = notificationUtil.hasPermissionToPostNotifications()
56+
dispatchEvent(HasPermissionToPostNotifications(canPostNotifications))
57+
}
58+
5059
private fun loadNotifications(dispatchEvent: (NotificationsScreenEvent) -> Unit) {
5160
val notificationsFlow = notificationRepository.notifications()
5261
notificationsFlow

app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEvent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ data class RemovedNotificationSchedule(val notificationId: UUID) : Notifications
2020
data class RemoveNotificationScheduleClicked(val notificationId: UUID) : NotificationsScreenEvent()
2121

2222
data class RestoredDeletedNotification(val notification: PinnitNotification) : NotificationsScreenEvent()
23+
24+
data class HasPermissionToPostNotifications(val canPostNotifications: Boolean) : NotificationsScreenEvent()

app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInit.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import javax.inject.Inject
77

88
class NotificationsScreenInit @Inject constructor() : Init<NotificationsScreenModel, NotificationsScreenEffect> {
99
override fun init(model: NotificationsScreenModel): First<NotificationsScreenModel, NotificationsScreenEffect> {
10-
val effects = if (model.notificationsQueried.not()) {
10+
val effects = mutableSetOf<NotificationsScreenEffect>(CheckPermissionToPostNotification)
11+
12+
if (model.notificationsQueried.not()) {
1113
// We are only checking for notifications visibility during
1214
// screen create because system notifications are disappear only
1315
// if the app is force closed (or when updating). So app needs
1416
// to be reopened again. Notifications will persist
1517
// orientation changes, so no point checking again.
16-
setOf(LoadNotifications, CheckNotificationsVisibility)
17-
} else {
18-
emptySet()
18+
effects.addAll(listOf(LoadNotifications, CheckNotificationsVisibility))
1919
}
2020

2121
return first(model, effects)

app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdate.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package dev.sasikanth.pinnit.notifications
33
import com.spotify.mobius.Next
44
import com.spotify.mobius.Next.dispatch
55
import com.spotify.mobius.Next.next
6+
import com.spotify.mobius.Next.noChange
67
import com.spotify.mobius.Update
78
import javax.inject.Inject
89

@@ -17,6 +18,15 @@ class NotificationsScreenUpdate @Inject constructor() : Update<NotificationsScre
1718
is RemovedNotificationSchedule -> dispatch(setOf(CancelNotificationSchedule(event.notificationId)))
1819
is RemoveNotificationScheduleClicked -> dispatch(setOf(RemoveSchedule(event.notificationId)))
1920
is RestoredDeletedNotification -> dispatch(setOf(ScheduleNotification(event.notification)))
21+
is HasPermissionToPostNotifications -> hasPermissionToPostNotifications(event.canPostNotifications)
22+
}
23+
}
24+
25+
private fun hasPermissionToPostNotifications(canPostNotifications: Boolean): Next<NotificationsScreenModel, NotificationsScreenEffect> {
26+
return if (!canPostNotifications) {
27+
dispatch(setOf(RequestNotificationPermission))
28+
} else {
29+
noChange()
2030
}
2131
}
2232

app/src/main/java/dev/sasikanth/pinnit/utils/notification/NotificationUtil.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package dev.sasikanth.pinnit.utils.notification
22

3+
import android.Manifest
34
import android.app.Notification
45
import android.app.NotificationChannel
56
import android.app.NotificationManager
67
import android.app.PendingIntent
78
import android.content.Context
89
import android.content.Intent
10+
import android.content.pm.PackageManager
911
import android.os.Build
1012
import androidx.core.app.NotificationCompat
1113
import androidx.core.app.NotificationCompat.Action
1214
import androidx.core.app.NotificationManagerCompat
15+
import androidx.core.content.ContextCompat
1316
import androidx.core.content.getSystemService
1417
import androidx.navigation.NavDeepLinkBuilder
1518
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -77,6 +80,15 @@ class NotificationUtil @Inject constructor(
7780
}
7881
}
7982

83+
/**
84+
* Check if the app has permission to post notifications on devices running Android version >= 13
85+
*/
86+
fun hasPermissionToPostNotifications() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
87+
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
88+
} else {
89+
true
90+
}
91+
8092
private fun buildSystemNotification(notification: PinnitNotification): Notification {
8193
val content = notification.content.orEmpty()
8294
val editorScreenArgs = EditorScreenArgs(

app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandlerTest.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
88
import com.nhaarman.mockitokotlin2.whenever
99
import com.spotify.mobius.Connection
1010
import com.spotify.mobius.test.RecordingConsumer
11-
import dev.sasikanth.sharedtestcode.TestData
1211
import dev.sasikanth.pinnit.scheduler.PinnitNotificationScheduler
1312
import dev.sasikanth.pinnit.utils.TestDispatcherProvider
14-
import dev.sasikanth.sharedtestcode.utils.TestUtcClock
1513
import dev.sasikanth.pinnit.utils.notification.NotificationUtil
1614
import kotlinx.coroutines.flow.flowOf
1715
import kotlinx.coroutines.test.TestCoroutineScope
@@ -273,4 +271,29 @@ class NotificationsScreenEffectHandlerTest {
273271
verify(pinnitNotificationScheduler).scheduleNotification(notification)
274272
verifyNoMoreInteractions(pinnitNotificationScheduler)
275273
}
274+
275+
@Test
276+
fun `when check notifications permission effect is received, then check the permission`() {
277+
// given
278+
whenever(notificationUtil.hasPermissionToPostNotifications()) doReturn true
279+
280+
// when
281+
connection.accept(CheckPermissionToPostNotification)
282+
283+
// then
284+
consumer.assertValues(HasPermissionToPostNotifications(canPostNotifications = true))
285+
286+
verify(notificationUtil).hasPermissionToPostNotifications()
287+
verifyNoMoreInteractions(notificationUtil)
288+
}
289+
290+
@Test
291+
fun `when request notification permission effect is received, then request permission to post notifications`() {
292+
// when
293+
connection.accept(RequestNotificationPermission)
294+
295+
// then
296+
consumer.assertValues()
297+
viewActionsConsumer.accept(RequestNotificationPermissionViewEffect)
298+
}
276299
}

0 commit comments

Comments
 (0)