Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 989cb1fd authored by Sunik Kupfer's avatar Sunik Kupfer Committed by Ricki Hirner
Browse files

Fix foreground service start not allowed exception (resolves bitfireAT/davx5#32)



* Make "Battery optimization whitelisting" a global DAVx5 setting
* Make "Keep in foreground" setting dependent of "Battery optimization whitelisting" setting
* if foreground service is enabled, remind user to enable battery optimization whitelisting too, before starting the foreground service

Co-authored-by: default avatarRicki Hirner <hirner@bitfire.at>
parent c9b88b0c
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -77,7 +77,7 @@ class App: Application(), Thread.UncaughtExceptionHandler {
            AccountSettings.repairSyncIntervals(this)

            // foreground service (possible workaround for devices which prevent DAVx5 from being started)
            ForegroundService.startIfEnabled(this)
            ForegroundService.startIfActive(this)
        }
    }

+46 −6
Original line number Diff line number Diff line
@@ -9,7 +9,9 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.AppSettingsActivity
@@ -20,32 +22,69 @@ class ForegroundService : Service() {
    companion object {

        /**
         * Starts/stops a foreground service, according to the app setting [Settings.FOREGROUND_SERVICE].
         * Starts/stops a foreground service, according to the app setting [Settings.FOREGROUND_SERVICE]
         * if [Settings.BATTERY_OPTIMIZATION] is enabled - meaning DAVx5 is whitelisted from optimization.
         */
        const val ACTION_FOREGROUND = "foreground"

        fun isEnabled(context: Context): Boolean {

        /**
         * Whether the app is currently exempted from battery optimization.
         * @return true if battery optimization is not applied to the current app; false if battery optimization is applied
         */
        fun batteryOptimizationWhitelisted(context: Context) =
            if (Build.VERSION.SDK_INT >= 23) {  // battery optimization exists since Android 6 (SDK level 23)
                val powerManager = context.getSystemService(PowerManager::class.java)
                powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)
            } else
                true

        /**
         * Whether the foreground service is enabled (checked) in the app settings.
         * @return true: foreground service enabled; false: foreground service not enabled
         */
        fun foregroundServiceActivated(context: Context): Boolean {
            val settings = SettingsManager.getInstance(context)
            return settings.getBooleanOrNull(Settings.FOREGROUND_SERVICE) == true
        }

        fun startIfEnabled(context: Context) {
            if (isEnabled(context)) {
        /**
         * Starts the foreground service when enabled in the app settings and applicable.
         */
        fun startIfActive(context: Context) {
            if (foregroundServiceActivated(context) && batteryOptimizationWhitelisted(context)) {
                val serviceIntent = Intent(ACTION_FOREGROUND, null, context, ForegroundService::class.java)
                if (Build.VERSION.SDK_INT >= 26)
                    context.startForegroundService(serviceIntent)
                else
                    context.startService(serviceIntent)
            } else
                notifyBatteryOptimization(context)
        }

        private fun notifyBatteryOptimization(context: Context) {
            val settingsIntent = Intent(context, AppSettingsActivity::class.java).apply {
                putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, Settings.BATTERY_OPTIMIZATION)
            }
            val pendingSettingsIntent = PendingIntent.getActivity(context, 0, settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
            val builder =
                NotificationCompat.Builder(context, NotificationUtils.CHANNEL_DEBUG)
                    .setSmallIcon(R.drawable.ic_warning_notify)
                    .setContentTitle(context.getString(R.string.battery_optimization_notify_title))
                    .setContentText(context.getString(R.string.battery_optimization_notify_text))
                    .setContentIntent(pendingSettingsIntent)
                    .setCategory(NotificationCompat.CATEGORY_ERROR)

            val nm = NotificationManagerCompat.from(context)
            nm.notify(NotificationUtils.NOTIFY_BATTERY_OPTIMIZATION, builder.build())
        }
    }


    override fun onBind(intent: Intent?): Nothing? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (isEnabled(this)) {
        if (foregroundServiceActivated(this)) {
            val settingsIntent = Intent(this, AppSettingsActivity::class.java).apply {
                putExtra(AppSettingsActivity.EXTRA_SCROLL_TO, Settings.FOREGROUND_SERVICE)
            }
@@ -55,6 +94,7 @@ class ForegroundService : Service() {
                    .setContentText(getString(R.string.foreground_service_notify_text))
                    .setStyle(NotificationCompat.BigTextStyle())
                    .setContentIntent(PendingIntent.getActivity(this, 0, settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
                    .setCategory(NotificationCompat.CATEGORY_STATUS)
            startForeground(NotificationUtils.NOTIFY_FOREGROUND, builder.build())
            return START_STICKY
        } else {
+1 −0
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ import androidx.appcompat.app.AppCompatDelegate

object Settings {

    const val BATTERY_OPTIMIZATION = "battery_optimization"
    const val FOREGROUND_SERVICE = "foreground_service"

    const val DISTRUST_SYSTEM_CERTIFICATES = "distrust_system_certs"
+27 −1
Original line number Diff line number Diff line
@@ -6,8 +6,11 @@ package at.bitfire.davdroid.ui

import android.content.Intent
import android.graphics.drawable.InsetDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.UiThread
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
@@ -53,6 +56,10 @@ class AppSettingsActivity: AppCompatActivity() {

        val settings by lazy { SettingsManager.getInstance(requireActivity()) }

        val onBatteryOptimizationResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            loadSettings()
        }


        override fun onCreatePreferences(bundle: Bundle?, s: String?) {
            addPreferencesFromResource(R.xml.settings_app)
@@ -101,8 +108,28 @@ class AppSettingsActivity: AppCompatActivity() {
        @UiThread
        private fun loadSettings() {
            // debug settings
            findPreference<SwitchPreferenceCompat>(Settings.BATTERY_OPTIMIZATION)!!.apply {
                // battery optimization exists since Android 6 (API level 23)
                if (Build.VERSION.SDK_INT >= 23) {
                    val powerManager = requireActivity().getSystemService(PowerManager::class.java)
                    val whitelisted = powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)
                    isChecked = whitelisted
                    isEnabled = !whitelisted
                    onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, nowChecked ->
                        if (nowChecked as Boolean)
                            onBatteryOptimizationResult.launch(Intent(
                                android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
                                Uri.parse("package:" + BuildConfig.APPLICATION_ID)
                            ))
                        false
                    }
                } else
                    isVisible = false
            }

            findPreference<SwitchPreferenceCompat>(Settings.FOREGROUND_SERVICE)!!.apply {
                isChecked = settings.getBooleanOrNull(Settings.FOREGROUND_SERVICE) == true
                isEnabled = settings.getBooleanOrNull(Settings.BATTERY_OPTIMIZATION) == true
                onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
                    settings.putBoolean(Settings.FOREGROUND_SERVICE, newValue as Boolean)
                    requireActivity().startService(Intent(ForegroundService.ACTION_FOREGROUND, null, requireActivity(), ForegroundService::class.java))
@@ -209,7 +236,6 @@ class AppSettingsActivity: AppCompatActivity() {
            }
        }


        private fun resetHints() {
            val settings = SettingsManager.getInstance(requireActivity())
            settings.remove(BatteryOptimizationsFragment.Model.HINT_BATTERY_OPTIMIZATIONS)
+1 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ object NotificationUtils {
    const val NOTIFY_REFRESH_COLLECTIONS = 2
    const val NOTIFY_FOREGROUND = 3
    const val NOTIFY_DATABASE_CORRUPTED = 4
    const val NOTIFY_BATTERY_OPTIMIZATION = 5
    const val NOTIFY_SYNC_ERROR = 10
    const val NOTIFY_INVALID_RESOURCE = 11
    const val NOTIFY_WEBDAV_ACCESS = 12
Loading