From cc819b3ac4c5c0ef1efd523fd7a91c2e38b4a291 Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Tue, 13 Dec 2022 13:31:22 -0500 Subject: [PATCH] Request notification permission on Android 13 --- .../QKSMS/manager/PermissionManagerImpl.kt | 10 +++ .../moez/QKSMS/manager/PermissionManager.kt | 4 +- .../java/com/moez/QKSMS/util/Preferences.kt | 1 + .../java/com/moez/QKSMS/common/Navigator.kt | 8 ++ .../common/util/NotificationManagerImpl.kt | 6 ++ .../moez/QKSMS/feature/main/MainActivity.kt | 23 ++++-- .../com/moez/QKSMS/feature/main/MainState.kt | 3 +- .../moez/QKSMS/feature/main/MainViewModel.kt | 79 ++++++++++++++----- presentation/src/main/res/values/strings.xml | 1 + 9 files changed, 108 insertions(+), 27 deletions(-) diff --git a/data/src/main/java/com/moez/QKSMS/manager/PermissionManagerImpl.kt b/data/src/main/java/com/moez/QKSMS/manager/PermissionManagerImpl.kt index 93620b69..f7baa995 100644 --- a/data/src/main/java/com/moez/QKSMS/manager/PermissionManagerImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/manager/PermissionManagerImpl.kt @@ -19,6 +19,7 @@ package com.moez.QKSMS.manager import android.Manifest +import android.app.NotificationManager import android.app.role.RoleManager import android.content.Context import android.content.pm.PackageManager @@ -29,6 +30,8 @@ import javax.inject.Inject class PermissionManagerImpl @Inject constructor(private val context: Context) : PermissionManager { + private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + override fun isDefaultSms(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { context.getSystemService(RoleManager::class.java)?.isRoleHeld(RoleManager.ROLE_SMS) == true @@ -49,6 +52,13 @@ class PermissionManagerImpl @Inject constructor(private val context: Context) : return hasPermission(Manifest.permission.READ_CONTACTS) } + override fun hasNotifications(): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return true + } + return notificationManager.areNotificationsEnabled() + } + override fun hasPhone(): Boolean { return hasPermission(Manifest.permission.READ_PHONE_STATE) } diff --git a/domain/src/main/java/com/moez/QKSMS/manager/PermissionManager.kt b/domain/src/main/java/com/moez/QKSMS/manager/PermissionManager.kt index 0fd6123a..256fa910 100644 --- a/domain/src/main/java/com/moez/QKSMS/manager/PermissionManager.kt +++ b/domain/src/main/java/com/moez/QKSMS/manager/PermissionManager.kt @@ -28,10 +28,12 @@ interface PermissionManager { fun hasContacts(): Boolean + fun hasNotifications(): Boolean + fun hasPhone(): Boolean fun hasCalling(): Boolean fun hasStorage(): Boolean -} \ No newline at end of file +} diff --git a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt index d7711f3b..aeee576a 100644 --- a/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt +++ b/domain/src/main/java/com/moez/QKSMS/util/Preferences.kt @@ -84,6 +84,7 @@ class Preferences @Inject constructor( val canUseSubId = rxPrefs.getBoolean("canUseSubId", true) val version = rxPrefs.getInteger("version", context.versionCode) val changelogVersion = rxPrefs.getInteger("changelogVersion", context.versionCode) + val hasAskedForNotificationPermission = rxPrefs.getBoolean("hasAskedForNotificationPermission", false) @Deprecated("This should only be accessed when migrating to @blockingManager") val sia = rxPrefs.getBoolean("sia", false) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt index b0659c99..b55b495a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/Navigator.kt @@ -281,6 +281,14 @@ class Navigator @Inject constructor( startActivityExternal(intent) } + fun showPermissions() { + val intent = Intent() + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + intent.data = Uri.fromParts("package", context.packageName, null) + + startActivity(intent) + } + fun showNotificationSettings(threadId: Long = 0) { val intent = Intent(context, NotificationPrefsActivity::class.java) intent.putExtra("threadId", threadId) diff --git a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt index 4e767f1f..dbe9c85a 100644 --- a/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt +++ b/presentation/src/main/java/com/moez/QKSMS/common/util/NotificationManagerImpl.kt @@ -57,6 +57,7 @@ import com.moez.QKSMS.util.GlideApp import com.moez.QKSMS.util.PhoneNumberUtils import com.moez.QKSMS.util.Preferences import com.moez.QKSMS.util.tryOrNull +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -94,6 +95,11 @@ class NotificationManagerImpl @Inject constructor( return } + if (!permissions.hasNotifications()) { + Timber.w("Cannot update notification because we don't have the notification permission") + return + } + val messages = messageRepo.getUnreadUnseenMessages(threadId) // If there are no messages to be displayed, make sure that the notification is dismissed diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt index e84c758c..695f38b3 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainActivity.kt @@ -289,7 +289,7 @@ class MainActivity : QkThemedActivity(), MainView { when (state.syncing) { is SyncRepository.SyncProgress.Idle -> { syncing.isVisible = false - snackbar.isVisible = !state.defaultSms || !state.smsPermission || !state.contactPermission + snackbar.isVisible = !state.defaultSms || !state.smsPermission || !state.contactPermission || !state.notificationPermission } is SyncRepository.SyncProgress.Running -> { @@ -319,6 +319,12 @@ class MainActivity : QkThemedActivity(), MainView { snackbarMessage?.setText(R.string.main_permission_contacts) snackbarButton?.setText(R.string.main_permission_allow) } + + !state.notificationPermission -> { + snackbarTitle?.setText(R.string.main_permission_required) + snackbarMessage?.setText(R.string.main_permission_notifications) + snackbarButton?.setText(R.string.main_permission_allow) + } } } @@ -350,10 +356,17 @@ class MainActivity : QkThemedActivity(), MainView { } override fun requestPermissions() { - ActivityCompat.requestPermissions(this, arrayOf( - Manifest.permission.READ_SMS, - Manifest.permission.SEND_SMS, - Manifest.permission.READ_CONTACTS), 0) + val permissions = mutableListOf( + Manifest.permission.READ_SMS, + Manifest.permission.SEND_SMS, + Manifest.permission.READ_CONTACTS + ) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissions += Manifest.permission.POST_NOTIFICATIONS + } + + ActivityCompat.requestPermissions(this, permissions.toTypedArray(), 0) } override fun clearSearch() { diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt index 01697633..9d66069b 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainState.kt @@ -32,7 +32,8 @@ data class MainState( val syncing: SyncRepository.SyncProgress = SyncRepository.SyncProgress.Idle, val defaultSms: Boolean = true, val smsPermission: Boolean = true, - val contactPermission: Boolean = true + val contactPermission: Boolean = true, + val notificationPermission: Boolean = true, ) sealed class MainPage diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt index a285c432..1fa12198 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/main/MainViewModel.kt @@ -18,6 +18,9 @@ */ package com.moez.QKSMS.feature.main +import android.content.Intent +import android.net.Uri +import android.provider.Settings import androidx.recyclerview.widget.ItemTouchHelper import com.moez.QKSMS.R import com.moez.QKSMS.common.Navigator @@ -135,28 +138,56 @@ class MainViewModel @Inject constructor( !permissionManager.hasReadSms() || !permissionManager.hasContacts() -> view.requestPermissions() } - val permissions = view.activityResumedIntent - .filter { resumed -> resumed } - .observeOn(Schedulers.io()) - .map { Triple(permissionManager.isDefaultSms(), permissionManager.hasReadSms(), permissionManager.hasContacts()) } - .distinctUntilChanged() - .share() + // If the default SMS state changes, reflect it in the State + view.activityResumedIntent + .filter { resumed -> resumed } + .observeOn(Schedulers.io()) + .map { permissionManager.isDefaultSms() } + .distinctUntilChanged() + .doOnNext { defaultSms -> newState { copy(defaultSms = defaultSms) } } + .autoDisposable(view.scope()) + .subscribe() - // If the default SMS state or permission states change, update the ViewState - permissions - .doOnNext { (defaultSms, smsPermission, contactPermission) -> - newState { copy(defaultSms = defaultSms, smsPermission = smsPermission, contactPermission = contactPermission) } - } - .autoDisposable(view.scope()) - .subscribe() + // If the SMS Permission state changes, reflect it in the State + view.activityResumedIntent + .filter { resumed -> resumed } + .observeOn(Schedulers.io()) + .map { permissionManager.hasReadSms() } + .distinctUntilChanged() + .doOnNext { smsPermission -> newState { copy(smsPermission = smsPermission) } } + .autoDisposable(view.scope()) + .subscribe() - // If we go from not having all permissions to having them, sync messages - permissions - .skip(1) - .filter { it.first && it.second && it.third } - .take(1) - .autoDisposable(view.scope()) - .subscribe { syncMessages.execute(Unit) } + // If the Contacts Permission state changes, reflect it in the State + view.activityResumedIntent + .filter { resumed -> resumed } + .observeOn(Schedulers.io()) + .map { permissionManager.hasContacts() } + .distinctUntilChanged() + .doOnNext { contactPermission -> newState { copy(contactPermission = contactPermission) } } + .autoDisposable(view.scope()) + .subscribe() + + // If the Notifications Permission state changes, reflect it in the State + view.activityResumedIntent + .filter { resumed -> resumed } + .observeOn(Schedulers.io()) + .map { permissionManager.hasNotifications() } + .distinctUntilChanged() + .doOnNext { notificationPermission -> newState { copy(notificationPermission = notificationPermission) } } + .autoDisposable(view.scope()) + .subscribe() + + // If we go from not having all SMS permissions to having them, sync messages + view.activityResumedIntent + .filter { resumed -> resumed } + .observeOn(Schedulers.io()) + .map { permissionManager.isDefaultSms() && permissionManager.hasReadSms() && permissionManager.hasContacts() } + .distinctUntilChanged() + .skip(1) + .filter { hasAllPermissions -> hasAllPermissions } + .autoDisposable(view.scope()) + .subscribe { syncMessages.execute(Unit) } // Launch screen from intent view.onNewIntentIntent @@ -448,6 +479,14 @@ class MainViewModel @Inject constructor( !state.defaultSms -> view.requestDefaultSms() !state.smsPermission -> view.requestPermissions() !state.contactPermission -> view.requestPermissions() + !state.notificationPermission -> { + if (prefs.hasAskedForNotificationPermission.get()) { + navigator.showPermissions() + } else { + prefs.hasAskedForNotificationPermission.set(true) + view.requestPermissions() + } + } } } .autoDisposable(view.scope()) diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 2fe3e330..ba998ac8 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -68,6 +68,7 @@ Permission required QKSMS needs permission to send and view SMS messages QKSMS needs permission to view your contacts + QKSMS needs permission to display notifications Allow Inbox