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

Commit

Permalink
Request permission to post notifications on devices running Android 1…
Browse files Browse the repository at this point in the history
…3 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
  • Loading branch information
msasikanth authored Oct 21, 2022
1 parent a155a43 commit cd6fbec
Show file tree
Hide file tree
Showing 12 changed files with 114 additions and 11 deletions.
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:name=".PinnitApp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ import java.util.UUID
sealed class NotificationScreenViewEffect

data class UndoNotificationDeleteViewEffect(val notificationUuid: UUID) : NotificationScreenViewEffect()

object RequestNotificationPermissionViewEffect : NotificationScreenViewEffect()
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package dev.sasikanth.pinnit.notifications

import android.Manifest
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.doOnPreDraw
import androidx.core.view.isGone
import androidx.core.view.isVisible
Expand Down Expand Up @@ -65,6 +68,11 @@ class NotificationsScreen : Fragment(), NotificationsScreenUi {
}
}

private val notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { _ ->
// 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!!

Expand Down Expand Up @@ -135,7 +143,10 @@ class NotificationsScreen : Fragment(), NotificationsScreenUi {
}
.show()
}
else -> throw IllegalArgumentException("Unknown view effect: $viewEffect")

RequestNotificationPermissionViewEffect -> requestNotificationPermission()

null -> throw NullPointerException()
}
}

Expand Down Expand Up @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import javax.inject.Inject

class NotificationsScreenInit @Inject constructor() : Init<NotificationsScreenModel, NotificationsScreenEffect> {
override fun init(model: NotificationsScreenModel): First<NotificationsScreenModel, NotificationsScreenEffect> {
val effects = if (model.notificationsQueried.not()) {
val effects = mutableSetOf<NotificationsScreenEffect>(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -17,6 +18,15 @@ class NotificationsScreenUpdate @Inject constructor() : Update<NotificationsScre
is RemovedNotificationSchedule -> 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<NotificationsScreenModel, NotificationsScreenEffect> {
return if (!canPostNotifications) {
dispatch(setOf(RequestNotificationPermission))
} else {
noChange()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class NotificationsScreenInitTest {
.then(
assertThatFirst(
hasModel(defaultModel),
hasEffects(LoadNotifications, CheckNotificationsVisibility)
hasEffects(LoadNotifications, CheckNotificationsVisibility, CheckPermissionToPostNotification)
)
)
}
Expand All @@ -41,7 +41,7 @@ class NotificationsScreenInitTest {
.then(
assertThatFirst(
hasModel(restoredModel),
hasNoEffects()
hasEffects(CheckPermissionToPostNotification)
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
)
)
}
}

0 comments on commit cd6fbec

Please sign in to comment.