mirror of
https://github.com/home-assistant/android
synced 2024-10-15 12:32:54 +00:00
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:
parent
80a669da4e
commit
550c18c6e2
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue