Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BaseWidgetProvider Refactor #4640

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
10 changes: 5 additions & 5 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="io.homeassistant.companion.android.widgets.ButtonWidget.CALL_SERVICE" />
<action android:name="io.homeassistant.companion.android.widgets.ButtonWidget.CALL_SERVICE_AUTH" />
<action android:name="io.homeassistant.companion.android.widgets.ButtonWidget.RECEIVE_DATA" />
<action android:name="io.homeassistant.companion.android.widgets.RECEIVE_DATA" />
</intent-filter>

<meta-data
Expand All @@ -124,7 +124,7 @@
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="io.homeassistant.companion.android.widgets.camera.CameraWidget.RECEIVE_DATA" />
<action android:name="io.homeassistant.companion.android.widgets.RECEIVE_DATA" />
<action android:name="io.homeassistant.companion.android.widgets.camera.CameraWidget.UPDATE_IMAGE" />
</intent-filter>

Expand All @@ -137,7 +137,7 @@
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="io.homeassistant.companion.android.widgets.StaticWidget.RECEIVE_DATA" />
<action android:name="io.homeassistant.companion.android.widgets.RECEIVE_DATA" />
</intent-filter>

<meta-data
Expand All @@ -149,7 +149,7 @@
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.RECEIVE_DATA" />
<action android:name="io.homeassistant.companion.android.widgets.RECEIVE_DATA" />
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.UPDATE_MEDIA_IMAGE" />
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_PREV_TRACK" />
<action android:name="io.homeassistant.companion.android.widgets.media_player_controls.MediaPlayerControlsWidget.CALL_REWIND" />
Expand All @@ -174,7 +174,7 @@
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
<action android:name="io.homeassistant.companion.android.background.REQUEST_SENSORS_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="io.homeassistant.companion.android.widgets.template.TemplateWidget.RECEIVE_DATA" />
<action android:name="io.homeassistant.companion.android.widgets.RECEIVE_DATA" />
</intent-filter>

<meta-data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.homeassistant.companion.android.settings.language.LanguagesManager
import io.homeassistant.companion.android.util.LifecycleHandler
import io.homeassistant.companion.android.websocket.WebsocketBroadcastReceiver
import io.homeassistant.companion.android.widgets.button.ButtonWidget
import io.homeassistant.companion.android.widgets.camera.CameraWidget
import io.homeassistant.companion.android.widgets.entity.EntityWidget
import io.homeassistant.companion.android.widgets.mediaplayer.MediaPlayerControlsWidget
import io.homeassistant.companion.android.widgets.template.TemplateWidget
Expand Down Expand Up @@ -277,6 +278,7 @@ open class HomeAssistantApplication : Application() {
// Update widgets when the screen turns on, updates are skipped if widgets were not added
val buttonWidget = ButtonWidget()
val entityWidget = EntityWidget()
val cameraWidget = CameraWidget()
val mediaPlayerWidget = MediaPlayerControlsWidget()
val templateWidget = TemplateWidget()

Expand All @@ -286,6 +288,7 @@ open class HomeAssistantApplication : Application() {

ContextCompat.registerReceiver(this, buttonWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.registerReceiver(this, entityWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.registerReceiver(this, cameraWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ivorsmorenburg marked this conversation as resolved.
Show resolved Hide resolved
ContextCompat.registerReceiver(this, mediaPlayerWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.registerReceiver(this, templateWidget, screenIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ import android.widget.Spinner
import android.widget.Toast
import io.homeassistant.companion.android.BaseActivity
import io.homeassistant.companion.android.common.R
import io.homeassistant.companion.android.common.data.repositories.BaseDaoWidgetRepository
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.database.widget.WidgetDao
import javax.inject.Inject

abstract class BaseWidgetConfigureActivity : BaseActivity() {
abstract class BaseWidgetConfigureActivity<T : BaseDaoWidgetRepository<*>> : BaseActivity() {

@Inject
lateinit var serverManager: ServerManager

protected var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID

abstract val dao: WidgetDao
@Inject
lateinit var repository: T

abstract val serverSelect: View
abstract val serverSelectList: Spinner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,51 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.widget.RemoteViews
import androidx.core.content.ContextCompat
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.repositories.BaseDaoWidgetRepository
import io.homeassistant.companion.android.common.data.servers.ServerManager
import io.homeassistant.companion.android.database.widget.ThemeableWidgetEntity
import io.homeassistant.companion.android.database.widget.WidgetBackgroundType
import io.homeassistant.companion.android.util.hasActiveConnection
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

/**
* A widget provider class for widgets that update based on entity state changes.
*/
abstract class BaseWidgetProvider : AppWidgetProvider() {
abstract class BaseWidgetProvider<T : BaseDaoWidgetRepository<*>, WidgetDataType> : AppWidgetProvider() {

companion object {
const val UPDATE_VIEW =
"io.homeassistant.companion.android.widgets.template.BaseWidgetProvider.UPDATE_VIEW"
"io.homeassistant.companion.android.widgets.UPDATE_VIEW"
const val RECEIVE_DATA =
"io.homeassistant.companion.android.widgets.template.TemplateWidget.RECEIVE_DATA"
"io.homeassistant.companion.android.widgets.RECEIVE_DATA"

var widgetScope: CoroutineScope? = null
var widgetWorkScope: CoroutineScope? = null
val widgetEntities = mutableMapOf<Int, List<String>>()
val widgetJobs = mutableMapOf<Int, Job>()
}

@Inject
lateinit var serverManager: ServerManager

private var thisSetScope = false
@Inject
lateinit var repository: T

protected var thisSetScope = false
protected var lastIntent = ""

init {
Expand All @@ -49,51 +60,62 @@ abstract class BaseWidgetProvider : AppWidgetProvider() {
private fun setupWidgetScope() {
if (widgetScope == null || !widgetScope!!.isActive) {
widgetScope = CoroutineScope(Dispatchers.Main + Job())
widgetWorkScope = CoroutineScope(Dispatchers.IO + Job())
thisSetScope = true
}
}

override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
private suspend fun updateAllWidgets(
context: Context
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
widgetScope?.launch {
val views = getWidgetRemoteViews(context, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
val widgetProvider = getWidgetProvider(context)
val systemWidgetIds = AppWidgetManager.getInstance(context)
.getAppWidgetIds(widgetProvider)
.toSet()
val dbWidgetIds = getAllWidgetIdsWithEntities().keys

val invalidWidgetIds = dbWidgetIds.minus(systemWidgetIds)
if (invalidWidgetIds.isNotEmpty()) {
Log.i(
getWidgetProvider(context).shortClassName,
"Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted"
)
onDeleted(context, invalidWidgetIds.toIntArray())
}
}

override fun onReceive(context: Context, intent: Intent) {
lastIntent = intent.action.toString()
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
dbWidgetIds.filter { systemWidgetIds.contains(it) }.forEach {
forceUpdateView(context, it)
}
}

super.onReceive(context, intent)
when (lastIntent) {
UPDATE_VIEW -> updateView(context, appWidgetId)
RECEIVE_DATA -> {
saveEntityConfiguration(
context,
intent.extras,
appWidgetId
)
onScreenOn(context)
}
Intent.ACTION_SCREEN_ON -> onScreenOn(context)
Intent.ACTION_SCREEN_OFF -> onScreenOff()
fun forceUpdateView(
context: Context,
appWidgetId: Int,
appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context)
) {
widgetScope?.launch {
val hasActiveConnection = context.hasActiveConnection()
val views = getWidgetRemoteViews(context, appWidgetId, hasActiveConnection)
Log.d(getWidgetProvider(context).shortClassName, "Updating Widget View updateAppWidget() hasActiveConnection: $hasActiveConnection")
appWidgetManager.updateAppWidget(appWidgetId, views)
onWidgetsViewUpdated(context, appWidgetId, appWidgetManager, views, hasActiveConnection)
}
}

fun onScreenOn(context: Context) {
private suspend fun getAllWidgetIdsWithEntities(): Map<Int, Pair<Int, List<String>>> =
repository.getAllFlow()
.first()
.associate {
it.id to (it.serverId to listOf(it.entityId.orEmpty()))
}

open fun onScreenOn(context: Context) {
setupWidgetScope()
if (!serverManager.isRegistered()) return
widgetScope!!.launch {
updateAllWidgets(context)

val allWidgets = getAllWidgetIdsWithEntities(context)
val allWidgets = getAllWidgetIdsWithEntities()
val widgetsWithDifferentEntities = allWidgets.filter { it.value.second != widgetEntities[it.key] }
if (widgetsWithDifferentEntities.isNotEmpty()) {
ContextCompat.registerReceiver(
Expand All @@ -109,7 +131,7 @@ abstract class BaseWidgetProvider : AppWidgetProvider() {
val (serverId, entities) = pair.first to pair.second
val entityUpdates =
if (serverManager.getServer(serverId) != null) {
serverManager.integrationRepository(serverId).getEntityUpdates(entities)
getUpdates(serverId, entities)
} else {
null
}
Expand All @@ -129,60 +151,100 @@ abstract class BaseWidgetProvider : AppWidgetProvider() {
}
}

abstract suspend fun getUpdates(serverId: Int, entityIds: List<String>): Flow<WidgetDataType>?

private fun onScreenOff() {
if (thisSetScope) {
widgetWorkScope?.cancel()
widgetScope?.cancel()
thisSetScope = false
widgetEntities.clear()
widgetJobs.clear()
}
}

private suspend fun updateAllWidgets(
context: Context
) {
val widgetProvider = getWidgetProvider(context)
val systemWidgetIds = AppWidgetManager.getInstance(context)
.getAppWidgetIds(widgetProvider)
.toSet()
val dbWidgetIds = getAllWidgetIdsWithEntities(context).keys
fun setWidgetBackground(views: RemoteViews, layoutId: Int, widget: ThemeableWidgetEntity?) {
when (widget?.backgroundType) {
WidgetBackgroundType.TRANSPARENT -> {
views.setInt(layoutId, "setBackgroundColor", Color.TRANSPARENT)
}

val invalidWidgetIds = dbWidgetIds.minus(systemWidgetIds)
if (invalidWidgetIds.isNotEmpty()) {
Log.i(
widgetProvider.shortClassName,
"Found widgets $invalidWidgetIds in database, but not in AppWidgetManager - sending onDeleted"
)
onDeleted(context, invalidWidgetIds.toIntArray())
else -> {
views.setInt(layoutId, "setBackgroundResource", R.drawable.widget_button_background)
}
}
}

dbWidgetIds.filter { systemWidgetIds.contains(it) }.forEach {
updateView(context, it)
protected fun removeSubscription(appWidgetId: Int) {
widgetEntities.remove(appWidgetId)
widgetJobs[appWidgetId]?.cancel()
widgetJobs.remove(appWidgetId)
}

override fun onDeleted(context: Context, appWidgetIds: IntArray) {
widgetScope?.launch {
repository.deleteAll(appWidgetIds)
appWidgetIds.forEach { removeSubscription(it) }
}
}

private fun updateView(
override fun onUpdate(
context: Context,
appWidgetId: Int,
appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context)
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
widgetScope?.launch {
val views = getWidgetRemoteViews(context, appWidgetId)
appWidgetManager.updateAppWidget(appWidgetId, views)
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
widgetScope?.launch {
forceUpdateView(context, appWidgetId, appWidgetManager)
}
}
}

protected fun removeSubscription(appWidgetId: Int) {
widgetEntities.remove(appWidgetId)
widgetJobs[appWidgetId]?.cancel()
widgetJobs.remove(appWidgetId)
override fun onReceive(context: Context, intent: Intent) {
lastIntent = intent.action.toString()
val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
Log.d(
getWidgetProvider(context).shortClassName,
"Broadcast received: " + System.lineSeparator() +
"Broadcast action: " + lastIntent + System.lineSeparator() +
"AppWidgetId: " + appWidgetId
)

super.onReceive(context, intent)
when (lastIntent) {
UPDATE_VIEW -> forceUpdateView(context, appWidgetId)
RECEIVE_DATA -> {
saveEntityConfiguration(
context,
intent.extras,
appWidgetId
)
onScreenOn(context)
}

Intent.ACTION_SCREEN_ON -> onScreenOn(context)
Intent.ACTION_SCREEN_OFF -> onScreenOff()
}
}

open fun onWidgetsViewUpdated(context: Context, appWidgetId: Int, appWidgetManager: AppWidgetManager, remoteViews: RemoteViews, hasActiveConnection: Boolean) {
Log.d(
getWidgetProvider(context).shortClassName,
"onWidgetsViewUpdated() received, AppWidgetId: $appWidgetId hasActiveConnection: $hasActiveConnection"
)
}

open suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: WidgetDataType) {
Log.d(
getWidgetProvider(context).shortClassName,
"onEntityStateChanged(), AppWidgetId: $appWidgetId"
)
}

abstract fun getWidgetProvider(context: Context): ComponentName
abstract suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, suggestedEntity: Entity<Map<String, Any>>? = null): RemoteViews
abstract suspend fun getWidgetRemoteViews(context: Context, appWidgetId: Int, hasActiveConnection: Boolean, suggestedEntity: WidgetDataType? = null): RemoteViews

// A map of widget IDs to [server ID, list of entity IDs]
abstract suspend fun getAllWidgetIdsWithEntities(context: Context): Map<Int, Pair<Int, List<String>>>
abstract fun saveEntityConfiguration(context: Context, extras: Bundle?, appWidgetId: Int)
abstract suspend fun onEntityStateChanged(context: Context, appWidgetId: Int, entity: Entity<*>)
}
Loading