Notify user if permission to schedule exact alarms is missing
This commit is contained in:
parent
16e98ed745
commit
73ae228f70
11 changed files with 199 additions and 11 deletions
|
@ -44,9 +44,13 @@ class K9CoreResourceProvider(private val context: Context) : CoreResourceProvide
|
|||
PushNotificationState.LISTENING -> R.string.push_notification_state_listening
|
||||
PushNotificationState.WAIT_BACKGROUND_SYNC -> R.string.push_notification_state_wait_background_sync
|
||||
PushNotificationState.WAIT_NETWORK -> R.string.push_notification_state_wait_network
|
||||
PushNotificationState.ALARM_PERMISSION_MISSING -> R.string.push_notification_state_alarm_permission_missing
|
||||
}
|
||||
return context.getString(resId)
|
||||
}
|
||||
|
||||
override fun pushNotificationInfoText(): String = context.getString(R.string.push_notification_info)
|
||||
|
||||
override fun pushNotificationGrantAlarmPermissionText(): String =
|
||||
context.getString(R.string.push_notification_grant_alarm_permission)
|
||||
}
|
||||
|
|
|
@ -32,4 +32,5 @@ interface CoreResourceProvider {
|
|||
val iconPushNotification: Int
|
||||
fun pushNotificationText(notificationState: PushNotificationState): String
|
||||
fun pushNotificationInfoText(): String
|
||||
fun pushNotificationGrantAlarmPermissionText(): String
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import com.fsck.k9.helper.AlarmManagerCompat
|
||||
|
||||
/**
|
||||
* Checks whether the app can schedule exact alarms.
|
||||
*/
|
||||
internal interface AlarmPermissionManager {
|
||||
/**
|
||||
* Checks whether the app can schedule exact alarms.
|
||||
*
|
||||
* If this method returns `false`, the app has to request the permission to schedule exact alarms. See
|
||||
* [Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM].
|
||||
*/
|
||||
fun canScheduleExactAlarms(): Boolean
|
||||
|
||||
/**
|
||||
* Register a listener to be notified when the app was granted the permission to schedule exact alarms.
|
||||
*/
|
||||
fun registerListener(listener: AlarmPermissionListener)
|
||||
|
||||
/**
|
||||
* Unregister the listener registered via [registerListener].
|
||||
*/
|
||||
fun unregisterListener()
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create an Android API-specific instance of [AlarmPermissionManager].
|
||||
*/
|
||||
internal fun AlarmPermissionManager(context: Context, alarmManagerCompat: AlarmManagerCompat): AlarmPermissionManager {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
AlarmPermissionManagerApi31(context, alarmManagerCompat)
|
||||
} else {
|
||||
AlarmPermissionManagerApi21()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener that can be notified when the app was granted the permission to schedule exact alarms.
|
||||
*
|
||||
* Note: Currently Android stops (and potentially restarts) the app when the permission is revoked. So there's no
|
||||
* callback mechanism for the permission revocation case.
|
||||
*/
|
||||
internal fun interface AlarmPermissionListener {
|
||||
fun onAlarmPermissionGranted()
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
/**
|
||||
* On Android versions prior to 12 there's no permission to limit an app's ability to schedule exact alarms.
|
||||
*/
|
||||
internal class AlarmPermissionManagerApi21 : AlarmPermissionManager {
|
||||
override fun canScheduleExactAlarms(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun registerListener(listener: AlarmPermissionListener) = Unit
|
||||
|
||||
override fun unregisterListener() = Unit
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package com.fsck.k9.controller.push
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.fsck.k9.helper.AlarmManagerCompat
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Starting with Android 12 we have to check whether the app can schedule exact alarms.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
internal class AlarmPermissionManagerApi31(
|
||||
private val context: Context,
|
||||
private val alarmManagerCompat: AlarmManagerCompat,
|
||||
) : AlarmPermissionManager {
|
||||
private var isRegistered = false
|
||||
private var listener: AlarmPermissionListener? = null
|
||||
|
||||
private val intentFilter = IntentFilter().apply {
|
||||
addAction(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED)
|
||||
}
|
||||
|
||||
private val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
val listener = synchronized(this@AlarmPermissionManagerApi31) { listener }
|
||||
listener?.onAlarmPermissionGranted()
|
||||
}
|
||||
}
|
||||
|
||||
override fun canScheduleExactAlarms(): Boolean {
|
||||
return alarmManagerCompat.canScheduleExactAlarms()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun registerListener(listener: AlarmPermissionListener) {
|
||||
if (!isRegistered) {
|
||||
Timber.v("Registering alarm permission listener")
|
||||
isRegistered = true
|
||||
this.listener = listener
|
||||
ContextCompat.registerReceiver(context, receiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun unregisterListener() {
|
||||
if (isRegistered) {
|
||||
Timber.v("Unregistering alarm permission listener")
|
||||
isRegistered = false
|
||||
listener = null
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,9 +22,12 @@ internal val controllerPushModule = module {
|
|||
pushServiceManager = get(),
|
||||
bootCompleteManager = get(),
|
||||
autoSyncManager = get(),
|
||||
alarmPermissionManager = get(),
|
||||
pushNotificationManager = get(),
|
||||
connectivityManager = get(),
|
||||
accountPushControllerFactory = get(),
|
||||
)
|
||||
}
|
||||
|
||||
single<AlarmPermissionManager> { AlarmPermissionManager(context = get(), alarmManagerCompat = get()) }
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.fsck.k9.network.ConnectivityChangeListener
|
|||
import com.fsck.k9.network.ConnectivityManager
|
||||
import com.fsck.k9.notification.PushNotificationManager
|
||||
import com.fsck.k9.notification.PushNotificationState
|
||||
import com.fsck.k9.notification.PushNotificationState.ALARM_PERMISSION_MISSING
|
||||
import com.fsck.k9.notification.PushNotificationState.LISTENING
|
||||
import com.fsck.k9.notification.PushNotificationState.WAIT_BACKGROUND_SYNC
|
||||
import com.fsck.k9.notification.PushNotificationState.WAIT_NETWORK
|
||||
|
@ -28,6 +29,7 @@ import timber.log.Timber
|
|||
/**
|
||||
* Starts and stops [AccountPushController]s as necessary. Manages the Push foreground service.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class PushController internal constructor(
|
||||
private val accountManager: AccountManager,
|
||||
private val generalSettingsManager: GeneralSettingsManager,
|
||||
|
@ -35,6 +37,7 @@ class PushController internal constructor(
|
|||
private val pushServiceManager: PushServiceManager,
|
||||
private val bootCompleteManager: BootCompleteManager,
|
||||
private val autoSyncManager: AutoSyncManager,
|
||||
private val alarmPermissionManager: AlarmPermissionManager,
|
||||
private val pushNotificationManager: PushNotificationManager,
|
||||
private val connectivityManager: ConnectivityManager,
|
||||
private val accountPushControllerFactory: AccountPushControllerFactory,
|
||||
|
@ -50,6 +53,7 @@ class PushController internal constructor(
|
|||
override fun onConnectivityChanged() = this@PushController.onConnectivityChanged()
|
||||
override fun onConnectivityLost() = this@PushController.onConnectivityLost()
|
||||
}
|
||||
private val alarmPermissionListener = AlarmPermissionListener(::onAlarmPermissionGranted)
|
||||
|
||||
/**
|
||||
* Initialize [PushController].
|
||||
|
@ -109,6 +113,10 @@ class PushController internal constructor(
|
|||
launchUpdatePushers()
|
||||
}
|
||||
|
||||
private fun onAlarmPermissionGranted() {
|
||||
launchUpdatePushers()
|
||||
}
|
||||
|
||||
private fun onConnectivityChanged() {
|
||||
coroutineScope.launch(coroutineDispatcher) {
|
||||
synchronized(lock) {
|
||||
|
@ -142,17 +150,24 @@ class PushController internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun updatePushers() {
|
||||
Timber.v("PushController.updatePushers()")
|
||||
|
||||
val generalSettings = generalSettingsManager.getSettings()
|
||||
|
||||
val alarmPermissionMissing = !alarmPermissionManager.canScheduleExactAlarms()
|
||||
val backgroundSyncDisabledViaSystem = autoSyncManager.isAutoSyncDisabled
|
||||
val backgroundSyncDisabledInApp = generalSettings.backgroundSync == BackgroundSync.NEVER
|
||||
val networkNotAvailable = !connectivityManager.isNetworkAvailable()
|
||||
val realPushAccounts = getPushAccounts()
|
||||
|
||||
val pushAccounts = if (backgroundSyncDisabledViaSystem || backgroundSyncDisabledInApp || networkNotAvailable) {
|
||||
val shouldDisablePushAccounts = backgroundSyncDisabledViaSystem ||
|
||||
backgroundSyncDisabledInApp ||
|
||||
networkNotAvailable ||
|
||||
alarmPermissionMissing
|
||||
|
||||
val pushAccounts = if (shouldDisablePushAccounts) {
|
||||
emptyList()
|
||||
} else {
|
||||
realPushAccounts
|
||||
|
@ -202,6 +217,11 @@ class PushController internal constructor(
|
|||
startServices()
|
||||
}
|
||||
|
||||
alarmPermissionMissing -> {
|
||||
setPushNotificationState(ALARM_PERMISSION_MISSING)
|
||||
startServices()
|
||||
}
|
||||
|
||||
arePushersActive -> {
|
||||
setPushNotificationState(LISTENING)
|
||||
startServices()
|
||||
|
@ -228,6 +248,7 @@ class PushController internal constructor(
|
|||
bootCompleteManager.enableReceiver()
|
||||
registerAutoSyncListener()
|
||||
registerConnectivityChangeListener()
|
||||
registerAlarmPermissionListener()
|
||||
connectivityManager.start()
|
||||
}
|
||||
|
||||
|
@ -236,6 +257,7 @@ class PushController internal constructor(
|
|||
bootCompleteManager.disableReceiver()
|
||||
autoSyncManager.unregisterListener()
|
||||
unregisterConnectivityChangeListener()
|
||||
alarmPermissionManager.unregisterListener()
|
||||
connectivityManager.stop()
|
||||
}
|
||||
|
||||
|
@ -254,4 +276,10 @@ class PushController internal constructor(
|
|||
private fun unregisterConnectivityChangeListener() {
|
||||
connectivityManager.removeListener(connectivityChangeListener)
|
||||
}
|
||||
|
||||
private fun registerAlarmPermissionListener() {
|
||||
if (!alarmPermissionManager.canScheduleExactAlarms()) {
|
||||
alarmPermissionManager.registerListener(alarmPermissionListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ import android.app.Notification
|
|||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.fsck.k9.CoreResourceProvider
|
||||
|
@ -49,26 +52,47 @@ internal class PushNotificationManager(
|
|||
}
|
||||
|
||||
private fun createNotification(): Notification {
|
||||
val intent = Intent(PUSH_INFO_ACTION).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
setPackage(context.packageName)
|
||||
}
|
||||
val contentIntent = PendingIntent.getActivity(context, 1, intent, FLAG_IMMUTABLE)
|
||||
|
||||
return NotificationCompat.Builder(context, notificationChannelManager.pushChannelId)
|
||||
.setSmallIcon(resourceProvider.iconPushNotification)
|
||||
.setContentTitle(resourceProvider.pushNotificationText(notificationState))
|
||||
.setContentText(resourceProvider.pushNotificationInfoText())
|
||||
.setContentIntent(contentIntent)
|
||||
.setContentText(getContentText())
|
||||
.setContentIntent(getContentIntent())
|
||||
.setOngoing(true)
|
||||
.setNotificationSilent()
|
||||
.setSilent(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
|
||||
.setLocalOnly(true)
|
||||
.setShowWhen(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getContentIntent(): PendingIntent {
|
||||
val intent = if (notificationState == PushNotificationState.ALARM_PERMISSION_MISSING) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
error("ACTION_REQUEST_SCHEDULE_EXACT_ALARM is only available on API 31+")
|
||||
}
|
||||
|
||||
Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
|
||||
data = Uri.parse("package:${context.packageName}")
|
||||
}
|
||||
} else {
|
||||
Intent(PUSH_INFO_ACTION).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
setPackage(context.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(context, 1, intent, FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
private fun getContentText(): String {
|
||||
return if (notificationState == PushNotificationState.ALARM_PERMISSION_MISSING) {
|
||||
resourceProvider.pushNotificationGrantAlarmPermissionText()
|
||||
} else {
|
||||
resourceProvider.pushNotificationInfoText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class PushNotificationState {
|
||||
|
@ -76,4 +100,5 @@ enum class PushNotificationState {
|
|||
LISTENING,
|
||||
WAIT_BACKGROUND_SYNC,
|
||||
WAIT_NETWORK,
|
||||
ALARM_PERMISSION_MISSING,
|
||||
}
|
||||
|
|
|
@ -39,4 +39,5 @@ class TestCoreResourceProvider : CoreResourceProvider {
|
|||
}
|
||||
|
||||
override fun pushNotificationInfoText(): String = throw UnsupportedOperationException("not implemented")
|
||||
override fun pushNotificationGrantAlarmPermissionText() = throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
|
|
@ -1098,7 +1098,9 @@ You can keep this message and use it as a backup for your secret key. If you wan
|
|||
<string name="push_notification_state_listening">Waiting for new emails</string>
|
||||
<string name="push_notification_state_wait_background_sync">Sleeping until background sync is allowed</string>
|
||||
<string name="push_notification_state_wait_network">Sleeping until network is available</string>
|
||||
<string name="push_notification_state_alarm_permission_missing">Missing permission to schedule alarms</string>
|
||||
<string name="push_notification_info">Tap to learn more.</string>
|
||||
<string name="push_notification_grant_alarm_permission">Tap to grant permission.</string>
|
||||
|
||||
<string name="push_info_title">Push Info</string>
|
||||
<string name="push_info_notification_explanation_text">When using Push, K-9 Mail maintains a connection to the mail server. Android requires displaying an ongoing notification while the app is active in the background. %s</string>
|
||||
|
|
|
@ -39,4 +39,5 @@ class TestCoreResourceProvider : CoreResourceProvider {
|
|||
}
|
||||
|
||||
override fun pushNotificationInfoText(): String = throw UnsupportedOperationException("not implemented")
|
||||
override fun pushNotificationGrantAlarmPermissionText() = throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue