Handle foreground service restrictions for persistent connection (#2790)

* Handle foreground service restrictions for persistent connection

 - When trying to start the WebsocketManager as a foreground service, catch exceptions due to restrictions and show the user a notification to fix it

* Update logging in WebsocketManager
This commit is contained in:
Joris Pelgröm 2022-08-14 02:49:55 +02:00 committed by GitHub
parent 80a669da4e
commit 550c18c6e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 6 deletions

View file

@ -2,13 +2,21 @@ package io.homeassistant.companion.android.settings.websocket
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
import androidx.core.content.getSystemService
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.google.android.material.composethemeadapter.MdcTheme
@ -23,6 +31,12 @@ class WebsocketSettingFragment : Fragment() {
val viewModel: SettingViewModel by viewModels()
private var isIgnoringBatteryOptimizations by mutableStateOf(false)
private val requestBackgroundAccessResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
setIgnoringBatteryOptimizations()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
@ -49,7 +63,18 @@ class WebsocketSettingFragment : Fragment() {
.collectAsState(initial = viewModel.getSetting(0))
WebsocketSettingView(
websocketSetting = settings.value.websocketSetting,
onSettingChanged = { viewModel.updateWebsocketSetting(0, it) }
unrestrictedBackgroundAccess = isIgnoringBatteryOptimizations,
onSettingChanged = { viewModel.updateWebsocketSetting(0, it) },
onBackgroundAccessTapped = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestBackgroundAccessResult.launch(
Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:${activity?.packageName}")
)
)
}
}
)
}
}
@ -59,5 +84,13 @@ class WebsocketSettingFragment : Fragment() {
override fun onResume() {
super.onResume()
activity?.title = getString(commonR.string.websocket_setting_name)
setIgnoringBatteryOptimizations()
}
private fun setIgnoringBatteryOptimizations() {
isIgnoringBatteryOptimizations = Build.VERSION.SDK_INT <= Build.VERSION_CODES.M ||
context?.getSystemService<PowerManager>()
?.isIgnoringBatteryOptimizations(requireActivity().packageName)
?: false
}
}

View file

@ -5,6 +5,8 @@ import android.content.res.Configuration
import android.os.Build
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -20,13 +22,16 @@ import io.homeassistant.companion.android.BuildConfig
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.util.websocketChannel
import io.homeassistant.companion.android.database.settings.WebsocketSetting
import io.homeassistant.companion.android.util.compose.HaAlertWarning
import io.homeassistant.companion.android.util.compose.InfoNotification
import io.homeassistant.companion.android.util.compose.RadioButtonRow
@Composable
fun WebsocketSettingView(
websocketSetting: WebsocketSetting,
onSettingChanged: (WebsocketSetting) -> Unit
unrestrictedBackgroundAccess: Boolean,
onSettingChanged: (WebsocketSetting) -> Unit,
onBackgroundAccessTapped: () -> Unit
) {
val scrollState = rememberScrollState()
val context = LocalContext.current
@ -36,6 +41,14 @@ fun WebsocketSettingView(
text = stringResource(R.string.websocket_setting_description),
modifier = Modifier.padding(bottom = 16.dp)
)
if (!unrestrictedBackgroundAccess && websocketSetting != WebsocketSetting.NEVER) {
HaAlertWarning(
message = stringResource(R.string.websocket_notification_backgroundaccess),
action = stringResource(R.string.allow),
onActionClicked = onBackgroundAccessTapped
)
Spacer(modifier = Modifier.height(16.dp))
}
Divider()
RadioButtonRow(
text = stringResource(if (BuildConfig.FLAVOR == "full") R.string.websocket_setting_never else R.string.websocket_setting_never_minimal),

View file

@ -0,0 +1,46 @@
package io.homeassistant.companion.android.util.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun HaAlertWarning(
message: String,
action: String?,
onActionClicked: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(colorResource(commonR.color.colorAlertWarning), MaterialTheme.shapes.medium)
.padding(all = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = message,
modifier = Modifier
.weight(1f)
.padding(end = if (action != null) 8.dp else 0.dp)
)
if (action != null) {
TextButton(
colors = ButtonDefaults.textButtonColors(contentColor = colorResource(commonR.color.colorOnAlertWarning)),
onClick = onActionClicked
) {
Text(action)
}
}
}
}

View file

@ -27,6 +27,7 @@ import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.data.url.UrlRepository
import io.homeassistant.companion.android.common.data.websocket.WebSocketRepository
import io.homeassistant.companion.android.common.util.websocketChannel
import io.homeassistant.companion.android.common.util.websocketIssuesChannel
import io.homeassistant.companion.android.database.settings.SettingsDao
import io.homeassistant.companion.android.database.settings.WebsocketSetting
import io.homeassistant.companion.android.notifications.MessagingManager
@ -37,6 +38,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.IllegalStateException
import java.util.concurrent.TimeUnit
class WebsocketManager(
@ -48,6 +50,7 @@ class WebsocketManager(
private const val TAG = "WebSockManager"
private const val SOURCE = "Websocket"
private const val NOTIFICATION_ID = 65423
private const val NOTIFICATION_RESTRICTED_ID = 65424
private val DEFAULT_WEBSOCKET_SETTING = if (BuildConfig.FLAVOR == "full") WebsocketSetting.NEVER else WebsocketSetting.ALWAYS
fun start(context: Context) {
@ -98,10 +101,12 @@ class WebsocketManager(
return@withContext Result.success()
}
Log.d(TAG, "Starting to listen to Websocket")
createNotification()
if (!createNotification()) {
return@withContext Result.success()
}
// Start listening for notifications
Log.d(TAG, "Starting to listen to Websocket")
val job = launch { collectNotifications() }
// play ping pong to ensure we have a connection.
@ -163,7 +168,12 @@ class WebsocketManager(
}
}
private suspend fun createNotification() {
/**
* Create a notification to start the service as a foreground service.
*
* @return `true` if the foreground service was started
*/
private suspend fun createNotification(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
var notificationChannel =
notificationManager.getNotificationChannel(websocketChannel)
@ -210,6 +220,31 @@ class WebsocketManager(
settingPendingIntent
)
.build()
setForeground(ForegroundInfo(NOTIFICATION_ID, notification))
return try {
setForeground(ForegroundInfo(NOTIFICATION_ID, notification))
true
} catch (e: IllegalStateException) {
Log.e(TAG, "Unable to setForeground due to restrictions", e)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (notificationManager.getNotificationChannel(websocketIssuesChannel) == null) {
val restrictedNotificationChannel = NotificationChannel(
websocketIssuesChannel,
applicationContext.getString(R.string.websocket_notification_issues),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(restrictedNotificationChannel)
}
}
val restrictedNotification = NotificationCompat.Builder(applicationContext, websocketIssuesChannel)
.setSmallIcon(R.drawable.ic_stat_ic_notification)
.setContentTitle(applicationContext.getString(R.string.websocket_restricted_title))
.setContentText(applicationContext.getString(R.string.websocket_restricted_fix))
.setContentIntent(settingPendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
.build()
notificationManager.notify(NOTIFICATION_RESTRICTED_ID, restrictedNotification)
false
}
}
}

View file

@ -3,6 +3,7 @@ package io.homeassistant.companion.android.common.util
const val sensorWorkerChannel = "Sensor Worker"
const val sensorCoreSyncChannel = "Sensor Sync"
const val websocketChannel = "Websocket"
const val websocketIssuesChannel = "Websocket Issues"
const val highAccuracyChannel = "High accuracy location"
const val databaseChannel = "App Database"
const val locationDisabledChannel = "Location disabled"
@ -13,6 +14,7 @@ val appCreatedChannels = listOf(
sensorWorkerChannel,
sensorCoreSyncChannel,
websocketChannel,
websocketIssuesChannel,
highAccuracyChannel,
databaseChannel,
locationDisabledChannel,

View file

@ -25,4 +25,6 @@
<color name="colorActionBarPopupBackground">@android:color/white</color>
<color name="colorLaunchScreenBackground">#fafafa</color>
<color name="colorCodeBackground">#f5f5f5</color>
<color name="colorAlertWarning">#1fffa600</color>
<color name="colorOnAlertWarning">#ffa600</color>
</resources>

View file

@ -9,6 +9,7 @@
<string name="add_ssid_name_suggestion">Add %1$s</string>
<string name="add_widget">Add widget</string>
<string name="all_entities">All entities</string>
<string name="allow">Allow</string>
<string name="app_name">Home Assistant</string>
<string name="app_version_info">App Version Info</string>
<string name="application_version">Application Version</string>
@ -748,6 +749,8 @@
<string name="wear_set_favorites">Select your favorite entities to appear at the top of the Wear home screen. You can also drag and drop to change the order in which they appear.</string>
<string name="wear_settings">Wear Device Settings</string>
<string name="websocket_listening">Connected to Home Assistant</string>
<string name="websocket_restricted_title">Unable to open connection to Home Assistant</string>
<string name="websocket_restricted_fix">Update settings to fix or disable</string>
<string name="webview_error_AUTH_SCHEME">Unsupported authentication scheme (not basic or digest), please check network settings.</string>
<string name="webview_error_AUTHENTICATION">User authentication failed on server, please check server settings.</string>
<string name="webview_error_description">Encountered error :</string>
@ -829,7 +832,9 @@
<string name="websocket_setting_always">Always</string>
<string name="websocket_setting_always_minimal">Always\n\nRequired to always receive notifications</string>
<string name="websocket_persistent_notification">In order to maintain the persistent connection the app will need to create a persistent notification. You may use the button below to manage the appearance of this notification. It is recommended to minimize the notification to hide the icon.</string>
<string name="websocket_notification_backgroundaccess">To reliably connect your server, allow Home Assistant to keep running in the background.</string>
<string name="websocket_notification_channel">Manage Persistent Connection Notification</string>
<string name="websocket_notification_issues">Persistent Connection Issues</string>
<string name="notification_channels">Notification Channels</string>
<string name="notification_channels_summary">Manage all notification channels configured on the device. Channels control the behavior of its notifications including visibility and sound.</string>
<string name="info">Information</string>