diff --git a/app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt b/app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt index cba50984a92..797186d6207 100644 --- a/app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt +++ b/app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt @@ -1,13 +1,9 @@ package au.com.shiftyjelly.pocketcasts.ui -import android.Manifest -import android.annotation.SuppressLint import android.app.Activity import android.content.Intent -import android.content.pm.PackageManager import android.content.res.Configuration import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.View import android.view.ViewGroup @@ -21,7 +17,6 @@ import androidx.appcompat.widget.Toolbar import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView -import androidx.core.content.ContextCompat import androidx.core.view.doOnLayout import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams @@ -142,6 +137,7 @@ import au.com.shiftyjelly.pocketcasts.ui.helper.StatusBarColor import au.com.shiftyjelly.pocketcasts.ui.theme.Theme import au.com.shiftyjelly.pocketcasts.ui.theme.ThemeColor import au.com.shiftyjelly.pocketcasts.utils.Network +import au.com.shiftyjelly.pocketcasts.utils.NotificationPermissionHelper import au.com.shiftyjelly.pocketcasts.utils.featureflag.Feature import au.com.shiftyjelly.pocketcasts.utils.featureflag.FeatureFlag import au.com.shiftyjelly.pocketcasts.utils.log.LogBuffer @@ -311,43 +307,6 @@ class MainActivity : private val deepLinkFactory = DeepLinkFactory() - @SuppressLint("WrongConstant") // for custom snackbar duration constant - private fun checkForNotificationPermission(onPermissionGranted: () -> Unit = {}) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - when { - ContextCompat.checkSelfPermission( - this, Manifest.permission.POST_NOTIFICATIONS, - ) == PackageManager.PERMISSION_GRANTED -> { - onPermissionGranted() - } - shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> { - if (settings.isNotificationsDisabledMessageShown()) return - Snackbar.make( - findViewById(R.id.root), - getString(LR.string.notifications_blocked_warning), - EXTRA_LONG_SNACKBAR_DURATION_MS, - ).setAction( - getString(LR.string.notifications_blocked_warning_snackbar_action) - .uppercase(Locale.getDefault()), - ) { - // Responds to click on the action - val intent = Intent(AndroidProviderSettings.ACTION_APPLICATION_DETAILS_SETTINGS) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val uri: Uri = Uri.fromParts("package", packageName, null) - intent.data = uri - startActivity(intent) - }.show() - settings.setNotificationsDisabledMessageShown(true) - } - else -> { - notificationPermissionLauncher.launch( - Manifest.permission.POST_NOTIFICATIONS, - ) - } - } - } - } - override fun onCreate(savedInstanceState: Bundle?) { Timber.d("Main Activity onCreate") super.onCreate(savedInstanceState) @@ -364,7 +323,6 @@ class MainActivity : binding = ActivityMainBinding.inflate(layoutInflater) val view = binding.root setContentView(view) - checkForNotificationPermission() binding.bottomNavigation.doOnLayout { val miniPlayerHeight = miniPlayerHeight @@ -1597,7 +1555,33 @@ class MainActivity : } override fun checkNotificationPermission(onPermissionGranted: () -> Unit) { - checkForNotificationPermission(onPermissionGranted) + NotificationPermissionHelper.checkForNotificationPermission( + this, + notificationPermissionLauncher, + onShowRequestPermissionRationale = { + if (settings.isNotificationsDisabledMessageShown()) return@checkForNotificationPermission + + Snackbar.make( + findViewById(R.id.root), + getString(LR.string.notifications_blocked_warning), + EXTRA_LONG_SNACKBAR_DURATION_MS, + ).setAction( + getString(LR.string.notifications_blocked_warning_snackbar_action) + .uppercase(Locale.getDefault()), + ) { + // Responds to click on the action + val intent = Intent(AndroidProviderSettings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val uri: Uri = Uri.fromParts("package", packageName, null) + intent.data = uri + startActivity(intent) + }.show() + + settings.setNotificationsDisabledMessageShown(true) + }, + onPermissionGranted = onPermissionGranted, + onPermissionHandlingNotRequired = {}, + ) } private fun showPlayerBookmarks() { diff --git a/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastFragment.kt b/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastFragment.kt index 40a64731b54..4835e8ae332 100644 --- a/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastFragment.kt +++ b/modules/features/podcasts/src/main/java/au/com/shiftyjelly/pocketcasts/podcasts/view/podcast/PodcastFragment.kt @@ -1,11 +1,14 @@ package au.com.shiftyjelly.pocketcasts.podcasts.view.podcast +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.os.BundleCompat @@ -67,6 +70,7 @@ import au.com.shiftyjelly.pocketcasts.ui.helper.FragmentHostListener import au.com.shiftyjelly.pocketcasts.ui.helper.StatusBarColor import au.com.shiftyjelly.pocketcasts.ui.images.CoilManager import au.com.shiftyjelly.pocketcasts.ui.theme.ThemeColor +import au.com.shiftyjelly.pocketcasts.utils.NotificationPermissionHelper import au.com.shiftyjelly.pocketcasts.utils.extensions.dpToPx import au.com.shiftyjelly.pocketcasts.utils.featureflag.Feature import au.com.shiftyjelly.pocketcasts.utils.featureflag.FeatureFlag @@ -84,7 +88,9 @@ import au.com.shiftyjelly.pocketcasts.views.helper.UiUtil import au.com.shiftyjelly.pocketcasts.views.multiselect.MultiSelectBookmarksHelper.NavigationState import au.com.shiftyjelly.pocketcasts.views.multiselect.MultiSelectHelper import au.com.shiftyjelly.pocketcasts.views.multiselect.MultiSelectToolbar +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import java.util.Locale import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers @@ -166,6 +172,14 @@ class PodcastFragment : BaseFragment(), Toolbar.OnMenuItemClickListener { private var listState: Parcelable? = null + private val notificationPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { isGranted -> + if (isGranted) { + viewModel.toggleNotifications(requireContext()) + } + } + private val onScrollListener = object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {} @@ -484,8 +498,29 @@ class PodcastFragment : BaseFragment(), Toolbar.OnMenuItemClickListener { } private val onNotificationsClicked: () -> Unit = { - context?.let { - viewModel.toggleNotifications(it) + context?.let { context -> + NotificationPermissionHelper.checkForNotificationPermission( + requireActivity(), + launcher = notificationPermissionLauncher, + onShowRequestPermissionRationale = { + (activity as? FragmentHostListener)?.snackBarView()?.let { snackBarView -> + Snackbar.make(snackBarView, getString(LR.string.notifications_blocked_warning), Snackbar.LENGTH_LONG) + .setAction( + getString(LR.string.notifications_blocked_warning_snackbar_action) + .uppercase(Locale.getDefault()), + ) { + // Open app settings for the user to enable permissions + val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val uri: Uri = Uri.fromParts("package", requireContext().packageName, null) + intent.data = uri + startActivity(intent) + }.show() + } + }, + onPermissionGranted = { viewModel.toggleNotifications(context) }, + onPermissionHandlingNotRequired = { viewModel.toggleNotifications(context) }, + ) } } diff --git a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/NotificationsSettingsFragment.kt b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/NotificationsSettingsFragment.kt index 06e03c4b91f..a4648ad179b 100644 --- a/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/NotificationsSettingsFragment.kt +++ b/modules/features/settings/src/main/java/au/com/shiftyjelly/pocketcasts/settings/NotificationsSettingsFragment.kt @@ -1,10 +1,12 @@ package au.com.shiftyjelly.pocketcasts.settings +import android.content.Intent import android.media.RingtoneManager import android.net.Uri import android.os.Build import android.os.Bundle import android.view.View +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.Toolbar import androidx.core.content.IntentCompat.getParcelableExtra import androidx.core.view.updatePadding @@ -26,7 +28,10 @@ import au.com.shiftyjelly.pocketcasts.preferences.model.NotificationVibrateSetti import au.com.shiftyjelly.pocketcasts.preferences.model.PlayOverNotificationSetting import au.com.shiftyjelly.pocketcasts.repositories.notification.NotificationHelper import au.com.shiftyjelly.pocketcasts.repositories.podcast.PodcastManager +import au.com.shiftyjelly.pocketcasts.ui.helper.FragmentHostListener import au.com.shiftyjelly.pocketcasts.ui.theme.Theme +import au.com.shiftyjelly.pocketcasts.utils.NotificationPermissionHelper +import au.com.shiftyjelly.pocketcasts.utils.NotificationPermissionHelper.hasNotificationPermissionGranted import au.com.shiftyjelly.pocketcasts.views.extensions.findToolbar import au.com.shiftyjelly.pocketcasts.views.extensions.setup import au.com.shiftyjelly.pocketcasts.views.fragments.PodcastSelectFragment @@ -37,7 +42,9 @@ import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.MultiChoiceListener import com.afollestad.materialdialogs.list.listItemsMultiChoice import com.afollestad.materialdialogs.list.updateListItemsMultiChoice +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import java.util.Locale import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -83,6 +90,16 @@ class NotificationsSettingsFragment : private val toolbar get() = view?.findViewById(R.id.toolbar) + private var pendingNotificationValue: Boolean = false + + private val notificationPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { granted -> + if (granted) { + updateNotificationToggle(pendingNotificationValue) + } + } + override val coroutineContext: CoroutineContext get() = Dispatchers.Default @@ -425,31 +442,36 @@ class NotificationsSettingsFragment : private fun setupEnabledNotifications() { launch(Dispatchers.Default) { val enabled = settings.notifyRefreshPodcast.flow.value + val hasPermissionGranted = hasNotificationPermissionGranted(requireContext()) launch(Dispatchers.Main) { - enabledPreference?.isChecked = enabled - enabledPreferences(enabled) + updateNotificationToggle(enabled && hasPermissionGranted) + enabledPreferences(enabled && hasPermissionGranted) enabledPreference?.setOnPreferenceChangeListener { _, newValue -> - val checked = newValue as Boolean - settings.notifyRefreshPodcast.set(checked, updateModifiedAt = true) - - analyticsTracker.track( - AnalyticsEvent.SETTINGS_NOTIFICATIONS_NEW_EPISODES_TOGGLED, - mapOf("enabled" to checked), - ) - - lifecycleScope.launch { - podcastManager.updateAllShowNotifications(checked) - // Don't change the podcasts summary until after the podcasts have been updated - changePodcastsSummary() - } - if (checked) { - settings.setNotificationLastSeenToNow() + pendingNotificationValue = newValue as Boolean + + if (!pendingNotificationValue || hasNotificationPermissionGranted(requireContext())) { + onNotifyNotificationsChange(pendingNotificationValue) + true + } else { + context?.let { + NotificationPermissionHelper.checkForNotificationPermission( + requireActivity(), + launcher = notificationPermissionLauncher, + onShowRequestPermissionRationale = { + showSnackbarPermissionBlocked() + }, + onPermissionGranted = { + updateNotificationToggle(pendingNotificationValue) + }, + onPermissionHandlingNotRequired = { + updateNotificationToggle(pendingNotificationValue) + }, + ) + } + false } - enabledPreferences(checked) - - true } notificationPodcasts?.setOnPreferenceClickListener { @@ -465,6 +487,46 @@ class NotificationsSettingsFragment : } } + private fun showSnackbarPermissionBlocked() { + (activity as? FragmentHostListener)?.snackBarView()?.let { snackBarView -> + Snackbar.make(snackBarView, getString(LR.string.notifications_blocked_warning), Snackbar.LENGTH_LONG) + .setAction( + getString(LR.string.notifications_blocked_warning_snackbar_action) + .uppercase(Locale.getDefault()), + ) { + val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val uri: Uri = Uri.fromParts("package", requireContext().packageName, null) + intent.data = uri + startActivity(intent) + }.show() + } + } + + private fun onNotifyNotificationsChange(newValue: Boolean) { + settings.notifyRefreshPodcast.set(newValue, updateModifiedAt = true) + + analyticsTracker.track( + AnalyticsEvent.SETTINGS_NOTIFICATIONS_NEW_EPISODES_TOGGLED, + mapOf("enabled" to newValue), + ) + + lifecycleScope.launch { + podcastManager.updateAllShowNotifications(newValue) + // Don't change the podcasts summary until after the podcasts have been updated + changePodcastsSummary() + } + if (newValue) { + settings.setNotificationLastSeenToNow() + } + enabledPreferences(newValue) + } + + private fun updateNotificationToggle(newValue: Boolean) { + enabledPreference?.isChecked = newValue + onNotifyNotificationsChange(newValue) + } + private fun setupNotificationVibrate() { vibratePreference?.let { val options = arrayOf( diff --git a/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/NotificationPermissionHelper.kt b/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/NotificationPermissionHelper.kt new file mode 100644 index 00000000000..b890e688abb --- /dev/null +++ b/modules/services/utils/src/main/java/au/com/shiftyjelly/pocketcasts/utils/NotificationPermissionHelper.kt @@ -0,0 +1,51 @@ +package au.com.shiftyjelly.pocketcasts.utils + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import androidx.activity.result.ActivityResultLauncher +import androidx.core.content.ContextCompat + +object NotificationPermissionHelper { + + fun checkForNotificationPermission( + activity: Activity, + launcher: ActivityResultLauncher, + onShowRequestPermissionRationale: () -> Unit = {}, + onPermissionGranted: () -> Unit = {}, + onPermissionHandlingNotRequired: () -> Unit = {}, + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + when { + ContextCompat.checkSelfPermission( + activity, Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED -> { + onPermissionGranted() + } + + activity.shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> { + onShowRequestPermissionRationale() + } + + else -> { + launcher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + } else { + onPermissionHandlingNotRequired() + } + } + + fun hasNotificationPermissionGranted(context: Context): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + } else { + true + } + } +}