Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Runtime notifications permission #18239

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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