Skip to content

Commit

Permalink
Merge pull request #18239 from wordpress-mobile/issue/17714-notificat…
Browse files Browse the repository at this point in the history
…ion-runtime-permission

Runtime notifications permission
  • Loading branch information
irfano authored Apr 8, 2023
2 parents cdfdd0b + d26cfbb commit e1ff390
Show file tree
Hide file tree
Showing 17 changed files with 495 additions and 44 deletions.
5 changes: 3 additions & 2 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
*** PLEASE FOLLOW THIS FORMAT: [<priority indicator, more stars = higher priority>] <description> [<PR URL>]

22.2
[***] [internal] Adds media permissions support for Android 13 [https://github.com/wordpress-mobile/WordPress-Android/pull/18183]
-----

* [*] Adds runtime notifications permission [https://github.com/wordpress-mobile/WordPress-Android/pull/18239]
* [**] [internal] Adds media permissions support for Android 13 [https://github.com/wordpress-mobile/WordPress-Android/pull/18183]
-----

22.1
-----
Expand Down
1 change: 1 addition & 0 deletions WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- GCM all build types configuration -->
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.wordpress.android.ui.bloggingreminders

import android.Manifest
import android.content.Context
import android.content.DialogInterface
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
Expand All @@ -19,6 +21,8 @@ import org.wordpress.android.databinding.RecyclerViewPrimaryButtonBottomSheetBin
import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboardingDialogFragment
import org.wordpress.android.ui.bloggingprompts.onboarding.BloggingPromptsOnboardingDialogFragment.DialogType.INFORMATION
import org.wordpress.android.ui.utils.UiHelpers
import org.wordpress.android.util.PermissionUtils
import org.wordpress.android.util.WPPermissionUtils
import org.wordpress.android.util.extensions.disableAnimation
import org.wordpress.android.viewmodel.observeEvent
import javax.inject.Inject
Expand Down Expand Up @@ -61,8 +65,10 @@ class BloggingReminderBottomSheetFragment : BottomSheetDialogFragment() {
}
}
})
viewModel =
ViewModelProvider(requireActivity(), viewModelFactory).get(BloggingRemindersViewModel::class.java)
viewModel = ViewModelProvider(requireActivity(), viewModelFactory)[BloggingRemindersViewModel::class.java]

setPermissionState()

viewModel.uiState.observe(this@BloggingReminderBottomSheetFragment) { uiState ->
(contentRecyclerView.adapter as? BloggingRemindersAdapter)?.submitList(uiState?.uiItems ?: listOf())
if (uiState?.primaryButton != null) {
Expand All @@ -86,11 +92,10 @@ class BloggingReminderBottomSheetFragment : BottomSheetDialogFragment() {
BloggingReminderUtils.observeTimePicker(
viewModel.isTimePickerShowing,
viewLifecycleOwner,
BloggingReminderTimePicker.TAG,
{
requireActivity().supportFragmentManager
}
)
BloggingReminderTimePicker.TAG
) {
requireActivity().supportFragmentManager
}

savedInstanceState?.let { viewModel.restoreState(it) }

Expand All @@ -101,6 +106,60 @@ class BloggingReminderBottomSheetFragment : BottomSheetDialogFragment() {
}
}

override fun onResume() {
super.onResume()
val hasPermission = PermissionUtils.checkNotificationsPermission(activity)
if (hasPermission) {
viewModel.onPermissionGranted()
}
}

@Suppress("OVERRIDE_DEPRECATION")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
val granted = WPPermissionUtils.setPermissionListAsked(
requireActivity(),
requestCode,
permissions,
grantResults,
false
)
if (granted) {
viewModel.onPermissionGranted()
} else {
val isAlwaysDenied = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
WPPermissionUtils.isPermissionAlwaysDenied(
requireActivity(),
Manifest.permission.POST_NOTIFICATIONS
)
viewModel.onPermissionDenied(isAlwaysDenied)
}
}

private fun setPermissionState() {
val isAlwaysDenied = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
WPPermissionUtils.isPermissionAlwaysDenied(
requireActivity(),
Manifest.permission.POST_NOTIFICATIONS
)
viewModel.setPermissionState(PermissionUtils.checkNotificationsPermission(activity), isAlwaysDenied)

@Suppress("DEPRECATION")
viewModel.requestPermission.observeEvent(this@BloggingReminderBottomSheetFragment) { request: Boolean ->
if (request && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions(
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
WPPermissionUtils.NOTIFICATIONS_PERMISSION_REQUEST_CODE
)
}
}
viewModel.showDevicePermissionSettings
.observeEvent(this@BloggingReminderBottomSheetFragment) { show: Boolean ->
if (show) {
WPPermissionUtils.showNotificationsSettings(requireActivity())
}
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
viewModel.saveState(outState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.lifecycle.distinctUntilChanged
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import org.wordpress.android.R
import org.wordpress.android.fluxc.store.BloggingRemindersStore
import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.modules.UI_THREAD
Expand All @@ -22,6 +23,7 @@ import org.wordpress.android.util.extensions.getSerializableCompat
import org.wordpress.android.util.merge
import org.wordpress.android.util.perform
import org.wordpress.android.viewmodel.Event
import org.wordpress.android.viewmodel.ResourceProvider
import org.wordpress.android.viewmodel.ScopedViewModel
import org.wordpress.android.workers.reminder.ReminderScheduler
import java.time.DayOfWeek
Expand All @@ -35,11 +37,13 @@ class BloggingRemindersViewModel @Inject constructor(
private val prologueBuilder: PrologueBuilder,
private val daySelectionBuilder: DaySelectionBuilder,
private val epilogueBuilder: EpilogueBuilder,
private val notificationsPermissionBuilder: NotificationsPermissionBuilder,
private val dayLabelUtils: DayLabelUtils,
private val analyticsTracker: BloggingRemindersAnalyticsTracker,
private val reminderScheduler: ReminderScheduler,
private val mapper: BloggingRemindersModelMapper,
private val siteStore: SiteStore
private val siteStore: SiteStore,
private val resourceProvider: ResourceProvider
) : ScopedViewModel(mainDispatcher) {
private val _isBottomSheetShowing = MutableLiveData<Event<Boolean>>()
val isBottomSheetShowing = _isBottomSheetShowing as LiveData<Event<Boolean>>
Expand All @@ -50,11 +54,19 @@ class BloggingRemindersViewModel @Inject constructor(
private val _showBloggingPromptHelpDialogVisible = MutableLiveData<Event<Boolean>>()
val showBloggingPromptHelpDialogVisible = _showBloggingPromptHelpDialogVisible as LiveData<Event<Boolean>>

private val _requestPermission = MutableLiveData<Event<Boolean>>()
val requestPermission = _requestPermission as LiveData<Event<Boolean>>

private val _showDevicePermissionSettings = MutableLiveData<Event<Boolean>>()
val showDevicePermissionSettings = _showDevicePermissionSettings as LiveData<Event<Boolean>>

private val _selectedScreen = MutableLiveData<Screen>()
private val selectedScreen = _selectedScreen.perform { onScreenChanged(it) }

private val _bloggingRemindersModel = MutableLiveData<BloggingRemindersUiModel>()
private val _isFirstTimeFlow = MutableLiveData<Boolean>()
private var hasNotificationsPermissionState = false
private var notificationsPermissionAlwaysDeniedState = false

val uiState: LiveData<UiState> = merge(
selectedScreen,
Expand All @@ -72,6 +84,12 @@ class BloggingRemindersViewModel @Inject constructor(
this::togglePromptSwitch,
this::showBloggingPromptDialog
)
Screen.NOTIFICATIONS_PERMISSION -> {
notificationsPermissionBuilder.buildUiItems(
appName = resourceProvider.getString(R.string.app_name),
showAppSettingsGuide = notificationsPermissionAlwaysDeniedState
)
}
Screen.EPILOGUE -> epilogueBuilder.buildUiItems(bloggingRemindersModel)
}
val primaryButton = when (screen) {
Expand All @@ -84,6 +102,9 @@ class BloggingRemindersViewModel @Inject constructor(
isFirstTimeFlow == true,
this::onSelectionButtonClick
)
Screen.NOTIFICATIONS_PERMISSION -> {
notificationsPermissionBuilder.buildPrimaryButton(onPermissionButtonTapped)
}
Screen.EPILOGUE -> epilogueBuilder.buildPrimaryButton(finish)
}
UiState(uiItems, primaryButton)
Expand All @@ -103,6 +124,14 @@ class BloggingRemindersViewModel @Inject constructor(
_isBottomSheetShowing.value = Event(false)
}

private val onPermissionButtonTapped: () -> Unit = {
if (notificationsPermissionAlwaysDeniedState) {
_showDevicePermissionSettings.value = Event(true)
} else {
_requestPermission.value = Event(true)
}
}

private fun onScreenChanged(screen: Screen) {
analyticsTracker.trackScreenShown(screen)
}
Expand Down Expand Up @@ -135,7 +164,7 @@ class BloggingRemindersViewModel @Inject constructor(
}
}

fun selectDay(day: DayOfWeek) {
private fun selectDay(day: DayOfWeek) {
val currentState = _bloggingRemindersModel.value!!
val enabledDays = currentState.enabledDays.toMutableSet()
if (enabledDays.contains(day)) {
Expand All @@ -146,7 +175,12 @@ class BloggingRemindersViewModel @Inject constructor(
_bloggingRemindersModel.value = currentState.copy(enabledDays = enabledDays)
}

fun selectTime() {
fun setPermissionState(hasNotificationsPermission: Boolean, notificationsPermissionAlwaysDenied: Boolean) {
hasNotificationsPermissionState = hasNotificationsPermission
notificationsPermissionAlwaysDeniedState = notificationsPermissionAlwaysDenied
}

private fun selectTime() {
_isTimePickerShowing.value = Event(true)
}

Expand Down Expand Up @@ -178,12 +212,16 @@ class BloggingRemindersViewModel @Inject constructor(
analyticsTracker.trackPrimaryButtonPressed(Screen.SELECTION)
if (bloggingRemindersModel != null) {
launch {
val daysCount = bloggingRemindersModel.enabledDays.size
if (!checkPermission()) {
// There is no permission
return@launch
}
bloggingRemindersStore.updateBloggingReminders(
mapper.toDomainModel(
bloggingRemindersModel
)
)
val daysCount = bloggingRemindersModel.enabledDays.size
if (daysCount > 0) {
reminderScheduler.schedule(
bloggingRemindersModel.siteId,
Expand All @@ -203,6 +241,20 @@ class BloggingRemindersViewModel @Inject constructor(
}
}

private fun checkPermission(): Boolean {
return when {
!hasNotificationsPermissionState && notificationsPermissionAlwaysDeniedState -> {
_selectedScreen.value = Screen.NOTIFICATIONS_PERMISSION
false
}
!hasNotificationsPermissionState -> {
_requestPermission.value = Event(true)
false
}
else -> true // Already has permission
}
}

fun saveState(outState: Bundle) {
_selectedScreen.value?.let {
outState.putSerializable(SELECTED_SCREEN, it)
Expand Down Expand Up @@ -274,16 +326,37 @@ class BloggingRemindersViewModel @Inject constructor(
when (val screen = selectedScreen.value) {
Screen.PROLOGUE,
Screen.PROLOGUE_SETTINGS,
Screen.NOTIFICATIONS_PERMISSION,
Screen.SELECTION -> analyticsTracker.trackFlowDismissed(screen)
Screen.EPILOGUE -> analyticsTracker.trackFlowCompleted()
null -> Unit // Do nothing
}
}

fun onPermissionGranted() {
if (!hasNotificationsPermissionState) {
// Permission state is changed.
hasNotificationsPermissionState = true
notificationsPermissionAlwaysDeniedState = false

if (_selectedScreen.value == Screen.NOTIFICATIONS_PERMISSION) {
onSelectionButtonClick(_bloggingRemindersModel.value)
}
}
}

fun onPermissionDenied(isAlwaysDenied: Boolean) {
hasNotificationsPermissionState = false
notificationsPermissionAlwaysDeniedState = isAlwaysDenied

_selectedScreen.value = Screen.NOTIFICATIONS_PERMISSION
}

enum class Screen(val trackingName: String) {
PROLOGUE("main"), // displayed after post is published
PROLOGUE_SETTINGS("main"), // displayed from Site Settings before showing cadence selector
SELECTION("day_picker"), // cadence selector
NOTIFICATIONS_PERMISSION("notifications_permission"),
EPILOGUE("all_set")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.wordpress.android.ui.bloggingreminders

import org.wordpress.android.R.drawable
import org.wordpress.android.R.string
import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.Caption
import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.EmphasizedText
import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.HighEmphasisText
import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.Illustration
import org.wordpress.android.ui.bloggingreminders.BloggingRemindersItem.Title
import org.wordpress.android.ui.bloggingreminders.BloggingRemindersViewModel.UiState.PrimaryButton
import org.wordpress.android.ui.utils.ListItemInteraction
import org.wordpress.android.ui.utils.UiString
import org.wordpress.android.ui.utils.UiString.UiStringRes
import javax.inject.Inject

class NotificationsPermissionBuilder @Inject constructor() {
fun buildUiItems(appName: String, showAppSettingsGuide: Boolean): List<BloggingRemindersItem> {
val title = UiStringRes(string.blogging_reminders_notifications_permission_title)

val body = UiString.UiStringResWithParams(
string.blogging_reminders_notifications_permission_description,
UiString.UiStringText(appName)
)

val uiItems = mutableListOf(
Illustration(drawable.img_illustration_bell_yellow_96dp),
Title(title),
Caption(UiStringRes(string.blogging_reminders_notifications_permission_caption))
)
if (showAppSettingsGuide) {
uiItems.add(HighEmphasisText(EmphasizedText(body, false)))
}
return uiItems
}

fun buildPrimaryButton(onDone: () -> Unit): PrimaryButton {
return PrimaryButton(
UiStringRes(string.blogging_reminders_notifications_permission_primary_button),
enabled = true,
ListItemInteraction.create(onDone)
)
}
}
Loading

0 comments on commit e1ff390

Please sign in to comment.