diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d0759c5b..7e2b513d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
+
+ // At the moment we are not handling permission results. Since this is a notification app
+ // expecting users to provide the notification permission for app to work
+ }
+
private var _binding: FragmentNotificationsBinding? = null
private val binding get() = _binding!!
@@ -135,7 +143,10 @@ class NotificationsScreen : Fragment(), NotificationsScreenUi {
}
.show()
}
- else -> throw IllegalArgumentException("Unknown view effect: $viewEffect")
+
+ RequestNotificationPermissionViewEffect -> requestNotificationPermission()
+
+ null -> throw NullPointerException()
}
}
@@ -166,6 +177,11 @@ class NotificationsScreen : Fragment(), NotificationsScreenUi {
adapter.submitList(null)
}
+ @SuppressLint("InlinedApi")
+ private fun requestNotificationPermission() {
+ notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+
private fun onToggleNotificationPinClicked(notification: PinnitNotification) {
viewModel.dispatchEvent(TogglePinStatusClicked(notification))
}
diff --git a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffect.kt b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffect.kt
index cf78d627..d3eba803 100644
--- a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffect.kt
+++ b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffect.kt
@@ -22,3 +22,7 @@ data class CancelNotificationSchedule(val notificationId: UUID) : NotificationsS
data class RemoveSchedule(val notificationId: UUID) : NotificationsScreenEffect()
data class ScheduleNotification(val notification: PinnitNotification) : NotificationsScreenEffect()
+
+object CheckPermissionToPostNotification : NotificationsScreenEffect()
+
+object RequestNotificationPermission : NotificationsScreenEffect()
diff --git a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandler.kt b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandler.kt
index 06f623ad..48b142bc 100644
--- a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandler.kt
+++ b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandler.kt
@@ -44,9 +44,18 @@ class NotificationsScreenEffectHandler @AssistedInject constructor(
is RemoveSchedule -> removeSchedule(effect, dispatchEvent)
is ScheduleNotification -> pinnitNotificationScheduler.scheduleNotification(effect.notification)
+
+ CheckPermissionToPostNotification -> checkPermissionToPostNotification(dispatchEvent)
+
+ RequestNotificationPermission -> viewEffectConsumer.accept(RequestNotificationPermissionViewEffect)
}
}
+ private fun checkPermissionToPostNotification(dispatchEvent: (NotificationsScreenEvent) -> Unit) {
+ val canPostNotifications = notificationUtil.hasPermissionToPostNotifications()
+ dispatchEvent(HasPermissionToPostNotifications(canPostNotifications))
+ }
+
private fun loadNotifications(dispatchEvent: (NotificationsScreenEvent) -> Unit) {
val notificationsFlow = notificationRepository.notifications()
notificationsFlow
diff --git a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEvent.kt b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEvent.kt
index 571a07cb..39500452 100644
--- a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEvent.kt
+++ b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEvent.kt
@@ -20,3 +20,5 @@ data class RemovedNotificationSchedule(val notificationId: UUID) : Notifications
data class RemoveNotificationScheduleClicked(val notificationId: UUID) : NotificationsScreenEvent()
data class RestoredDeletedNotification(val notification: PinnitNotification) : NotificationsScreenEvent()
+
+data class HasPermissionToPostNotifications(val canPostNotifications: Boolean) : NotificationsScreenEvent()
diff --git a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInit.kt b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInit.kt
index 55b5d0bb..f5009f3b 100644
--- a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInit.kt
+++ b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInit.kt
@@ -7,15 +7,15 @@ import javax.inject.Inject
class NotificationsScreenInit @Inject constructor() : Init {
override fun init(model: NotificationsScreenModel): First {
- val effects = if (model.notificationsQueried.not()) {
+ val effects = mutableSetOf(CheckPermissionToPostNotification)
+
+ if (model.notificationsQueried.not()) {
// We are only checking for notifications visibility during
// screen create because system notifications are disappear only
// if the app is force closed (or when updating). So app needs
// to be reopened again. Notifications will persist
// orientation changes, so no point checking again.
- setOf(LoadNotifications, CheckNotificationsVisibility)
- } else {
- emptySet()
+ effects.addAll(listOf(LoadNotifications, CheckNotificationsVisibility))
}
return first(model, effects)
diff --git a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdate.kt b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdate.kt
index 319664af..f044eec9 100644
--- a/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdate.kt
+++ b/app/src/main/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdate.kt
@@ -3,6 +3,7 @@ package dev.sasikanth.pinnit.notifications
import com.spotify.mobius.Next
import com.spotify.mobius.Next.dispatch
import com.spotify.mobius.Next.next
+import com.spotify.mobius.Next.noChange
import com.spotify.mobius.Update
import javax.inject.Inject
@@ -17,6 +18,15 @@ class NotificationsScreenUpdate @Inject constructor() : Update dispatch(setOf(CancelNotificationSchedule(event.notificationId)))
is RemoveNotificationScheduleClicked -> dispatch(setOf(RemoveSchedule(event.notificationId)))
is RestoredDeletedNotification -> dispatch(setOf(ScheduleNotification(event.notification)))
+ is HasPermissionToPostNotifications -> hasPermissionToPostNotifications(event.canPostNotifications)
+ }
+ }
+
+ private fun hasPermissionToPostNotifications(canPostNotifications: Boolean): Next {
+ return if (!canPostNotifications) {
+ dispatch(setOf(RequestNotificationPermission))
+ } else {
+ noChange()
}
}
diff --git a/app/src/main/java/dev/sasikanth/pinnit/utils/notification/NotificationUtil.kt b/app/src/main/java/dev/sasikanth/pinnit/utils/notification/NotificationUtil.kt
index d7502c36..4e5b0759 100644
--- a/app/src/main/java/dev/sasikanth/pinnit/utils/notification/NotificationUtil.kt
+++ b/app/src/main/java/dev/sasikanth/pinnit/utils/notification/NotificationUtil.kt
@@ -1,15 +1,18 @@
package dev.sasikanth.pinnit.utils.notification
+import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.Action
import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.navigation.NavDeepLinkBuilder
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -77,6 +80,15 @@ class NotificationUtil @Inject constructor(
}
}
+ /**
+ * Check if the app has permission to post notifications on devices running Android version >= 13
+ */
+ fun hasPermissionToPostNotifications() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
+ } else {
+ true
+ }
+
private fun buildSystemNotification(notification: PinnitNotification): Notification {
val content = notification.content.orEmpty()
val editorScreenArgs = EditorScreenArgs(
diff --git a/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandlerTest.kt b/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandlerTest.kt
index 6467a148..88eedff3 100644
--- a/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandlerTest.kt
+++ b/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenEffectHandlerTest.kt
@@ -8,10 +8,8 @@ import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.whenever
import com.spotify.mobius.Connection
import com.spotify.mobius.test.RecordingConsumer
-import dev.sasikanth.sharedtestcode.TestData
import dev.sasikanth.pinnit.scheduler.PinnitNotificationScheduler
import dev.sasikanth.pinnit.utils.TestDispatcherProvider
-import dev.sasikanth.sharedtestcode.utils.TestUtcClock
import dev.sasikanth.pinnit.utils.notification.NotificationUtil
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestCoroutineScope
@@ -273,4 +271,29 @@ class NotificationsScreenEffectHandlerTest {
verify(pinnitNotificationScheduler).scheduleNotification(notification)
verifyNoMoreInteractions(pinnitNotificationScheduler)
}
+
+ @Test
+ fun `when check notifications permission effect is received, then check the permission`() {
+ // given
+ whenever(notificationUtil.hasPermissionToPostNotifications()) doReturn true
+
+ // when
+ connection.accept(CheckPermissionToPostNotification)
+
+ // then
+ consumer.assertValues(HasPermissionToPostNotifications(canPostNotifications = true))
+
+ verify(notificationUtil).hasPermissionToPostNotifications()
+ verifyNoMoreInteractions(notificationUtil)
+ }
+
+ @Test
+ fun `when request notification permission effect is received, then request permission to post notifications`() {
+ // when
+ connection.accept(RequestNotificationPermission)
+
+ // then
+ consumer.assertValues()
+ viewActionsConsumer.accept(RequestNotificationPermissionViewEffect)
+ }
}
diff --git a/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInitTest.kt b/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInitTest.kt
index 46347c57..e4235647 100644
--- a/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInitTest.kt
+++ b/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenInitTest.kt
@@ -21,7 +21,7 @@ class NotificationsScreenInitTest {
.then(
assertThatFirst(
hasModel(defaultModel),
- hasEffects(LoadNotifications, CheckNotificationsVisibility)
+ hasEffects(LoadNotifications, CheckNotificationsVisibility, CheckPermissionToPostNotification)
)
)
}
@@ -41,7 +41,7 @@ class NotificationsScreenInitTest {
.then(
assertThatFirst(
hasModel(restoredModel),
- hasNoEffects()
+ hasEffects(CheckPermissionToPostNotification)
)
)
}
diff --git a/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdateTest.kt b/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdateTest.kt
index 012df837..4468497b 100644
--- a/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdateTest.kt
+++ b/app/src/test/java/dev/sasikanth/pinnit/notifications/NotificationsScreenUpdateTest.kt
@@ -6,8 +6,6 @@ import com.spotify.mobius.test.NextMatchers.hasNoEffects
import com.spotify.mobius.test.NextMatchers.hasNoModel
import com.spotify.mobius.test.UpdateSpec
import com.spotify.mobius.test.UpdateSpec.assertThatNext
-import dev.sasikanth.sharedtestcode.TestData
-import dev.sasikanth.sharedtestcode.utils.TestUtcClock
import org.junit.Test
import java.time.Instant
import java.time.LocalDate
@@ -188,4 +186,30 @@ class NotificationsScreenUpdateTest {
)
)
}
+
+ @Test
+ fun `when notifications permission is not granted, then request notifications permission`() {
+ updateSpec
+ .given(defaultModel)
+ .whenEvent(HasPermissionToPostNotifications(canPostNotifications = false))
+ .then(
+ assertThatNext(
+ hasNoModel(),
+ hasEffects(RequestNotificationPermission)
+ )
+ )
+ }
+
+ @Test
+ fun `when notifications permission is granted, then do nothing`() {
+ updateSpec
+ .given(defaultModel)
+ .whenEvent(HasPermissionToPostNotifications(canPostNotifications = true))
+ .then(
+ assertThatNext(
+ hasNoModel(),
+ hasNoEffects()
+ )
+ )
+ }
}